Skip to content

Theming & Customization

New to Sigil?

Start with the Usage Guide to learn the basics of importing and using components before diving into theming.

Sigil is designed to be highly customizable through CSS Custom Properties (CSS variables). All design tokens are defined in a central theme file that you can reference and override.

Design Tokens

Sigil provides a comprehensive set of design tokens organized into the following categories:

CategoryPrefixDescription
Spacing Scale--size-{n}4px-increment spacing (0 → 96)
Container Sizes--size-{2xs–7xl}Named width breakpoints (256px → 1280px)
Special Sizes--size-{full,fit,min,max,auto,none,prose}Keyword size utilities
Viewport & Breakpoints--size-screen-*Viewport units + breakpoint values
Aspect Ratios--aspect-*Common aspect ratios (square, video, wide…)
3D Perspective--perspective-*Transform perspective distances
Grid Templates--grid-{1–12}CSS Grid column repeat helpers
Border Widths--border-*Stroke widths + ring utilities
Border Radius--rounded-*Corner rounding scale
Blur Effects--blur-*Blur filter scale
Box Shadows--shadow-*Elevation shadows
Inset Shadows--inset-shadow-*Inner shadow scale
Drop Shadows--drop-shadow-*CSS filter drop-shadows
Text Shadows--text-shadow-*Typographic text shadows
Halo Shadows--halo-shadow-*Branded glow shadows per semantic color
Font Families--font-{sans,serif,mono}System font stacks
Font Weights--font-*Numeric weight scale (100–900)
Letter Spacing--tracking-*Tracking utilities
Line Heights--leading-*Relative (named) + absolute (numeric) line height scale
Body Text Scale--text-{2xs–3xl}8-step body font size scale (2xs through 3xl)
Heading Scale--heading-{xs–2xl}6-step heading font size scale
Semantic Text Colors--text-color-*Role-based text color tokens
Transitions--transition-*Pre-built timing + easing combos
Durations--duration-{75,100,150,200,300,500,700,1000}Millisecond step scale (all zero under prefers-reduced-motion)
Easing Functions--ease-*Named cubic-bezier curves
Contrast Scale--color-contrast-{50–900}11-step light/dark adaptive palette (includes 150)
Canvas / Contrast--color-canvas, --color-contrastAliases for contrast-50 and contrast-900
Semantic Colors--color-{name}-*7 sub-tokens per semantic color
Section Spacing--section-spacingDefault block section gap (2rem)
Neutral Overrides--sigil-color-neutral-*Global neutral palette override surface (6 tokens)

Color Palette

Brand Colors

PrimaryPeriwinkle violet — primary brand color
Base
Backdrop
Content
Contrast
Focus
Border
SecondaryInk/charcoal (light) · silver (dark) — contrast-driven adaptive
Base
Backdrop
Content
Contrast
Focus
Border
NeutralTrue gray — neutral UI surfaces
Base
Backdrop
Content
Contrast
Focus
Border

Semantic Colors

InfoCyan-blue — informational messages
Base
Backdrop
Content
Contrast
Focus
Border
SuccessTeal — positive outcomes & confirmations
Base
Backdrop
Content
Contrast
Focus
Border
WarningAmber — cautionary states & alerts
Base
Backdrop
Content
Contrast
Focus
Border
ErrorVermilion — destructive actions & errors
Base
Backdrop
Content
Contrast
Focus
Border

Contrast Scale

Adaptive gray scale — surfaces (50–400) and text (500–900). Automatically inverts in dark mode.

50
100
200
300
400
500
600
700
800
900

Theme Reference

View theme.css
css
@layer sigil.tokens {
  :root {
    /* ========================================
       Spacing Scale
       Based on 0.25rem (4px) increments
       ======================================== */
    --size-0: 0;
    --size-px: 1px;
    --size-0-5: 0.125rem; /* 2px */
    --size-1: 0.25rem; /* 4px */
    --size-1-5: 0.375rem; /* 6px */
    --size-2: 0.5rem; /* 8px */
    --size-2-5: 0.625rem; /* 10px */
    --size-3: 0.75rem; /* 12px */
    --size-3-5: 0.875rem; /* 14px */
    --size-4: 1rem; /* 16px */
    --size-5: 1.25rem; /* 20px */
    --size-6: 1.5rem; /* 24px */
    --size-7: 1.75rem; /* 28px */
    --size-8: 2rem; /* 32px */
    --size-9: 2.25rem; /* 36px */
    --size-10: 2.5rem; /* 40px */
    --size-11: 2.75rem; /* 44px */
    --size-12: 3rem; /* 48px */
    --size-14: 3.5rem; /* 56px */
    --size-16: 4rem; /* 64px */
    --size-20: 5rem; /* 80px */
    --size-24: 6rem; /* 96px */
    --size-28: 7rem; /* 112px */
    --size-32: 8rem; /* 128px */
    --size-36: 9rem; /* 144px */
    --size-40: 10rem; /* 160px */
    --size-44: 11rem; /* 176px */
    --size-48: 12rem; /* 192px */
    --size-52: 13rem; /* 208px */
    --size-56: 14rem; /* 224px */
    --size-60: 15rem; /* 240px */
    --size-64: 16rem; /* 256px */
    --size-72: 18rem; /* 288px */
    --size-80: 20rem; /* 320px */
    --size-96: 24rem; /* 384px */

    /* Section Spacing */
    --section-spacing: 2rem; /* 32px */

    /* Container Sizes */
    --size-2xs: 16rem; /* 256px */
    --size-xs: 20rem; /* 320px */
    --size-sm: 24rem; /* 384px */
    --size-md: 28rem; /* 448px */
    --size-lg: 32rem; /* 512px */
    --size-xl: 36rem; /* 576px */
    --size-2xl: 42rem; /* 672px */
    --size-3xl: 48rem; /* 768px */
    --size-4xl: 56rem; /* 896px */
    --size-5xl: 64rem; /* 1024px */
    --size-6xl: 72rem; /* 1152px */
    --size-7xl: 80rem; /* 1280px */

    /* Special Sizes */
    --size-full: 100%;
    --size-fit: fit-content;
    --size-min: min-content;
    --size-max: max-content;
    --size-auto: auto;
    --size-none: none;
    --size-prose: 65ch; /* Optimal reading width */

    /* Viewport Units */
    --size-screen-width: 100dvw;
    --size-screen-height: 100dvh;

    /* Breakpoints */
    --size-screen-xs: 480px;
    --size-screen-sm: 640px;
    --size-screen-md: 768px;
    --size-screen-lg: 1024px;
    --size-screen-xl: 1280px;
    --size-screen-2xl: 1536px;

    /* ========================================
       3D Perspective (for transforms)
       ======================================== */
    --perspective-dramatic: 100px;
    --perspective-near: 300px;
    --perspective-normal: 500px;
    --perspective-midrange: 800px;
    --perspective-distant: 1200px;

    /* ========================================
       Aspect Ratios
       ======================================== */
    --aspect-square: 1 / 1;
    --aspect-video: 16 / 9;
    --aspect-wide: 21 / 9;
    --aspect-ultrawide: 32 / 9;
    --aspect-portrait: 3 / 4;
    --aspect-photo: 4 / 3;

    /* ========================================
       Grid Template Columns
       ======================================== */
    --grid-1: repeat(1, minmax(0, 1fr));
    --grid-2: repeat(2, minmax(0, 1fr));
    --grid-3: repeat(3, minmax(0, 1fr));
    --grid-4: repeat(4, minmax(0, 1fr));
    --grid-5: repeat(5, minmax(0, 1fr));
    --grid-6: repeat(6, minmax(0, 1fr));
    --grid-7: repeat(7, minmax(0, 1fr));
    --grid-8: repeat(8, minmax(0, 1fr));
    --grid-9: repeat(9, minmax(0, 1fr));
    --grid-10: repeat(10, minmax(0, 1fr));
    --grid-11: repeat(11, minmax(0, 1fr));
    --grid-12: repeat(12, minmax(0, 1fr));

    /* ========================================
       Border Widths
       ======================================== */
    --border-0: 0;
    --border: 1px;
    --border-2: 2px;
    --border-4: 4px;
    --border-8: 8px;

    /* Ring Utilities (for focus states) */
    --ring-0: 0 0 0 0;
    --ring: 0 0 0 var(--border);
    --ring-2: 0 0 0 var(--border-2);
    --ring-4: 0 0 0 var(--border-4);
    --ring-8: 0 0 0 var(--border-8);

    /* ========================================
       Border Radius
       Optimized for accessibility - minimum 4px for touch targets
       ======================================== */
    --rounded-none: 0;
    --rounded-xs: 0.125rem; /* 2px - minimal */
    --rounded-sm: 0.25rem; /* 4px - small */
    --rounded: 0.25rem; /* 4px - default */
    --rounded-md: 0.375rem; /* 6px - medium */
    --rounded-lg: 0.5rem; /* 8px - large */
    --rounded-xl: 0.75rem; /* 12px - extra large */
    --rounded-2xl: 1rem; /* 16px - 2x extra large */
    --rounded-3xl: 1.5rem; /* 24px - 3x extra large */
    --rounded-full: 9999px; /* Full circle/pill */

    /* ========================================
       Blur Effects
       ======================================== */
    --blur-none: 0;
    --blur-xs: 4px;
    --blur-sm: 8px;
    --blur-md: 12px;
    --blur-lg: 16px;
    --blur-xl: 24px;
    --blur-2xl: 40px;
    --blur-3xl: 64px;

    /* ========================================
       Box Shadows
       All shadows use light-dark() to remain visible in dark mode.
       ======================================== */
    --shadow-none: 0 0 transparent;
    --shadow-2xs: light-dark(0 1px 2px rgb(0 0 0 / 3%), 0 1px 2px rgb(0 0 0 / 20%));
    --shadow-xs: light-dark(0 2px 4px rgb(0 0 0 / 3%), 0 2px 4px rgb(0 0 0 / 22%));
    --shadow-sm: light-dark(0 4px 10px -1px rgb(0 0 0 / 5%), 0 4px 10px -1px rgb(0 0 0 / 28%));
    --shadow: light-dark(0 8px 16px -2px rgb(0 0 0 / 6%), 0 8px 16px -2px rgb(0 0 0 / 32%));
    --shadow-md: light-dark(0 12px 24px -4px rgb(0 0 0 / 6%), 0 12px 24px -4px rgb(0 0 0 / 34%));
    --shadow-lg: light-dark(0 24px 32px -8px rgb(0 0 0 / 8%), 0 24px 32px -8px rgb(0 0 0 / 38%));
    --shadow-xl: light-dark(0 32px 64px -16px rgb(0 0 0 / 12%), 0 32px 64px -16px rgb(0 0 0 / 44%));
    --shadow-2xl: light-dark(0 48px 80px -20px rgb(0 0 0 / 20%), 0 48px 80px -20px rgb(0 0 0 / 52%));

    /* Inset Shadows — also light-dark() for dark mode depth */
    --inset-shadow-none: inset 0 0 transparent;
    --inset-shadow-2xs: light-dark(inset 0 1px rgb(0 0 0 / 5%), inset 0 1px rgb(255 255 255 / 4%));
    --inset-shadow-xs: light-dark(inset 0 1px 1px rgb(0 0 0 / 5%), inset 0 1px 1px rgb(255 255 255 / 4%));
    --inset-shadow-sm: light-dark(inset 0 2px 4px rgb(0 0 0 / 5%), inset 0 2px 4px rgb(0 0 0 / 25%));
    --inset-shadow: light-dark(inset 0 2px 4px rgb(0 0 0 / 6%), inset 0 2px 4px rgb(0 0 0 / 28%));

    /* Drop Shadows (for filters) */
    --drop-shadow-none: 0 0 transparent;
    --drop-shadow-xs: 0 1px 1px rgb(0 0 0 / 5%);
    --drop-shadow-sm: 0 1px 2px rgb(0 0 0 / 15%);
    --drop-shadow: 0 1px 2px rgb(0 0 0 / 10%), 0 1px 1px rgb(0 0 0 / 6%);
    --drop-shadow-md: 0 3px 3px rgb(0 0 0 / 12%);
    --drop-shadow-lg: 0 4px 4px rgb(0 0 0 / 15%);
    --drop-shadow-xl: 0 9px 7px rgb(0 0 0 / 10%);
    --drop-shadow-2xl: 0 25px 25px rgb(0 0 0 / 15%);

    /* Text Shadows (for readability) */
    --text-shadow-none: 0 0 transparent;
    --text-shadow-2xs: 0 1px 0 rgb(0 0 0 / 15%);
    --text-shadow-xs: 0 1px 1px rgb(0 0 0 / 20%);
    --text-shadow-sm: 0 1px 0 rgb(0 0 0 / 7.5%), 0 1px 1px rgb(0 0 0 / 7.5%), 0 2px 2px rgb(0 0 0 / 7.5%);
    --text-shadow: 0 1px 1px rgb(0 0 0 / 10%), 0 2px 2px rgb(0 0 0 / 10%);
    --text-shadow-md: 0 1px 1px rgb(0 0 0 / 10%), 0 1px 2px rgb(0 0 0 / 10%), 0 2px 4px rgb(0 0 0 / 10%);
    --text-shadow-lg: 0 1px 2px rgb(0 0 0 / 10%), 0 3px 2px rgb(0 0 0 / 10%), 0 4px 8px rgb(0 0 0 / 10%);

    /* ========================================
       Typography
       Optimized for WCAG AAA readability
       ======================================== */

    /* Font Family */
    --font-sans:
      'Inter Variable', 'Plus Jakarta Sans Variable', system-ui, -apple-system, blinkmacsystemfont, 'Segoe UI', roboto,
      sans-serif;
    --font-serif: ui-serif, georgia, cambria, 'Times New Roman', times, serif;
    --font-mono: ui-monospace, 'Cascadia Code', 'Source Code Pro', menlo, consolas, 'Liberation Mono', monospace;

    /* Font Weights (Accessibility: minimum 400 for body text) */
    --font-thin: 100;
    --font-extralight: 200;
    --font-light: 300;
    --font-normal: 400; /* Minimum for body text */
    --font-medium: 500;
    --font-semibold: 600;
    --font-bold: 700;
    --font-extrabold: 800;
    --font-black: 900;

    /* Letter Spacing */
    --tracking-tight: -0.025em; /* -2.5% — headers, display text */
    --tracking-header: -0.025em; /* alias for --tracking-tight */
    --tracking-normal: 0em; /* default body tracking */
    --tracking-wide: 0.05em; /* labels, badges, uppercase caps */

    /* Line Heights (Optimized for readability - WCAG recommends 1.5 for body text) */
    --leading-none: 1;
    --leading-tight: 1.15; /* 115% for headers */
    --leading-snug: 1.375;
    --leading-normal: 1.5; /* Optimal for body text (WCAG) */
    --leading-relaxed: 1.625;
    --leading-loose: 2;

    /* Absolute Line Heights */
    --leading-3: 0.75rem; /* 12px */
    --leading-4: 1rem; /* 16px */
    --leading-5: 1.25rem; /* 20px */
    --leading-6: 1.5rem; /* 24px */
    --leading-7: 1.75rem; /* 28px */
    --leading-8: 2rem; /* 32px */
    --leading-9: 2.25rem; /* 36px */
    --leading-10: 2.5rem; /* 40px */

    /* Component UI Font Sizes (7 steps — used inside components) */
    --text-2xs: 0.625rem; /* 10px - micro labels, dot-only badges */
    --text-xs: 0.75rem; /* 12px - small labels only */
    --text-sm: 0.875rem; /* 14px - secondary text */
    --text-base: 1rem; /* 16px - body text (minimum) */
    --text-lg: 1.125rem; /* 18px - large UI / headings */
    --text-xl: 1.25rem; /* 20px - subheadings */
    --text-2xl: 1.5rem; /* 24px - major headings */
    --text-3xl: 1.875rem; /* 30px - display / circular progress label */

    /* Document Heading Sizes (h1–h6 — use outside components, in page/doc context) */
    --heading-xs: 0.875rem;
    --heading-sm: 1rem;
    --heading-md: 1.5rem;
    --heading-lg: 2rem;
    --heading-xl: 3rem;
    --heading-2xl: 4rem;

    /* Semantic Text Colors (for accessibility)
       These adapt to light/dark mode automatically */
    --text-color-heading: var(--color-contrast-900);
    --text-color-body: var(--color-contrast-800);
    --text-color-secondary: var(--color-contrast-600);
    --text-color-tertiary: var(--color-contrast-500);
    --text-color-disabled: var(--color-contrast-400);
    --text-color-contrast: var(--color-contrast-100);

    /* ========================================
       Transitions & Animations
       Optimized for smooth, accessible motion
       ======================================== */

    /* Easing Functions */
    --ease-linear: linear;
    --ease-in: cubic-bezier(0.4, 0, 1, 1);
    --ease-out: cubic-bezier(0, 0, 0.2, 1);
    --ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
    --ease-spring: cubic-bezier(0.22, 1, 0.36, 1); /* Fast start, soft landing — no overshoot */
    --ease-move: cubic-bezier(0.25, 1, 0.5, 1); /* Spatial slides — drawers, panels, carousels */
    --ease-overshoot: cubic-bezier(0.34, 1.1, 0.64, 1); /* Subtle spring overshoot — toasts, popups */

    /* Duration (respects prefers-reduced-motion) */
    --duration-75: 75ms;
    --duration-100: 100ms;
    --duration-150: 150ms;
    --duration-200: 200ms;
    --duration-300: 300ms;
    --duration-500: 500ms;
    --duration-600: 600ms;
    --duration-700: 700ms;
    --duration-1000: 1000ms;
    --duration-1400: 1400ms; /* Looping indicator cycle */
    --duration-1500: 1500ms; /* Looping indicator cycle */

    /* Combined Transitions (most commonly used) */
    --transition-none: none;
    --transition-fast: 150ms var(--ease-in-out);
    --transition-normal: 200ms var(--ease-in-out);
    --transition-slow: 300ms var(--ease-in-out);
    --transition-slower: 500ms var(--ease-in-out);
    --transition-spring: 300ms var(--ease-spring);
    --transition-exit: 300ms var(--ease-out); /* Exit animations — overlay dismiss, alert collapse */
    --transition-loader: 600ms linear; /* Spinner — constant rotation */

    /* Overlay entrance defaults — shared by @starting-style blocks */
    --overlay-enter-translate-y: -4px; /* dropdown slides down from */
    --overlay-enter-translate-y-up: 4px; /* bottom-anchored overlays slide up from */
    --overlay-enter-scale: 0.97; /* slight scale-in for menus / panels */
    --overlay-enter-scale-dialog: 0.96; /* slightly more pronounced for dialogs */

    /* ========================================
       CSS Color Scheme
       ======================================== */
    color-scheme: light dark;

    /* Contrast scale — Neutral OKLCH with a lifted, non-pure-black Dark Mode baseline */
    --color-contrast-50: light-dark(oklch(99% 0.001 264deg), oklch(17% 0.001 250deg)); /* Canvas, page background */
    --color-contrast-100: light-dark(oklch(97% 0.001 264deg), oklch(21% 0.001 250deg)); /* Cards, elevated surfaces */
    --color-contrast-150: light-dark(
      oklch(95.5% 0.001 264deg),
      oklch(23.5% 0.001 250deg)
    ); /* Midpoint — chip base, subtle fills */

    --color-contrast-200: light-dark(oklch(94% 0.001 264deg), oklch(26% 0.001 250deg)); /* Nested cards, hover states */
    --color-contrast-300: light-dark(oklch(89% 0.002 264deg), oklch(32% 0.001 250deg)); /* Borders, dividers */
    --color-contrast-400: light-dark(
      oklch(81% 0.002 264deg),
      oklch(40% 0.001 250deg)
    ); /* Disabled backgrounds, subtle UI */

    --color-contrast-500: light-dark(
      oklch(49% 0.002 264deg),
      oklch(58% 0.001 250deg)
    ); /* Tertiary text - AA compliant */

    --color-contrast-600: light-dark(
      oklch(40% 0.002 264deg),
      oklch(68% 0.001 250deg)
    ); /* Secondary text - AA compliant */

    --color-contrast-700: light-dark(oklch(32% 0.002 264deg), oklch(78% 0.001 250deg)); /* Body text - AAA compliant */
    --color-contrast-800: light-dark(oklch(22% 0.002 264deg), oklch(88% 0.001 250deg)); /* Headings - AAA compliant */
    --color-contrast-900: light-dark(
      oklch(12% 0.002 264deg),
      oklch(95% 0.001 250deg)
    ); /* High contrast text - AAA compliant */

    --color-canvas: color-mix(in oklch, var(--color-contrast-50) 85%, transparent);
    --color-divider: color-mix(in oklch, var(--color-contrast-300) 85%, transparent);
    --color-contrast: color-mix(in oklch, var(--color-contrast-900) 85%, transparent);

    /* Halo Shadows */
    --halo-shadow-neutral:
      inset 0 1px 1px light-dark(rgb(0 0 0 / 5%), rgb(0 0 0 / 15%)),
      0 0 var(--size-1) light-dark(rgb(255 255 255 / 10%), rgb(0 0 0 / 20%)) inset,
      0 var(--size-2) var(--size-6) light-dark(oklch(58% 0 264deg / 15%), oklch(72% 0 250deg / 15%)),
      0 var(--size-1) var(--size-3) light-dark(oklch(58% 0 264deg / 8%), oklch(72% 0 250deg / 8%));
    --halo-shadow-primary:
      inset 0 1px 1px light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 3%)),
      0 0 var(--size-1) light-dark(rgb(255 255 255 / 10%), rgb(255 255 255 / 12%)) inset,
      0 var(--size-2) var(--size-6) light-dark(oklch(56% 0.22 293deg / 15%), oklch(72% 0.2 293deg / 15%)),
      0 var(--size-1) var(--size-3) light-dark(oklch(56% 0.22 293deg / 8%), oklch(72% 0.2 293deg / 8%));
    --halo-shadow-secondary:
      inset 0 1px 1px light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 3%)),
      0 0 var(--size-1) light-dark(rgb(255 255 255 / 10%), rgb(255 255 255 / 12%)) inset,
      0 var(--size-2) var(--size-6) light-dark(oklch(22% 0.005 264deg / 12%), oklch(82% 0.004 250deg / 12%)),
      0 var(--size-1) var(--size-3) light-dark(oklch(22% 0.005 264deg / 7%), oklch(82% 0.004 250deg / 7%));
    --halo-shadow-info:
      inset 0 1px 1px light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 3%)),
      0 0 var(--size-1) light-dark(rgb(255 255 255 / 10%), rgb(255 255 255 / 12%)) inset,
      0 var(--size-2) var(--size-6) light-dark(oklch(60% 0.18 230deg / 15%), oklch(74% 0.16 230deg / 15%)),
      0 var(--size-1) var(--size-3) light-dark(oklch(60% 0.18 230deg / 8%), oklch(74% 0.16 230deg / 8%));
    --halo-shadow-success:
      inset 0 1px 1px light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 3%)),
      0 0 var(--size-1) light-dark(rgb(255 255 255 / 10%), rgb(255 255 255 / 12%)) inset,
      0 var(--size-2) var(--size-6) light-dark(oklch(62% 0.15 160deg / 15%), oklch(78% 0.14 160deg / 15%)),
      0 var(--size-1) var(--size-3) light-dark(oklch(62% 0.15 160deg / 8%), oklch(78% 0.14 160deg / 8%));
    --halo-shadow-warning:
      inset 0 1px 1px light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 3%)),
      0 0 var(--size-1) light-dark(rgb(255 255 255 / 10%), rgb(255 255 255 / 12%)) inset,
      0 var(--size-2) var(--size-6) light-dark(oklch(74% 0.18 70deg / 15%), oklch(84% 0.16 70deg / 15%)),
      0 var(--size-1) var(--size-3) light-dark(oklch(74% 0.18 70deg / 8%), oklch(84% 0.16 70deg / 8%));
    --halo-shadow-error:
      inset 0 1px 1px light-dark(rgb(0 0 0 / 5%), rgb(255 255 255 / 3%)),
      0 0 var(--size-1) light-dark(rgb(255 255 255 / 10%), rgb(255 255 255 / 12%)) inset,
      0 var(--size-2) var(--size-6) light-dark(oklch(54% 0.2 29deg / 15%), oklch(66% 0.18 29deg / 15%)),
      0 var(--size-1) var(--size-3) light-dark(oklch(54% 0.2 29deg / 8%), oklch(66% 0.18 29deg / 8%));

    /* ── Neutral ─────────────────────────────────────────────────────────────── */
    --color-neutral: light-dark(oklch(56% 0 264deg), oklch(72% 0 250deg));
    --color-neutral-backdrop: light-dark(oklch(97% 0 264deg / 83%), oklch(27% 0 250deg / 60%));
    --color-neutral-content: light-dark(oklch(98% 0 264deg), oklch(15% 0 250deg));
    --color-neutral-contrast: light-dark(oklch(99% 0 264deg), oklch(13% 0 250deg));
    --color-neutral-focus: light-dark(oklch(62% 0 264deg), oklch(78% 0 250deg));
    --color-neutral-border: light-dark(oklch(86% 0 264deg / 75%), oklch(72% 0 250deg / 40%));
    --color-neutral-focus-shadow:
      0 0 0 4px color-mix(in oklch, var(--color-neutral) 40%, transparent), var(--shadow-sm);

    /* ── Primary (violet) ────────────────────────────────────────────────────── */
    --color-primary: light-dark(
      oklch(56% 0.22 293deg),
      oklch(72% 0.2 293deg)
    ); /* Bright enough to read on dark canvas */

    --color-primary-backdrop: light-dark(oklch(93% 0.04 293deg), oklch(24% 0.1 293deg / 40%));
    --color-primary-content: light-dark(
      oklch(98% 0.01 293deg),
      oklch(15% 0.08 293deg)
    ); /* Dark ink on bright dark-mode button */

    --color-primary-contrast: light-dark(oklch(98% 0.01 293deg), oklch(14% 0.06 293deg));
    --color-primary-focus: light-dark(oklch(62% 0.22 293deg), oklch(78% 0.2 293deg));
    --color-primary-border: light-dark(oklch(56% 0.22 293deg / 60%), oklch(72% 0.2 293deg / 60%));
    --color-primary-focus-shadow:
      0 0 0 4px color-mix(in oklch, var(--color-primary) 40%, transparent), var(--shadow-sm);

    /* ── Secondary (near-black / near-white) ─────────────────────────────────── */
    --color-secondary: light-dark(oklch(22% 0.005 264deg), oklch(82% 0.004 250deg));
    --color-secondary-backdrop: light-dark(oklch(95% 0.003 264deg / 83%), oklch(20% 0.004 250deg / 60%));
    --color-secondary-content: light-dark(oklch(98% 0.002 264deg), oklch(15% 0.005 250deg));
    --color-secondary-contrast: light-dark(oklch(98% 0.002 264deg), oklch(13% 0.005 250deg));
    --color-secondary-focus: light-dark(oklch(30% 0.005 264deg), oklch(88% 0.003 250deg));
    --color-secondary-border: light-dark(oklch(22% 0.005 264deg / 60%), oklch(82% 0.004 250deg / 60%));
    --color-secondary-focus-shadow:
      0 0 0 4px color-mix(in oklch, var(--color-secondary) 40%, transparent), var(--shadow-sm);

    /* ── Info (cyan-blue) ────────────────────────────────────────────────────── */
    --color-info: light-dark(oklch(60% 0.18 230deg), oklch(74% 0.16 230deg));
    --color-info-backdrop: light-dark(oklch(92% 0.06 230deg / 83%), oklch(24% 0.08 230deg / 50%));
    --color-info-content: light-dark(
      oklch(15% 0.08 230deg),
      oklch(15% 0.08 230deg)
    ); /* Base is bright in both modes, content stays dark */

    --color-info-contrast: light-dark(oklch(97% 0.02 230deg), oklch(18% 0.06 230deg));
    --color-info-focus: light-dark(oklch(66% 0.18 230deg), oklch(80% 0.16 230deg));
    --color-info-border: light-dark(oklch(60% 0.18 230deg / 60%), oklch(74% 0.16 230deg / 60%));
    --color-info-focus-shadow: 0 0 0 4px color-mix(in oklch, var(--color-info) 40%, transparent), var(--shadow-sm);

    /* ── Success (teal-green) ────────────────────────────────────────────────── */
    --color-success: light-dark(
      oklch(62% 0.15 160deg),
      oklch(78% 0.14 160deg)
    ); /* Kept brighter than error for colorblindness */

    --color-success-backdrop: light-dark(oklch(93% 0.04 160deg / 83%), oklch(22% 0.07 160deg / 50%));
    --color-success-content: light-dark(oklch(15% 0.06 160deg), oklch(15% 0.06 160deg));
    --color-success-contrast: light-dark(oklch(97% 0.02 160deg), oklch(17% 0.06 160deg));
    --color-success-focus: light-dark(oklch(66% 0.15 160deg), oklch(84% 0.14 160deg));
    --color-success-border: light-dark(oklch(62% 0.15 160deg / 60%), oklch(78% 0.14 160deg / 60%));
    --color-success-focus-shadow:
      0 0 0 4px color-mix(in oklch, var(--color-success) 40%, transparent), var(--shadow-sm);

    /* ── Warning (amber) ─────────────────────────────────────────────────────── */
    --color-warning: light-dark(oklch(74% 0.18 70deg), oklch(84% 0.16 70deg));
    --color-warning-backdrop: light-dark(oklch(93% 0.07 70deg / 83%), oklch(26% 0.08 70deg / 50%));
    --color-warning-content: light-dark(oklch(15% 0.08 70deg), oklch(15% 0.08 70deg));
    --color-warning-contrast: light-dark(oklch(96% 0.03 70deg), oklch(22% 0.06 70deg));
    --color-warning-focus: light-dark(oklch(79% 0.18 70deg), oklch(90% 0.16 70deg));
    --color-warning-border: light-dark(oklch(74% 0.18 70deg / 60%), oklch(84% 0.16 70deg / 60%));
    --color-warning-focus-shadow:
      0 0 0 4px color-mix(in oklch, var(--color-warning) 40%, transparent), var(--shadow-sm);

    /* ── Error (red-orange) ──────────────────────────────────────────────────── */
    --color-error: light-dark(oklch(54% 0.2 29deg), oklch(66% 0.18 29deg)); /* Deeper than success for colorblindness */
    --color-error-backdrop: light-dark(oklch(92% 0.06 29deg / 83%), oklch(20% 0.08 29deg / 50%));
    --color-error-content: light-dark(oklch(98% 0.03 29deg), oklch(15% 0.06 29deg));
    --color-error-contrast: light-dark(oklch(97% 0.02 29deg), oklch(16% 0.06 29deg));
    --color-error-focus: light-dark(oklch(60% 0.2 29deg), oklch(72% 0.18 29deg));
    --color-error-border: light-dark(oklch(54% 0.2 29deg / 60%), oklch(66% 0.18 29deg / 60%));
    --color-error-focus-shadow: 0 0 0 4px color-mix(in oklch, var(--color-error) 40%, transparent), var(--shadow-sm);
  }

  /* ========================================
     Reduced Motion
     ======================================== */

  @media (prefers-reduced-motion: reduce) {
    :root {
      --duration-75: 0ms;
      --duration-100: 0ms;
      --duration-150: 0ms;
      --duration-200: 0ms;
      --duration-300: 0ms;
      --duration-500: 0ms;
      --duration-700: 0ms;
      --duration-1000: 0ms;
      --transition-fast: none;
      --transition-normal: none;
      --transition-slow: none;
      --transition-slower: none;
      --transition-spring: none;
      --transition-exit: none;
      --transition-loader: none;
    }
  }

  /* VitePress dark theme: force dark color-scheme so light-dark() resolves to dark values */
  html.dark {
    color-scheme: dark;
  }

  /* VitePress light theme: force light color-scheme so light-dark() resolves to light values */
  html:not(.dark) {
    color-scheme: light;
  }
}

Contrast Scale

Sigil uses an 11-step contrast scale driven entirely by light-dark(). The values flip automatically between the light and dark poles — no @media query needed.

Background Range (50–400)

Optimized for surfaces, borders, and UI structure:

TokenLightDarkUsage
--color-contrast-50oklch(99% 0.004 264deg)oklch(13% 0.005 250deg)Canvas, page background
--color-contrast-100oklch(97% 0.004 264deg)oklch(17% 0.005 250deg)Cards, elevated surfaces
--color-contrast-150oklch(95.5% 0.0045 264deg)oklch(19.5% 0.005 250deg)Midpoint — chip base, subtle fills
--color-contrast-200oklch(94% 0.005 264deg)oklch(22% 0.005 250deg)Nested cards, hover states
--color-contrast-300oklch(89% 0.006 264deg)oklch(29% 0.006 250deg)Borders, dividers
--color-contrast-400oklch(81% 0.007 264deg)oklch(38% 0.006 250deg)Disabled backgrounds, subtle UI

Text Range (500–900)

Optimized for readability and WCAG compliance. OKLCH provides perceptually-uniform lightness steps so contrast ratios are consistent across the ramp.

TokenLightDarkWCAGUsage
--color-contrast-500oklch(49% 0.008 264deg)oklch(58% 0.006 250deg)AA (large text)Tertiary text, placeholders
--color-contrast-600oklch(40% 0.008 264deg)oklch(68% 0.005 250deg)AASecondary / muted text
--color-contrast-700oklch(32% 0.008 264deg)oklch(78% 0.004 250deg)AAASupplemental body text
--color-contrast-800oklch(22% 0.008 264deg)oklch(88% 0.004 250deg)AAADefault body text
--color-contrast-900oklch(12% 0.008 264deg)oklch(95% 0.003 250deg)AAAHeadings, highest contrast

Accessibility

All text color values (500–900) meet or exceed WCAG AA standards. Values 700–900 achieve AAA compliance for body text and headings.

Canvas & Contrast Aliases

Two convenience aliases are provided for the most common surface and text needs:

TokenResolves toUsage
--color-canvas--color-contrast-50Default page/component background
--color-contrast--color-contrast-900Maximum-contrast text/icon color

Semantic Text Colors

Six role-based text color tokens are derived from the contrast scale. Use these instead of raw contrast values so your overrides stay meaningful in both light and dark mode.

TokenContrast stepIntended use
--text-color-heading--color-contrast-900Headings — highest contrast, AAA
--text-color-body--color-contrast-800Default body text, AAA
--text-color-secondary--color-contrast-600Secondary / muted text, AA
--text-color-tertiary--color-contrast-500Placeholder, hint text — AA large
--text-color-disabled--color-contrast-400Disabled state — decorative only
--text-color-contrast--color-contrast-100Text on dark/colored backgrounds
css
/* <sg-icon name="check" size="16"></sg-icon> Good — semantic token adapts to light and dark automatically */
color: var(--text-color-body);

/* <sg-icon name="x" size="16"></sg-icon> Avoid — raw contrast value is less expressive */
color: var(--color-contrast-800);

Semantic Colors

Each semantic color ships with 7 coordinated sub-tokens that cover every common UI need. All values use light-dark() and adapt to the active color scheme.

Sub-tokenPurpose
--color-{name}Base interactive color (buttons, links, highlights)
--color-{name}-backdropTinted surface behind a colored element
--color-{name}-contentForeground text/icon on the base color
--color-{name}-contrastHigh-contrast surface — light in light mode, dark in dark mode
--color-{name}-focusHover / active state shift of the base color
--color-{name}-borderBorder treatment at reduced opacity
--color-{name}-focus-shadowFocus ring box-shadow with spread + --shadow-sm

Available semantic color families: neutral, primary, secondary, info, success, warning, error.

css
/* Example — primary color family */
--color-primary             /* base */
--color-primary-backdrop    /* tinted surface */
--color-primary-content     /* text/icon on primary */
--color-primary-contrast    /* inverse surface */
--color-primary-focus       /* hover/active */
--color-primary-border      /* border at 60% opacity */
--color-primary-focus-shadow /* focus ring */

Halo Shadows

Each semantic color also provides a matching --halo-shadow-{name} value — a multi-layer box-shadow that creates a soft branded glow around interactive elements.

css
--halo-shadow-neutral
--halo-shadow-primary
--halo-shadow-secondary
--halo-shadow-info
--halo-shadow-success
--halo-shadow-warning
--halo-shadow-error

Typography Scale

Sigil uses two parallel font-size scales to cleanly separate body text sizing from display heading sizing.

Body Scale (--text-*)

Used by default for all non-heading text:

TokenValueUsage
--text-2xs0.625rem (10px)Micro labels, dot-only badges
--text-xs0.75rem (12px)Small labels, captions
--text-sm0.875rem (14px)Secondary text, labels
--text-base1rem (16px)Body text (minimum accessible size)
--text-lg1.125rem (18px)Large UI text
--text-xl1.25rem (20px)Subheadings
--text-2xl1.5rem (24px)Major display text
--text-3xl1.875rem (30px)Display / circular progress label

Heading Scale (--heading-*)

Used exclusively by variant="heading" on <sg-text>:

TokenValueUsage
--heading-xs0.875rem (14px)Tiny UI heading
--heading-sm1rem (16px)Small heading
--heading-md1.5rem (24px)Default heading size
--heading-lg2rem (32px)Section heading
--heading-xl3rem (48px)Page heading
--heading-2xl4rem (64px)Hero / display heading

Font Weights

css
--font-thin: 100;
--font-extralight: 200;
--font-light: 300;
--font-normal: 400; /* minimum for body text */
--font-medium: 500;
--font-semibold: 600;
--font-bold: 700;
--font-extrabold: 800;
--font-black: 900;

Line Heights

css
--leading-none: 1;
--leading-tight: 1.15; /* headings */
--leading-snug: 1.375;
--leading-normal: 1.5; /* body text — WCAG recommended */
--leading-relaxed: 1.625;
--leading-loose: 2;

/* Absolute (pixel-aligned) */
--leading-3: 0.75rem; /* 12px */
--leading-4: 1rem; /* 16px */
--leading-5: 1.25rem; /* 20px */
--leading-6: 1.5rem; /* 24px */
--leading-7: 1.75rem; /* 28px */
--leading-8: 2rem; /* 32px */
--leading-9: 2.25rem; /* 36px */
--leading-10: 2.5rem; /* 40px */

Letter Spacing

css
--tracking-tight: -0.025em; /* -2.5% — headers, display text */
--tracking-header: -0.025em; /* alias for --tracking-tight */
--tracking-normal: 0em; /* default body tracking */
--tracking-wide: 0.05em; /* labels, badges, uppercase caps */

Cascade Layer

All Sigil tokens are defined inside @layer sigil.tokens { … }. This means any unlayered :root rule in your own stylesheet wins automatically — no !important needed for overrides.

css
/* <sg-icon name="check" size="16"></sg-icon> This beats sigil.tokens without !important */
:root {
  --color-primary: light-dark(oklch(55% 0.18 220deg), oklch(62% 0.18 220deg));
}

Dark Mode

Sigil uses the CSS light-dark() function to define every color token once with both a light and dark value. color-scheme: light dark on :root means the OS preference is respected automatically — no @media block required.

How it works

Every color variable in the theme is defined using OKLCH for perceptually-uniform lightness:

css
--color-primary: light-dark(oklch(56% 0.22 293deg), oklch(62% 0.22 293deg));

OKLCH provides two key advantages over HSL: lightness steps that look visually equal across the ramp, and a wider gamut for P3 displays.

Two rules on <html> override the OS preference when needed:

css
html.dark {
  color-scheme: dark;
} /* force dark */

html:not(.dark) {
  color-scheme: light;
} /* force light */

VitePress Integration

VitePress automatically adds .dark to <html> when the user toggles the theme. Sigil responds to this automatically — no extra configuration needed.

Manual Control

For other frameworks, toggle the .dark class on <html>:

javascript
// Switch to dark
document.documentElement.classList.add('dark');

// Switch to light (remove .dark — falls back to OS preference)
document.documentElement.classList.remove('dark');

// Toggle
document.documentElement.classList.toggle('dark');

Transitions & Animations

Sigil provides pre-built transition and animation tokens. Under prefers-reduced-motion: reduce, all --duration-* tokens resolve to 0ms and all --transition-* tokens resolve to none — no extra configuration needed.

Duration Scale

css
--duration-75: 75ms;
--duration-100: 100ms;
--duration-150: 150ms;
--duration-200: 200ms;
--duration-300: 300ms;
--duration-500: 500ms;
--duration-700: 700ms;
--duration-1000: 1000ms;

Easing

css
--ease-linear: linear;
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-spring: cubic-bezier(0.22, 1, 0.36, 1); /* fast start, soft landing — no overshoot */

Pre-built Transitions

css
--transition-none: none;
--transition-fast: 150ms var(--ease-in-out);
--transition-normal: 200ms var(--ease-in-out);
--transition-slow: 300ms var(--ease-in-out);
--transition-slower: 500ms var(--ease-in-out);
--transition-spring: 300ms var(--ease-spring);

WARNING

--transition-all has been removed. Use explicit property-specific transitions (transition: color var(--transition-fast), background var(--transition-fast)) to avoid forcing layout recalculation on every style change.

Global Customization

Override the default theme by setting CSS variables in your root stylesheet. A plain value always wins over light-dark() — use light-dark() yourself when you want the override to stay mode-aware:

css
:root {
  /* Mode-aware override — adapts to light/dark automatically (OKLCH recommended) */
  --color-primary: light-dark(oklch(55% 0.18 220deg), oklch(62% 0.18 220deg));

  /* Static override — same in both modes */
  --rounded-md: 0.5rem;
  --size-4: 1.2rem;
}

Component Customization

Each component exposes specific CSS custom properties for fine-tuned control. Set them inline or in a stylesheet scoped to your component.

html
<sg-button
  style="
    --button-bg: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
    --button-radius: 20px;
    --button-padding: 0.75rem 2rem;
  ">
  Gradient Button
</sg-button>

Component-Specific Variables

TIP

Refer to each component's documentation for the complete list of CSS custom properties.

css
/* sg-text */
--text-size           /* font-size */
--text-weight         /* font-weight */
--text-color          /* color */
--text-line-height    /* line-height */
--text-letter-spacing /* letter-spacing */

/* sg-button */
--button-bg
--button-color
--button-border
--button-radius
--button-padding

/* sg-input */
--input-bg
--input-color
--input-border-color
--input-placeholder-color
--input-radius

/* sg-checkbox */
--checkbox-size
--checkbox-bg
--checkbox-checked-bg
--checkbox-radius

Advanced Theming

Overriding the Neutral Palette

All uncolored Sigil components (buttons, inputs, chips, etc. without a color attribute) use the neutral token family. Override it globally with the --sigil-color-neutral-* surface — no !important needed:

css
:root {
  --sigil-color-neutral: light-dark(oklch(50% 0.01 240deg), oklch(68% 0.01 240deg));
  --sigil-color-neutral-focus: light-dark(oklch(56% 0.01 240deg), oklch(74% 0.01 240deg));
  --sigil-color-neutral-backdrop: light-dark(oklch(95% 0.005 240deg / 83%), oklch(25% 0.005 240deg / 60%));
  --sigil-color-neutral-border: light-dark(oklch(84% 0.006 240deg / 75%), oklch(28% 0.006 240deg / 75%));
  --sigil-color-neutral-content: light-dark(oklch(40% 0.01 240deg), oklch(80% 0.01 240deg));
  --sigil-color-neutral-contrast: light-dark(oklch(99% 0 240deg), oklch(13% 0 240deg));
}

This affects every component that defaults to neutral — buttons, chips, badges, inputs — without needing color="primary" on each element. Follow the same OKLCH + light-dark() pattern to keep both modes consistent.

TIP

The six --sigil-color-neutral-* tokens mirror the --color-{name}-* family shape. All six must be defined together to avoid partially-themed components.

Creating Custom Theme Variants

Create custom theme variants by defining new color palettes on a scoped selector. Use the full 7-token pattern to keep components consistent:

css
/* Ocean theme — OKLCH for perceptual uniformity, light-dark() for mode-awareness */
.theme-ocean {
  --color-primary: light-dark(oklch(55% 0.18 220deg), oklch(62% 0.18 220deg));
  --color-primary-backdrop: light-dark(oklch(93% 0.05 220deg), oklch(28% 0.08 220deg / 40%));
  --color-primary-content: light-dark(oklch(22% 0.08 220deg), oklch(95% 0.02 220deg));
  --color-primary-contrast: light-dark(oklch(98% 0.01 220deg), oklch(14% 0.05 220deg));
  --color-primary-focus: light-dark(oklch(61% 0.18 220deg), oklch(68% 0.18 220deg));
  --color-primary-border: light-dark(oklch(61% 0.18 220deg / 60%), oklch(68% 0.18 220deg / 60%));
  --color-primary-focus-shadow: 0 0 0 4px color-mix(in oklch, var(--color-primary) 40%, transparent), var(--shadow-sm);
}

/* Sunset theme */
.theme-sunset {
  --color-primary: light-dark(oklch(60% 0.2 35deg), oklch(66% 0.2 35deg));
  --color-primary-backdrop: light-dark(oklch(93% 0.06 35deg), oklch(28% 0.09 35deg / 40%));
  --color-primary-content: light-dark(oklch(22% 0.09 35deg), oklch(95% 0.02 35deg));
  --color-primary-contrast: light-dark(oklch(98% 0.01 35deg), oklch(14% 0.06 35deg));
  --color-primary-focus: light-dark(oklch(66% 0.2 35deg), oklch(72% 0.2 35deg));
  --color-primary-border: light-dark(oklch(66% 0.2 35deg / 60%), oklch(72% 0.2 35deg / 60%));
  --color-primary-focus-shadow: 0 0 0 4px color-mix(in oklch, var(--color-primary) 40%, transparent), var(--shadow-sm);
}

Dynamic Theme Switching

typescript
type ThemeName = 'ocean' | 'sunset';

const themes: Record<ThemeName, Record<string, string>> = {
  ocean: {
    '--color-primary': 'light-dark(oklch(55% 0.18 220deg), oklch(62% 0.18 220deg))',
    '--color-primary-backdrop': 'light-dark(oklch(93% 0.05 220deg), oklch(28% 0.08 220deg / 40%))',
    '--color-primary-content': 'light-dark(oklch(22% 0.08 220deg), oklch(95% 0.02 220deg))',
    '--color-primary-contrast': 'light-dark(oklch(98% 0.01 220deg), oklch(14% 0.05 220deg))',
    '--color-primary-focus': 'light-dark(oklch(61% 0.18 220deg), oklch(68% 0.18 220deg))',
    '--color-primary-border': 'light-dark(oklch(61% 0.18 220deg / 60%), oklch(68% 0.18 220deg / 60%))',
  },
  sunset: {
    '--color-primary': 'light-dark(oklch(60% 0.20 35deg), oklch(66% 0.20 35deg))',
    '--color-primary-backdrop': 'light-dark(oklch(93% 0.06 35deg), oklch(28% 0.09 35deg / 40%))',
    '--color-primary-content': 'light-dark(oklch(22% 0.09 35deg), oklch(95% 0.02 35deg))',
    '--color-primary-contrast': 'light-dark(oklch(98% 0.01 35deg), oklch(14% 0.06 35deg))',
    '--color-primary-focus': 'light-dark(oklch(66% 0.20 35deg), oklch(72% 0.20 35deg))',
    '--color-primary-border': 'light-dark(oklch(66% 0.20 35deg / 60%), oklch(72% 0.20 35deg / 60%))',
  },
};

function applyTheme(name: ThemeName) {
  const theme = themes[name];
  Object.entries(theme).forEach(([key, value]) => {
    document.documentElement.style.setProperty(key, value);
  });
}

// Usage
applyTheme('ocean');

Best Practices

Use Semantic Tokens

Prefer semantic tokens over raw contrast values:

css
/* <sg-icon name="check" size="16"></sg-icon> Good — semantic tokens */
color: var(--text-color-body);
background: var(--color-primary-backdrop);

/* <sg-icon name="x" size="16"></sg-icon> Avoid — raw contrast values */
color: var(--color-contrast-800);
background: hsl(260deg 85% 65% / 20%);

Respect the Contrast Scale

Use the background range (50–400) for surfaces and the text range (500–900) for text:

css
/* <sg-icon name="check" size="16"></sg-icon> Good */
background: var(--color-contrast-100); /* card surface */
color: var(--text-color-body); /* body text */

/* <sg-icon name="x" size="16"></sg-icon> Avoid — text-range value used as background */
background: var(--color-contrast-700);

Override with light-dark() for Mode-Aware Colors

Flat overrides are always light (or always dark). Wrap in light-dark() to keep an override mode-aware:

css
:root {
  /* <sg-icon name="check" size="16"></sg-icon> Adapts to light and dark (OKLCH recommended) */
  --color-primary: light-dark(oklch(55% 0.18 220deg), oklch(62% 0.18 220deg));

  /* <sg-icon name="triangle-alert" size="16"></sg-icon> Static — always the same regardless of color scheme */
  --color-primary: oklch(58% 0.18 220deg);
}

Maintain WCAG Compliance

When customizing colors, verify contrast ratios:

  • AA: 4.5:1 for normal text, 3:1 for large text
  • AAA: 7:1 for normal text, 4.5:1 for large text
css
:root {
  /* OKLCH — perceptually uniform, wide gamut, light-dark() aware */
  --custom-bg: light-dark(oklch(99% 0.004 264deg), oklch(13% 0.005 250deg));
  --custom-text: light-dark(oklch(12% 0.008 264deg), oklch(95% 0.003 250deg)); /* ~17:1 in both modes — AAA ✓ */
}

Set the Full Token Set for Custom Colors

When introducing a brand color, define all 7 sub-tokens so every component renders correctly in both modes:

css
.my-brand {
  /* Use OKLCH for perceptual uniformity; wrap in light-dark() for automatic mode-switching */
  --color-primary: light-dark(oklch(L% C H), oklch(L% C H));
  --color-primary-backdrop: light-dark(oklch(93% C H), oklch(28% C H / 40%));
  --color-primary-content: light-dark(oklch(22% C H), oklch(95% C H));
  --color-primary-contrast: light-dark(oklch(98% C H), oklch(14% C H));
  --color-primary-focus: light-dark(oklch((L + 6) % C H), oklch((L + 6) % C H));
  --color-primary-border: light-dark(oklch((L + 6) % C H / 60%), oklch((L + 6) % C H / 60%));
  --color-primary-focus-shadow: 0 0 0 4px color-mix(in oklch, var(--color-primary) 40%, transparent), var(--shadow-sm);
}