skip to main content
Browse documentation

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-family

Light & 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);
}
Restyle the panel header
/* 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; }
Override a token for one primitive only
/* 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; }
}
Tune motion, respecting reduced-motion

Next steps