# Tokens

> The semantic token vocabulary in tokens.css — colour roles, sizes, shapes and motion — how dark mode is mapped, and what the design-system passes refuse.

Page: https://sdk.chatfuel.com/docs/reference/ui/tokens
Markdown: https://sdk.chatfuel.com/docs/reference/ui/tokens.md

Tokens are semantic — a role, never a measurement — so a rebrand is one edit per role rather than a
hunt through call sites, and a component that only ever names roles themes itself.

## The colour roles [#the-colour-roles]

| Group             | Tokens                                                                                                                                                        | What they are for                                                                                                                                                                                                                                             |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Surfaces          | `--color-surface`, `-raised`, `-sunken`, `-hover`, `-overlay`, `-inverse`, `--color-translucent`, `-strong`                                                   | The page, then cards and chrome above it, inset rows below it, the panel a popover draws, and the dark body a tooltip uses. The two translucent fills are alpha rather than a grey step, so one value works over any surface in the ramp.                     |
| Borders           | `--color-border`, `-strong`, `-subtle`                                                                                                                        | The default hairline, the heavier rule a table column or a sticky header needs, and the quiet one inside a card.                                                                                                                                              |
| Text              | `--color-text`, `-muted`, `-faint`, `-inverse`                                                                                                                | Body, secondary, placeholder and timestamp, and text on `surface-inverse`.                                                                                                                                                                                    |
| Accent and status | `--color-accent`, `-hover`, `-soft`, `-fg`; `--color-danger`, `-success`, `-warning`, `-info`, each with a `-soft`                                            | Accent means "selected" everywhere in the system. Each status tone carries a soft companion for the fill behind it.                                                                                                                                           |
| Interaction       | `--color-focus`, `-scrim`, `-row-hover`, `-row-selected`, `-row-selected-hover`, `-skeleton`, `-skeleton-highlight`, `-control-knob`                          | The focus outline, the dialog scrim, table row states, the loading shimmer, and the switch thumb that stays light on both tracks.                                                                                                                             |
| Pipeline          | `--color-pipeline-1` … `-6`                                                                                                                                   | An ordered cool-to-warm ramp, six values because a sales pipeline has six stages. Terminal stages keep `success` and `danger` instead.                                                                                                                        |
| Canvas            | `--color-canvas`, `-grid`, `--color-edge`, `-hover`, `-selected`, `-ghost`, `--color-handle`, `-active`, `--color-selection-fill`, `-stroke`, `--color-guide` | A canvas is the one surface explicitly behind everything, and the only place a 1px line must stay readable at 10% zoom and quiet at 250%. The guide is deliberately not the accent: two accent lines meeting at a selected node is one line too many to read. |
| Chat              | `--color-bubble-in`, `-in-fg`, `--color-bubble-out`, `-out-fg`                                                                                                | The two bubble sides, each with its own foreground.                                                                                                                                                                                                           |
| Run               | `--color-run-active`, `-active-soft`, `--color-run-skipped`, `-skipped-soft`                                                                                  | A tool call's step card. `running` is not `info` — four blue alerts stacked in a thread is a page of warnings about nothing — and `skipped` is not `text-faint`, because "this did not run" is a state rather than a de-emphasis.                             |
| Event palette     | `--color-event-1` … `-8`, each with `-soft` and `-fg`                                                                                                         | Eight nominal tones for calendar events: a solid for a bar or a dot, a soft for a block fill, a foreground for text on it. Spread round the wheel rather than stepped along it, and indigo is absent because indigo means "selected".                         |
| Calendar          | `--color-now`, `--color-off-hours`, `--color-busy`, `-soft`, `--color-blocked`, `--color-available`, `-soft`                                                  | The now-line is a landmark, not a warning, so it is not `danger`: a rebrand that cools the danger red must be able to leave the clock hand alone.                                                                                                             |
| Channels          | `--color-channel-instagram`, `-whatsapp`, `-facebook`, `-tiktok`, `-widget`, each with `-soft`                                                                | One pair per messaging platform — glyph and the tinted circle behind it. Brand-adjacent, not brand-exact: every solid clears 3:1 on its own soft in both themes. Widget is the product's own channel, so it is the accent.                                    |

## Sizes, shapes and motion [#sizes-shapes-and-motion]

| Group                | Tokens                                                                                                                                                                                                         | Notes                                                                                                                                                                                                                                                                                                                                         |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Radius               | `--radius-control`, `-card`, `-chip`, `-bubble`, `-island`, `-pill`                                                                                                                                            | Roles, not a t-shirt scale. 6px reads as a control at field height where 8px reads as a pill.                                                                                                                                                                                                                                                 |
| Elevation            | `--shadow-raised`, `-overlay`, `-modal`, `-drag`, `-island`, `--shadow-card-inset`, `--shadow-secondary-button`                                                                                                | Tints go through plain `:root` variables, so the dark theme re-tints all of them in three lines. A card is a surface step plus a 1px inset ring, not a lifted plane.                                                                                                                                                                          |
| Motion               | `--transition-duration-instant` (80ms), `-fast` (120ms), `-base` (180ms), `-slow` (260ms), `--transition-delay-tooltip` (400ms), `--ease-standard`, `-entrance`, `-exit`, `-spring`, and the `--animate-*` set | Entrances decelerate, exits accelerate. The `--animate-*` values inline their easing because the keyframe extractor scans them literally.                                                                                                                                                                                                     |
| Z-index              | `--z-index-sticky` (10), `-island` (15), `-rail` (20), `-dropdown` (30), `-drag` (40), `-overlay` (50), `-popover` (60), `-toast` (70), `-tooltip` (80)                                                        | One ordered ladder so no component invents a number. The namespace is `--z-index-*`, not `--z-*`; that is what makes Tailwind emit `z-popover`.                                                                                                                                                                                               |
| Chrome heights       | `--height-topbar`, `-field`, `-field-sm`, `-row-compact`, `-row-cozy`, `-row-comfortable`, `-touch`                                                                                                            | `--height-touch` is 44px, the WCAG 2.5.8 AAA target, opt-in through the `touch-target` utility — never by retuning `--height-field` under `(pointer: coarse)`.                                                                                                                                                                                |
| Time grid            | `--height-hour-compact`, `-cozy`, `-comfortable`, `--height-event-min`                                                                                                                                         | Pixels, and mirrored in `lib/time/timeGrid.ts`: the event geometry is arithmetic on the same numbers the CSS draws with, and a test parses this file to prove they agree. `rem` would break that.                                                                                                                                             |
| Band thresholds      | `--container-compact` (600px), `--container-wide` (900px), `--container-inline` (1280px)                                                                                                                       | The same three numbers as the `Band` boundaries in `lib/interaction/layout.ts`, asserted by a test that parses this file. They generate both `max-w-*` and the `@compact:` / `@wide:` / `@inline:` container variants. Pixels, because container queries in `rem` resolve against the root font size while a `ResizeObserver` reports pixels. |
| Content measures     | `--container-drawer`, `-auth`, `-auth-wide`, `-form`, `-composer`, `-prose`, `-app`                                                                                                                            | Max-widths for content columns, in the same namespace.                                                                                                                                                                                                                                                                                        |
| Layout widths        | `--width-rail`, `-rail-expanded`, `-nav-panel`, `-sidenav`, `-list`, `-inspector`, `-panel`, `-column`, `-column-rail`, `-time-gutter`, `-time-column`                                                         | Semantic roles again: `--width-list` and `--width-inspector` being equal today is a coincidence, not a synonym. `w-*` reads this namespace; `max-w-*` does not, so a max-width has to be written `max-w-[var(--width-…)]`.                                                                                                                    |
| Spacing              | `--spacing-gutter`, `-tight`, `-loose`                                                                                                                                                                         | `p-gutter` compiles to the variable, so a band scope re-assigns the variable and every `p-gutter` and `gap-gutter` already written below it becomes fluid.                                                                                                                                                                                    |
| Viewport breakpoints | `--breakpoint-sm` … `-2xl`                                                                                                                                                                                     | Tailwind's stock values, declared explicitly. They drive shell chrome only — a module can be 700px wide inside a 2560px viewport, so the viewport answers the wrong question.                                                                                                                                                                 |
| Type                 | `--font-sans`, `--font-mono`, `--font-display`, `--font-weight-*`, and the roles `--text-title`, `-heading`, `-body`, `-label`, `-meta`, `-micro`, `-nano`                                                     | Size and line height are baked; weight is not, so `text-sm font-semibold` still means what it says. The upper register (`--text-title-4` … `--text-display`) is for auth screens and empty states. Tailwind's `text-xs` / `text-sm` / `text-base` are aliased onto the same ramp so two scales cannot run side by side.                       |
| Utilities            | `focus-ring`, `skeleton`, `touch-target`                                                                                                                                                                       | `focus-ring` uses `outline`, not a `ring` box-shadow: data-dense screens are full of `overflow-hidden` containers that clip a shadow, and an outline is never clipped.                                                                                                                                                                        |

## How dark mode works [#how-dark-mode-works]

The dark palette is declared once in a plain `:root` block, and then mapped onto the semantic names
twice: once under `@media (prefers-color-scheme: dark)` scoped to `:root:not([data-theme])`, and
once under `[data-theme='dark']`. An explicit choice — either value — disables the media rule, and
the attribute selector is deliberately not `:root`-prefixed so an embedding host can put
`data-theme` on its own wrapper element instead of on `<html>`.

Duplicating the map is the point. `light-dark()` would end up nested inside the `color-mix()`
Tailwind compiles opacity modifiers to: `bg-accent/50` becomes a `color-mix()` over the variable,
guarded by `@supports`, with a hex fallback baked from the light value at build time. The two blocks
are kept honest by a validate pass that fails if either declares a property the other does not.

`@theme` compiles into `@layer theme`, and unlayered rules beat layered ones regardless of source
order, so these overrides win wherever the app places the `@import` — no `!important` and no
ordering fragility. `color-scheme` is set in both blocks and is not cosmetic: `forms/Select` is a
real native `<select>`, and without it the dropdown, the scrollbars and autofill stay light.

<Callout type="info">
  No component carries a `dark:` variant and none hardcodes a colour, so theming — dark mode
  included — is a tokens-only edit to this one file. Reduced motion works the same way: the
  `prefers-reduced-motion` block retunes `--transition-duration-*` and the `--animate-*` set rather
  than resetting animations globally, which would also freeze the spinner.
</Callout>

## Why `@theme static` [#why-theme-static]

`@theme static` keeps every variable in `:root` even when no utility currently uses it. Without it
Tailwind tree-shakes unused theme variables, and the design-system gallery — which reads token
values back with `getComputedStyle` — renders them as a phantom em dash. That is also why Tailwind's
own breakpoint values are re-declared here: their defaults live in a non-static `@theme` and get
shaken out.

## What the passes enforce [#what-the-passes-enforce]

`pnpm validate` runs these; pass 11 is the design system's own.

| Rule                      | What fails                                                                                                                                                                                                                                                                           |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 11(a) dark-block parity   | A property declared in one dark block and not the other. The two are duplicated on purpose, and this is what keeps the duplicate in lockstep.                                                                                                                                        |
| 11(b) no raw colour       | `#rgb`, `rgb(`, `rgba(`, `hsl(` or `hsla(` in any `.ts`/`.tsx` under `packages/ui/src`, outside `icons/` where the match would be SVG path data. `Avatar` is the one component that needs a ramp, and it renders `oklch()` over `--avatar-*` tokens.                                 |
| 11(d) barrel completeness | A component file missing from its directory's barrel, a directory barrel missing from `src/index.ts`, or a new directory classified as neither a component nor a non-component directory. `internal/` is the opt-out.                                                                |
| 11(e) `focus-ring`        | The utility written bare. It sets `outline` unconditionally, so bare it draws all the time — on the deals board that was every card, permanently ringed. Write `focus-visible:focus-ring`.                                                                                           |
| 11(g) no viewport prefix  | `sm:` / `md:` / `lg:` / `xl:` / `2xl:` in `apps/shell/src/modules`. A module is sized by its container: use `useBand` when the answer changes what renders, a container variant when it only changes how it looks. Shell chrome is exempt, because the shell really is window-sized. |
| 11(h) no `text-[Npx]`     | A pixel size outside the type scale, anywhere in the shell, the gallery or `packages/ui`.                                                                                                                                                                                            |
| 11(i) glued classes       | A class written flush against a template interpolation. Tailwind reads source as text, so `shadow-island${` matches no utility, the rule is never generated, and nothing reports it — a missing class is a missing rule, not an error.                                               |
| 11(j) zero-sized `<svg>`  | `h-0`, `w-0` or `size-0` on an `<svg>`. The specification disables rendering of an element whose width or height is zero, and `overflow: visible` never gets a say; it cost this repository every line on the canvas.                                                                |

Both 11(g) and 11(h) are adjudicated by the TypeScript parser rather than a line regex, because the
spelling each bans is also the spelling its own comment has to use — a comment is trivia and never a
node, which is the property the rule rests on.
