> ## Documentation Index
> Fetch the complete documentation index at: https://help.statisfy.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Theming the Statisfy SDK

> Restyle the SDK with --dw-* CSS variables and data-dw-part anatomy selectors, and opt into the built-in dark palette with data-dw-theme.

Everything visual in the SDK is reachable from your CSS through two seams:

1. **CSS variables** — \~31 `--dw-*` tokens covering colors, radius, elevation, focus rings, status colors, and motion. Set one and every element that uses it follows.
2. **Anatomy selectors** — every styled element carries a `data-dw-part` attribute naming its role. Target one to restyle exactly that element.

Both work with plain declarations. **You never need `!important`, and you should not use it.** All SDK CSS ships inside `@layer` (`theme, base, components, utilities`), and an unlayered rule in your own stylesheet outranks every layer regardless of specificity or import order. `!important` inverts layer order, so it makes the SDK's own rules win again — the opposite of what you want.

<Note>
  The theming contract described here ships in both `@statisfy/portal-react` and `@statisfy/ai-react`, from **0.1.0**. The two stylesheets carry an identical token layer, so a page that imports both is styled once, not twice. The light palette is the default, and dark mode stays off until you [opt in](#dark-mode-opt-in).
</Note>

## Override a variable

Declare the variable in an unlayered rule — `:root`, or any scope you want it confined to. It holds no matter which stylesheet loads first.

```css theme={null}
:root { --dw-primary: 13 148 136; }

/* or scope it to one mount point */
.support-drawer { --dw-primary: 13 148 136; }
```

<Warning>
  Color values are **space-separated RGB triplets**, not `rgb()` or hex — that's what lets the SDK apply opacity internally. Write `13 148 136`, never `#0d9488`.
</Warning>

### Colors

| Variable                    | Default       | Used for                                        |
| --------------------------- | ------------- | ----------------------------------------------- |
| `--dw-background`           | `255 255 255` | primary surface                                 |
| `--dw-background-secondary` | `248 248 249` | headers, inset surfaces                         |
| `--dw-foreground`           | `26 26 26`    | body text                                       |
| `--dw-muted`                | `241 241 242` | hover fills, agent bubble, badges               |
| `--dw-muted-foreground`     | `107 114 128` | secondary text, timestamps                      |
| `--dw-primary`              | `83 35 181`   | brand fill: send button, progress, current step |
| `--dw-primary-foreground`   | `255 255 255` | text on `--dw-primary`                          |
| `--dw-border`               | `229 231 235` | hairlines, dividers, table cells                |
| `--dw-input`                | `209 213 219` | form control borders                            |
| `--dw-destructive`          | `220 38 38`   | errors, failed tool calls                       |
| `--dw-success`              | `22 128 90`   | completed states                                |
| `--dw-warning`              | `180 83 9`    | "Awaiting reply", pending states                |

If you re-point `--dw-primary`, keep it readable against `--dw-primary-foreground` (aim for at least 4.5:1) — the pair is used both as a solid fill under inverse text and as link ink on the page.

### Focus ring

One rule draws the focus ring for every interactive part.

| Variable            | Default                                                           |
| ------------------- | ----------------------------------------------------------------- |
| `--dw-ring`         | `var(--dw-primary)` (triplet — inherits your brand automatically) |
| `--dw-focus-width`  | `2px`                                                             |
| `--dw-focus-offset` | `2px`                                                             |

### Elevation

`--dw-shadow-color` is the shadow ink; the four steps are complete `box-shadow` strings. Set a step to `none` to flatten that tier everywhere.

| Variable            | Used for                                 |
| ------------------- | ---------------------------------------- |
| `--dw-shadow-color` | shadow ink (triplet), default `15 23 42` |
| `--dw-elev-1`       | chips, badges, widget root, project card |
| `--dw-elev-2`       | cards, task groups, form card            |
| `--dw-elev-3`       | toast                                    |
| `--dw-elev-4`       | drawer, lightbox                         |

### Overlay

The modal scrim. These take full color values, not triplets — the scrim carries its own alpha.

| Variable            | Default                |
| ------------------- | ---------------------- |
| `--dw-overlay`      | `rgb(15 23 42 / 0.55)` |
| `--dw-overlay-blur` | `2px`                  |

### Radius

| Variable         | Default    | Used for                           |
| ---------------- | ---------- | ---------------------------------- |
| `--dw-radius-sm` | `0.375rem` | chips, inline code, thumbnails     |
| `--dw-radius-md` | `0.5rem`   | inputs, buttons, rows              |
| `--dw-radius-lg` | `0.75rem`  | cards, bubbles, toast, task groups |
| `--dw-radius-xl` | `1rem`     | widget root, task drawer           |

There is no `--dw-radius-full`: pills, dots, and avatars are fully round by design and aren't on the radius scale. To change one, target its [part](#target-one-element).

### Motion

| Variable                                                           | Default                              |
| ------------------------------------------------------------------ | ------------------------------------ |
| `--dw-default-transition-duration`                                 | `160ms`                              |
| `--dw-default-transition-timing-function`                          | `cubic-bezier(0.2, 0.7, 0.2, 1)`     |
| `--dw-animate-spin` / `--dw-animate-pulse` / `--dw-animate-bounce` | standard spin/pulse/bounce keyframes |

Under `prefers-reduced-motion: reduce` the SDK stills its own animations for the whole widget, even if you've set a custom transition duration. You don't need to add your own media query.

## Dark mode (opt-in)

The SDK ships a dark palette as of 0.2.0. It's **opt-in**: an embedded widget should match the page it's mounted in, not the visitor's OS, so nothing changes until you set the `data-dw-theme` attribute on your `html` element.

```html theme={null}
<html data-dw-theme="dark">   <!-- always dark -->
<html data-dw-theme="auto">   <!-- follow the visitor's OS -->
<!-- attribute absent: light, same as 0.1.x -->
```

| Your setup                     | OS light | OS dark |
| ------------------------------ | -------- | ------- |
| no `data-dw-theme`             | light    | light   |
| `data-dw-theme="dark"`         | dark     | dark    |
| `data-dw-theme="auto"`         | light    | dark    |
| full unlayered `:root` palette | yours    | yours   |

The dark palette re-points the color tokens (backgrounds, borders, status colors, `--dw-shadow-color`, `--dw-overlay`) plus `--dw-color-scheme`, which switches the browser-painted controls — checkbox ticks, `<select>` arrows, scrollbars — inside the widget without touching your page.

Both dark blocks live inside `@layer theme`, exactly like the light defaults, so your own unlayered `:root` declarations still win at any OS setting.

<Warning>
  If you opt into dark mode with a **partial** custom palette, own every color token or none of them. A light-tuned `--dw-primary` on the SDK's dark grounds becomes unreadable, and the SDK can't detect a half-set palette to compensate. In particular, if you re-point `--dw-primary` for dark, re-point `--dw-primary-foreground` with it.
</Warning>

## Target one element

Every styled element carries a `data-dw-part` attribute naming its role in the anatomy. Select it directly — an attribute selector has the same specificity as a class, and the same unlayered-wins rule applies.

```css theme={null}
[data-dw-part='task-row'] { border-radius: 0; }
```

Some parts also carry state attributes you can key off: `data-dw-role` (`user|agent`), `data-dw-state` (`loading|error|empty|ready`), `data-dw-status` (slugified status value), `data-dw-step` (`done|current|upcoming`), `data-dw-selected`, `data-dw-unread`, `data-dw-busy`, `data-dw-level` (`1`–`6`), `data-dw-held`, `data-dw-field`, `data-dw-type`, `data-dw-empty`.

```css theme={null}
[data-dw-part='chat-bubble'][data-dw-role='user'] { background: #0d9488; }
[data-dw-part='journey-marker'][data-dw-step='done'] { background: #ecfdf5; }
```

`data-dw-status` carries your own status value, lowercased with runs of separators collapsed to a single `-`. Letters and digits survive in any script: `In Progress` → `in-progress`, `進行中` → `進行中`.

### Guaranteed part names

These 74 names are the public contract and won't be renamed without a major version. The SDK renders more `data-dw-part` values than these; the rest exist in the DOM but aren't yet guaranteed, so style them at your own risk.

`attachment-chip` · `attachment-list` · `auth-card` · `auth-input` ·
`auth-submit` · `chat-bubble` · `chat-composer` · `chat-error` · `chat-header` ·
`chat-input` · `chat-name` · `chat-pane` · `chat-root` · `chat-row` ·
`chat-send` · `chat-toast` · `chat-tool-chip` · `comment` · `comment-body` ·
`comment-composer` · `comment-input` · `comment-submit` · `comments` ·
`comments-list` · `field` · `field-input` · `field-label` · `field-value` ·
`form-card` · `form-submit` · `history-list` · `history-root` · `history-row` ·
`journey` · `journey-marker` · `journey-step` · `journey-title` ·
`lightbox-overlay` · `lightbox-panel` · `md-blockquote` · `md-code-inline` ·
`md-heading` · `md-link` · `md-list` · `md-p` · `md-pre` · `md-root` ·
`md-table` · `messages` · `picker-button` · `picker-chip` · `progress` ·
`progress-fill` · `progress-track` · `progress-value` · `project-card` ·
`project-error` · `project-header` · `project-title` · `status-badge` ·
`status-dot` · `status-notice` · `status-select` · `status-select-control` ·
`task-detail-backdrop` · `task-detail-body` · `task-detail-panel` ·
`task-detail-save` · `task-detail-title` · `task-group` · `task-group-header` ·
`task-list` · `task-row` · `task-row-title`

## Examples

**Recolor to your brand.** Two variables move the majority of the surface; `--dw-ring` follows `--dw-primary` automatically.

```css theme={null}
:root {
  --dw-primary: 13 148 136;
  --dw-primary-foreground: 255 255 255;
}
```

**Go square and flat.** Radius and elevation both run to zero — no part selectors needed.

```css theme={null}
:root {
  --dw-radius-sm: 0;
  --dw-radius-md: 0;
  --dw-radius-lg: 0;
  --dw-radius-xl: 0;

  --dw-elev-1: none;
  --dw-elev-2: none;
  --dw-elev-3: none;
  --dw-elev-4: none;
}
```

**Restyle one element.** No variable covers "make task rows a card"; reach for the part instead.

```css theme={null}
[data-dw-part='task-row'] {
  margin-block: 4px;
  border-top: 0;
  border-radius: var(--dw-radius-md);
  box-shadow: var(--dw-elev-1);
}
```

## Caveats

1. **Overriding `background` or `color` on a part also overrides that element's `:hover` and `:focus-visible` fill.** Your unlayered rule beats the SDK's layered rule in every state, not just the resting one, so re-declare the states you still want:

   ```css theme={null}
   [data-dw-part='task-row'] { background: #fafafa; }
   [data-dw-part='task-row']:hover { background: #f0f0f0; }
   ```

   Overriding `border-radius` or `box-shadow` has no such side effect.

2. **A few radii are not tokenized.** Pills, dots, and avatars are fully round with no `--dw-radius-full` behind them. To change one, target its part.
