Documentation

Theming and custom CSS

How to restyle the widget on your site: what the widget sets for you automatically, which CSS custom properties and class names you can rely on, where custom CSS goes, and which techniques do not work. Everything here is what the shipping widget actually does.

How the widget renders

The widget builds its DOM inside your page — there is no iframe and no shadow root. That means your own stylesheet can target it, and your page's CSS resets can reach it too.

On load it appends one <style id="va-wave-widget-styles"> element to <head> containing the base widget CSS, then a second <style id="va-wave-widget-custom-styles"> element containing your saved custom CSS. Because the custom sheet comes last, your rules win against base rules of equal specificity. They do not win against a more specific base rule, and they never win against the inline values the widget writes on the container (see Overriding variables).

The root element is div.va-container, appended to <body> with z-index: 2147483600.

Automatic host-theme detection

The widget picks light or dark for itself by reading your page, not by a setting. On startup it walks up from <body> looking for the first element with a non-transparent background colour, converts it to sRGB, and computes its WCAG relative luminance. Luminance above 0.5 means a light page; at or below means dark. If every ancestor is transparent it falls back to the operating system's prefers-color-scheme.

The result is written to the container as attributes you can hook:

Attribute Values Set when
data-va-theme light or dark Always present
data-va-host-theme light Only on a light page; removed on a dark one
data-va-orb-palette present (empty value) Effectively always — it is set when an orb background or ring colour is present, and the init response always supplies a ring colour

Detection re-runs automatically when the class or data-theme attribute changes on <html> or <body>, and when the OS colour scheme changes — so a widget on a page with a light/dark toggle follows the toggle without any work from you.

/* Your page's stylesheet */
.va-container[data-va-theme=light] .va-branding-badge { opacity: 0.9; }

Warning

In the Custom CSS field, attribute selectors must be written without quotes ([data-va-theme=light], not [data-va-theme="light"]). Quotes in a selector cause the whole rule to be discarded — see What the Custom CSS field accepts.

Class hooks

These class names are set by the widget's own JavaScript and are the stable surface to target.

Container state

All of these appear on .va-container.

Class Meaning
pos-bottom-right, pos-bottom-left, pos-top-right, pos-top-left Configured corner
va-loading / va-ready Before / after the entry animation
chat-mode Chat is open
voice-mode A voice session is active
minimized Collapsed to the name chip
va-mobile-chat Chat is in the small-viewport takeover layout
voice-cue-on Voice is enabled, so the mic satellite is shown
va-style-waves The wave skin is active
va-waves-ambient Ambient corner treatment (pill and gear, no band)
fullscreen-mode / fs-collapsed Minimal presentation, expanded / collapsed
va-fs-side-left / va-fs-side-right Which side the Minimal surface docks to
va-fs-idle Minimal surface loaded, no conversation yet
va-fs-showband The optional full-width waves band is on

Orb state

.va-orb always carries exactly one state class, rebuilt on every state change:

idle · listening · processing · loading · speaking · error

It also carries voice while a voice session is running, plus no-logo when no logo is configured, always-show-logo when the logo stays visible behind the waveform, and no-waveform when the waveform animation is turned off.

.va-container .va-orb.listening .va-orb-bg { border-radius: 40%; }

Elements

Selector What it is
.va-orb The orb button
.va-orb-glow, .va-orb-bg, .va-orb-inner Glow halo, ring layer, interior surface
.va-logo Logo image inside the orb
.va-waveform Waveform canvas inside the orb
.va-voice-cue Mic satellite next to the orb
.va-name Assistant name chip
.va-launcher Legacy launcher button (a chat-bubble icon appended to the container); it is hidden in the minimized state, where the name chip acts as the launcher
.va-chat-backdrop Frosted chat panel — the parent of the message list
.va-chat-header, .va-header-btn Chat header and its buttons
.va-chat-close Close button in the composer
.va-history Scrolling message list
.va-history-message One message row
.va-history-user / .va-history-assistant Role modifier on a message row
.va-chat-input-wrapper Composer pill
.va-chat-input Composer text field
.va-voice-input-btn, .va-fs-send-btn Mic and send buttons inside the composer
.va-transcript Voice caption bubble
.va-voice-controls Voice control shelf
.va-status-stack Transient tool-status stack
.va-waves-hint, .va-waves-band Waves pill and the bottom waves band
.va-form-panel Form panel rendered inside the widget
.va-branding-badge, .va-brand-name "by hiroi" badge (hidden when branding is off)

Warning

Some widget elements also carry classes beginning with hi- (for example hi-chip on the name chip). Those come from the internal design system and are regenerated wholesale every time the design system is synced. They are not a stable API — never target a hi- class. Use the va- hook on the same element.

CSS custom properties the widget writes

The widget writes these properties inline on .va-container from the assistant's appearance settings. The clamp column is what the widget enforces in the browser; values outside it are pulled to the nearest bound.

Property Source Clamp Default
--va-size Orb diameter 20–200 (enforced when saving) 72px
--va-primary, --va-secondary Brand colours (also set on .va-orb) — #8b5cf6, #6d28d9
--va-idle-color Idle state colour — brand primary
--va-listening-color Listening state colour — #22c55e
--va-speaking-color Speaking state colour — #3b82f6
--va-processing-color Processing state colour — #f59e0b
--va-ring-color Orb ring colour — always written inline, because the init response supplies #ffffff when no ring colour is configured, so the stylesheet's light/dark ring defaults never apply and overriding it from your own CSS needs !important — #ffffff
--va-glow-intensity Glow strength — 0.4
--va-font-family Resolved font stack for the chosen font — system stack
--va-font-size Orb-area text size — 13px
--va-chat-font-size Chat text size — 13px
--va-text-width Caption max width 200–1200 500px
--va-offset-x Distance from the side edge 20–500 24px
--va-offset-y Distance from the top/bottom edge 20–500 100px
--va-chat-width Chat panel width 200–800 320px
--va-chat-height Chat panel height 200–900 420px
--va-chat-input-width Composer width at least 200 matches chat width
--va-fullscreen-orb-size Orb size on the Minimal surface fixed 140px

Three more are written at runtime rather than from settings:

  • --va-chat-history-max — height available to the message list while the panel is being resized.
  • --va-kb-inset — height of the on-screen keyboard on mobile, so the composer can lift above it. 0px when no keyboard is showing.
  • --va-offset-x is recalculated upward when the name chip is wider than the orb, to keep the centred name from running off the viewport edge.

When a visitor drags the chat panel's resize handle, the widget overwrites --va-chat-width, --va-chat-input-width, --va-chat-font-size and --va-chat-history-max and remembers the size in that browser. The panel is clamped to a 280px minimum in both axes and to the viewport.

The base stylesheet declares many more variables on .va-container — spacing, radii, shadow stacks, the glass material, easing curves. Those are internal, they differ between light and dark, and they change between releases. Do not build on them.

Where custom CSS goes

  1. Open your assistant, go to the Appearance tab.
  2. Scroll to Custom CSS and enter your rules in Custom CSS Rules (optional).
  3. Click Save.

Your CSS is delivered with the widget's configuration and injected after the base stylesheet on every page load. There is nothing to add to your embed snippet.

Scope every rule to .va-container or one of the hooks above. The stylesheet lives in your page's <head>, so an unscoped selector like button { … } will restyle your own site.

.va-container .va-chat-backdrop { border-radius: 4px; }
.va-container .va-history-user { background: #1d4ed8; color: #ffffff; }
.va-container .va-chat-input { font-size: 15px; }

What the Custom CSS field accepts

Custom CSS is filtered before it reaches the browser, and the filter is strict. Read this before writing anything non-trivial — most of these failures are silent.

Rejected outright when you save — you get an error and the save fails: javascript:, expression(…), behavior:, -moz-binding, @import, and url() pointing at a javascript: or data:text/html target.

Discards your entire custom CSS when the widget loads — no error anywhere, the stylesheet just comes out empty:

  • more than 50,000 characters
  • url( anywhere in the sheet, for any reason
  • @import or @charset
  • expression(, javascript:, behavior:, -moz-binding
  • a backslash escape sequence such as \2014

Dropped rule by rule, leaving the rest of your CSS intact:

  • any selector containing >, <, " or ' — so no child combinators, and attribute selectors must be unquoted
  • any at-rule with a nested block, including @media, @supports, @font-face and @keyframes
  • any declaration whose property is not on the allowed list below, including every custom property (--anything)

Comments are stripped safely, and !important is preserved.

The allowed properties are exactly:

  • Colour and text — color, background, background-color, border-color, outline-color, text-decoration-color, font-family, font-size, font-weight, font-style, line-height, letter-spacing, text-align, text-transform, text-decoration, text-indent, word-spacing
  • Box — margin and its four sides, padding and its four sides, border, border-width, border-style, border-radius, border-top, border-right, border-bottom, border-left, width, max-width, min-width, height, max-height, min-height
  • Layout — display, flex, flex-direction, flex-wrap, justify-content, align-items, align-content, gap, order, flex-grow, flex-shrink, grid, grid-template, grid-gap, position, top, right, bottom, left, z-index
  • Other — opacity, visibility, overflow, overflow-x, overflow-y, box-shadow, cursor, transition, transition-property, transition-duration, transition-timing-function, transition-delay

Anything not on that list is silently removed. That includes transform, filter, backdrop-filter, animation, background-image, content, grid-template-columns and aspect-ratio.

Overriding variables from your own stylesheet

Because custom properties cannot pass the Custom CSS filter, the only way to change a --va-* variable is a rule in your own site's stylesheet, which the widget does not filter.

The widget writes most of these variables inline on the container, and an inline declaration beats a normal stylesheet rule. Use !important to win:

/* In your site's own CSS, not the Custom CSS field */
.va-container {
  --va-primary: #0f766e !important;
  --va-listening-color: #16a34a !important;
}

This is a page-by-page override. It applies wherever that stylesheet loads and nowhere else, so prefer the Appearance settings when you want the change everywhere. See Appearance for the settings themselves.

What does not work

Old integration notes and third-party snippets circulate with instructions that were never accurate or no longer are. None of the following has any effect:

Technique Why it fails
--va-bg-color / --va-text-color to recolour the chat The widget explicitly removes both inline properties every time it applies settings. The chat surface and text colour are owned by the detected host theme so the panel stays legible on any page.
--va-shadow No such variable. The shadow stack is --va-shadow-sm, --va-shadow-md, --va-shadow-lg and --va-shadow-orb, and those are internal.
.va-send-btn No such class. The send button is .va-fs-send-btn, inside .va-chat-input-wrapper.
.va-input No such class. The text field is .va-chat-input.
Custom properties in the Custom CSS field Custom properties are not on the allowed property list and are dropped.
@media blocks in the Custom CSS field At-rules with nested blocks do not survive the filter. Target the container state classes instead, or put media queries in your own stylesheet.
Web fonts or background images via url() Any url( discards the whole custom sheet. Load the font on your page and reference the family by name.
Targeting hi- classes Regenerated on every design-system sync.

Setting a chat background or text colour in the assistant's colour settings does not repaint the chat panel either — the values themselves are no longer applied anywhere. The only remaining side effect is that a chat background colour contributes to data-va-orb-palette, which turns off the frosted orb treatment used on light pages; a chat text colour has no effect at all.