Custom CSS
The escape hatch: restyle any widget surface with raw CSS when the studio's token controls don't reach far enough.
The Widget Design Studio's token controls cover most branding needs โ they stay AA-safe and survive widget updates. When they don't reach far enough, each design carries a Custom CSS field (General step): raw CSS injected into the widget's Shadow DOM, targeting the stable selectors and --syncanix-* custom properties documented on this page.
How it applies
The CSS is validated as you type and re-validated server-side on save. It goes live when you save the design; if that design is the one in use, embedded widgets pick the change up on their next config fetch โ within a few minutes.
One CSS string per design: it applies across every widget size and both colour schemes (scope dark-only rules with the selector below), and per-page theme rules never carry their own custom CSS.
Your CSS wins the cascade
The widget injects your CSS as the last stylesheet in its Shadow DOM, after every built-in style. A rule of equal specificity therefore overrides the widget's own โ no !important needed for ordinary restyling.
/* Does NOT work โ tokens are applied inline on the host element,
which beats any :host rule: */
:host { --syncanix-primary: #0f766e; }
/* Works โ re-declare the variable on a descendant instead: */
.syncanix-panel { --syncanix-primary: #0f766e; }Stable selectors
These selectors are a documented, stable contract โ pinned by tests, so a widget release can't rename them silently.
/* Launcher (the closed floating button โ an attribute, not a class) */
[data-syncanix='launcher']
.syncanix-launcher-avatar
.syncanix-launcher-brand
/* Panel chrome */
.syncanix-panel
.syncanix-panel-header
/* Messages */
.syncanix-panel-item-user
.syncanix-panel-item-assistant
.syncanix-msg-body
/* Composer */
.syncanix-panel-composer
.syncanix-panel-composer-form
.syncanix-panel-composer-input
.syncanix-panel-composer-submit
.syncanix-panel-composer-stop
.syncanix-panel-composer-limit
/* Rich primitives โ each renders with its type on the root */
.syncanix-card
.syncanix-confirm-card
[data-syncanix-primitive='list' | 'table' | 'card' | 'markdown' | 'button'
| 'detail' | 'form' | 'status' | 'empty-state' | 'confirm-card' | 'navigate'
| 'component' | 'iframe' | 'action-plan' | 'action-in-progress'
| 'action-result' | 'chart' | 'embed' | 'metric' | 'funnel' | 'flow'
| 'sign-in-card' | 'onboarding-checklist']Custom properties
Every widget surface paints from namespaced --syncanix-* custom properties, so re-declaring one on a subtree restyles just that subtree. The same token names are accepted by the data-theme embed attribute โ see Theming the widget for how the theme layers compose.
/* Colour */
--syncanix-primary --syncanix-primary-foreground
--syncanix-background --syncanix-foreground
--syncanix-muted --syncanix-muted-foreground
--syncanix-border --syncanix-ring
--syncanix-destructive --syncanix-destructive-foreground
--syncanix-link
/* Typography */
--syncanix-font-family --syncanix-font-family-heading
--syncanix-line-height-base --syncanix-letter-spacing-base
--syncanix-measure
/* Shape */
--syncanix-radius --syncanix-radius-sm
--syncanix-radius-md --syncanix-radius-lg
--syncanix-radius-full --syncanix-avatar-radius
/* Elevation, spacing, motion, icons */
--syncanix-shadow-lg --syncanix-shadow-xl
--syncanix-spacing --syncanix-spacing-sm
--syncanix-motion-duration --syncanix-motion-easing
--syncanix-icon-stroke-width
/* Per-element studio overrides (set on a descendant to override locally) */
--syncanix-launcher-bg --syncanix-launcher-fg
--syncanix-header-bg --syncanix-header-fg
--syncanix-user-bubble-bg --syncanix-user-bubble-fg
--syncanix-assistant-bubble-bg --syncanix-assistant-bubble-fg
--syncanix-composer-bg --syncanix-pane-bg
--syncanix-ai-font-family --syncanix-user-font-familyLight & dark
The resolved colour scheme is stamped on the widget host as data-color-scheme. Your CSS applies in both schemes โ scope dark-only rules with :host([data-color-scheme='dark']).
.syncanix-panel-header { background: #f8fafc; }
:host([data-color-scheme='dark']) .syncanix-panel-header {
background: #1e293b;
}What's blocked
Custom CSS is validated in the editor, on save, and again in the widget before injection. The validator refuses:
- @import โ external CSS fetch / exfiltration.
- expression(), javascript: and data:text/html URLs, behavior:, -moz-binding โ legacy script-injection vectors.
- External url() references โ a network beacon / data-leak vector. Embed assets (including font files) as data: URIs instead.
- A </style> close tag โ a Shadow-DOM escape.
- Hiding or restyling the "made with AI" disclosure (syncanix-ai-* / syncanix-first-message-* selectors) โ a locked regulatory surface.
- More than 64 KB of CSS per design.
Examples
/* A gradient panel header with a hairline divider */
.syncanix-panel-header {
background: linear-gradient(135deg, #0f766e, #134e4a);
border-block-end: 1px solid rgba(255, 255, 255, 0.12);
}/* Round only the cards, and give tables a tighter radius */
[data-syncanix-primitive='card'] { --syncanix-radius: 1rem; }
[data-syncanix-primitive='table'] { --syncanix-radius: 0.25rem; }/* Slow the widget's motion down, and switch it off for
reduced-motion visitors */
.syncanix-panel { --syncanix-motion-duration: 300ms; }
@media (prefers-reduced-motion: reduce) {
.syncanix-panel { --syncanix-motion-duration: 0ms; }
}