sigil/UI

Theming

Change once, update everywhere. Sigil's three-layer token system makes theming deterministic.

How tokens work

Every visual property in Sigil flows through CSS custom properties prefixed with --s-. Components never hardcode colors, spacing, or radii — they read from variables.

This means a single edit to your token file cascades through every component that references it. No grep-and-replace. No stale overrides.

┌────────────────────────────────────────────────────┐
│  tokens.ts (or preset)                             │
│  ↓ compile                                         │
│  :root { --s-primary: oklch(0.65 0.15 280); ... }  │
│  ↓ consumed by                                     │
│  <Button /> <Card /> <Input /> ...                  │
└────────────────────────────────────────────────────┘

The three layers

LayerPurposeExample
PrimitiveRaw valuesoklch(0.65 0.15 280), 8px, 150ms
SemanticMeaning-driven aliases--s-primary, --s-radius-md, --s-duration-fast
ComponentScoped to a specific component--s-card-radius, --s-grid-cell

Higher layers reference lower ones. A component token like --s-card-radius defaults to a semantic token like --s-radius-lg, which resolves to the primitive 12px.

Creating a preset

A preset is a named token object with optional metadata. Define one in TypeScript:

presets/brand.ts
import type { SigilPreset } from "@sigil-ui/tokens";

export const brandPreset: SigilPreset = {
  name: "brand",
  metadata: {
    description: "Corporate brand theme",
    author: "Design team",
    version: "1.0.0",
  },
  tokens: {
    colors: {
      background: { light: "oklch(0.99 0 0)", dark: "oklch(0.08 0.01 260)" },
      surface: { light: "oklch(0.97 0 0)", dark: "oklch(0.13 0.01 260)" },
      "surface-elevated": { light: "oklch(0.98 0 0)", dark: "oklch(0.16 0.01 260)" },
      primary: "oklch(0.55 0.20 145)",
      "primary-hover": "oklch(0.50 0.23 145)",
      "primary-muted": "oklch(0.88 0.05 145)",
      secondary: "oklch(0.65 0.12 30)",
      text: { light: "oklch(0.15 0 0)", dark: "oklch(0.93 0 0)" },
      "text-secondary": { light: "oklch(0.40 0 0)", dark: "oklch(0.70 0 0)" },
      "text-muted": { light: "oklch(0.55 0 0)", dark: "oklch(0.55 0 0)" },
      "text-subtle": { light: "oklch(0.70 0 0)", dark: "oklch(0.40 0 0)" },
      "text-disabled": { light: "oklch(0.80 0 0)", dark: "oklch(0.30 0 0)" },
      border: { light: "oklch(0.90 0 0)", dark: "oklch(0.22 0 0)" },
      "border-muted": { light: "oklch(0.94 0 0)", dark: "oklch(0.18 0 0)" },
      "border-strong": { light: "oklch(0.80 0 0)", dark: "oklch(0.35 0 0)" },
      "border-interactive": { light: "oklch(0.55 0.20 145)", dark: "oklch(0.55 0.20 145)" },
      success: "oklch(0.65 0.17 160)",
      warning: "oklch(0.75 0.15 85)",
      error: "oklch(0.60 0.20 25)",
      info: "oklch(0.60 0.15 250)",
    },
    typography: {
      "font-display": '"Inter", system-ui, sans-serif',
      "font-body": "system-ui, -apple-system, sans-serif",
      "font-mono": '"JetBrains Mono", ui-monospace, monospace',
    },
    spacing: { scale: [4, 8, 12, 16, 24, 32, 48, 64, 80, 96], unit: "px" },
    sigil: {
      "grid-cell": "48px",
      "cross-arm": "10px",
      "cross-stroke": "1.5px",
      "rail-gap": "24px",
      "content-max": "1280px",
      "card-radius": "12px",
    },
    radius: {
      none: "0px",
      sm: "4px",
      md: "8px",
      lg: "12px",
      xl: "16px",
      "2xl": "24px",
      full: "9999px",
    },
    shadows: {
      sm: "0 1px 2px 0 rgb(0 0 0 / 0.05)",
      md: "0 0 0 1px rgb(0 0 0 / 0.06), 0 1px 2px -1px rgb(0 0 0 / 0.06), 0 2px 4px 0 rgb(0 0 0 / 0.04)",
      lg: "0 0 0 1px rgb(0 0 0 / 0.04), 0 2px 4px -2px rgb(0 0 0 / 0.06), 0 4px 8px -2px rgb(0 0 0 / 0.08)",
      xl: "0 0 0 1px rgb(0 0 0 / 0.03), 0 4px 6px -4px rgb(0 0 0 / 0.06), 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 20px 25px -5px rgb(0 0 0 / 0.06)",
    },
    motion: {
      duration: { instant: "0ms", fast: "150ms", normal: "250ms", slow: "400ms", slower: "600ms" },
      easing: {
        default: "cubic-bezier(0.16, 1, 0.3, 1)",
        in: "cubic-bezier(0.55, 0, 1, 0.45)",
        out: "cubic-bezier(0, 0.55, 0.45, 1)",
        "in-out": "cubic-bezier(0.45, 0, 0.55, 1)",
        spring: "cubic-bezier(0.34, 1.56, 0.64, 1)",
      },
    },
    borders: {
      width: { none: "0px", thin: "1px", medium: "1.5px", thick: "2px" },
    },
  },
};

Applying a preset

Use the CLI to write the selected preset imports and then validate the project:

npx @sigil-ui/cli preset brand
npx @sigil-ui/cli doctor

This updates your token CSS so the compiled --s-* variables resolve to your preset values, including light/dark mode support.

Applying a preset at runtime

Load the compiled CSS dynamically or swap the <link> tag:

<link rel="stylesheet" href="/brand.css" />

Or override variables in a scoped container:

<div style={{ "--s-primary": "oklch(0.55 0.20 145)" } as React.CSSProperties}>
  <Button variant="primary">Themed button</Button>
</div>

Dark mode

Themed color tokens include light and dark variants. The compiled CSS uses the .dark and [data-theme="dark"] selectors automatically.

Sigil works with any dark mode strategy — class-based, attribute-based, or prefers-color-scheme. The fumadocs RootProvider handles the toggle for this docs site.

Agent workflow

The key insight: your agent edits one file. When a designer says "make the primary color green," the agent updates a single token value. Every button, link, card border, and focus ring updates in lockstep.

No searching through component files. No stale overrides. No Tailwind config wrestling.

  // One line changes everything
  colors: {
-   primary: "oklch(0.65 0.15 280)",
+   primary: "oklch(0.55 0.20 145)",
  },

Tokens reference

Colors

TokenLightDark
--s-backgroundoklch(0.99 0 0)oklch(0.07 0.01 280)
--s-surfaceoklch(0.97 0 0)oklch(0.12 0.01 280)
--s-surface-elevatedoklch(0.98 0 0)oklch(0.15 0.01 280)
--s-primaryoklch(0.65 0.15 280)
--s-primary-hoveroklch(0.60 0.18 280)
--s-primary-mutedoklch(0.90 0.04 280)
--s-secondaryoklch(0.70 0.12 60)
--s-textoklch(0.15 0 0)oklch(0.93 0 0)
--s-text-secondaryoklch(0.40 0 0)oklch(0.70 0 0)
--s-borderoklch(0.90 0 0)oklch(0.22 0 0)

Spacing

TokenValue
--s-space-04px
--s-space-18px
--s-space-212px
--s-space-316px
--s-space-424px
--s-space-532px
--s-space-648px
--s-space-764px
--s-space-880px
--s-space-996px

Radius

TokenValue
--s-radius-none0px
--s-radius-sm4px
--s-radius-md8px
--s-radius-lg12px
--s-radius-xl16px
--s-radius-2xl24px
--s-radius-full9999px

Motion

Duration tokenValue
--s-duration-instant0ms
--s-duration-fast150ms
--s-duration-normal250ms
--s-duration-slow400ms
--s-duration-slower600ms
Easing tokenValue
--s-ease-defaultcubic-bezier(0.16, 1, 0.3, 1)
--s-ease-incubic-bezier(0.55, 0, 1, 0.45)
--s-ease-outcubic-bezier(0, 0.55, 0.45, 1)
--s-ease-in-outcubic-bezier(0.45, 0, 0.55, 1)
--s-ease-springcubic-bezier(0.34, 1.56, 0.64, 1)
Studiodefault
Presets
primary
secondary
background
surface
text
border
accent
success
warning
error
info
display
body
mono
heading wt
600
heading trk
-0.025em
base size
16px
page margin
24px
section pad
64px
card pad
24px
grid gap
24px
stack gap
12px
global
8px
button
8px
card
12px
input
6px
border w
1px
style
card border
card shadow
btn shadow
glow
spring
Type
Duration
0.20
Bounce
1.00
easing
cubic-bezier(0.16, 1, 0.3, 1)
fast
150ms
normal
200ms
slow
300ms
hover scale
1.02
press scale
0.98
hover lift
-1px
stagger
50ms
weight
transform
hover
active scale
0.98
min-width
0px
letter sp
0.000em
icon gap
8px
shadow
hover
border
shadow
padding
24px
title size
1px
title wt
desc size
0.875px
aspect
outline
height
36px
focus ring
2px
focus ring
h1 size
2.25px
h2 size
1.875px
h3 size
1.5px
h4 size
1.25px
weight
tracking
-0.020em
leading
1.20
pattern
pattern α
0.03
noise
gradient
grad angle
180°
height
50px
blur
12px
border
padding
24px
item gap
24px
min-height
600px
padding Y
80px
content-max
680px
layout
title size
56px
desc size
18px
padding Y
64px
max-width
600px
layout
title size
36px
padding Y
48px
columns
4
gap
36px
content-max
1200px
rail-gap
24px
grid-cell
50px
cross-stroke
1.5px
navbar-h
50px
bento-gap
16px
grid lines
dots
cell borders
cell bg
Gutter
Margin
content
hero
navbar
rail visible
enabled