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.0pxwhen no keyboard is showing.--va-offset-xis 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
- Open your assistant, go to the Appearance tab.
- Scroll to Custom CSS and enter your rules in Custom CSS Rules (optional).
- 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@importor@charsetexpression(,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-faceand@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 —
marginand its four sides,paddingand 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.
Related
- Embedding the widget — the embed snippet and its attributes.
- JavaScript API — controlling the widget at runtime.
- Appearance — the settings behind these variables.