Theming
Ten themes, each a full personality: brand color, tinted grays, corner radius, typeface and the colors that mark AI output. Every one passes WCAG AA in light and dark.
Themes
Five switches
Theme, mode, radius, density and font are independent. Set them as attributes on any element. Most apps set them once on html.
<html data-theme="iris" class="dark" data-radius="lg" data-density="compact" data-font="inter">Themes nest, so a preview, an embed or a marketing section can use a different theme from the app around it.
<section data-theme="ember"> {/* Everything in here renders in Ember, the rest of the page stays as it is. */}</section>Density
Density sets control heights, row heights, panel padding, icon sizes and the small type sizes, all in whole pixels, so borders stay sharp. The 4px spacing unit never changes. Use dense for workstation screens, compact for busy SaaS screens and comfortable for sparse ones. Like the other switches it works on any element, so one panel can be denser than the page.
| Density | Control | Row | Label text | Body text | Panel padding |
|---|---|---|---|---|---|
| dense | 24px | 24px | 11px | 12px | 12px |
| compact | 28px | 28px | 12px | 13px | 16px |
| default | 32px | 32px | 13px | 14px | 20px |
| comfortable | 36px | 36px | 13px | 14px | 24px |
Components read --control-md, --row-height, --icon-size and --text-ui. Use them in your own controls the same way: h-(--control-md), h-(--row-height). For metadata below the body size use text-2xs (11px), never smaller.
Tokens
Components only use semantic tokens. Each one maps to a Tailwind utility (bg-card, text-muted-foreground) and to a Figma variable with the same name.
Surfaces
--backgroundbg-background--cardbg-card--popoverbg-popover--mutedbg-muted--accentbg-accent--secondarybg-secondaryText
--foregroundtext-foreground--muted-foregroundtext-muted-foreground--brand-subtle-foregroundtext-brand-subtle-foregroundBrand
--primarybg-primary--primary-hoverbg-primary-hover--primary-foregroundtext-primary-foreground--brand-subtlebg-brand-subtle--ringborder-ringBorders
--borderborder-border--border-strongborder-border-strong--inputborder-inputStatus
--successbg-success--warningbg-warning--infobg-info--destructivebg-destructive--success-subtlebg-success-subtle--warning-subtlebg-warning-subtle--info-subtlebg-info-subtle--destructive-subtlebg-destructive-subtleAI
--ai-1bg-ai-1--ai-2bg-ai-2--ai-3bg-ai-3--ai-subtlebg-ai-subtle--ai-borderborder-ai-border--ai-foregroundtext-ai-foregroundColors for canvas, maps and charts
Tokens are OKLCH in CSS. Renderers that take color strings instead of CSS variables, such as canvas, WebGL, MapLibre and D3, read them resolved to sRGB with tokenColor. It follows the theme, mode and any scoped data-theme, and returns null during server rendering, so draw in an effect.
import { tokenColor, useTokenColors } from "@/lib/token-color"
// Once, for example in an event handler:const ink = tokenColor("chart-1") // "#2563eb" in this theme
// In React, updated when the theme or mode changes:const colors = useTokenColors(["chart-1", "severity-high", "border"])useEffect(() => { if (colors) map.setPaintProperty("sites", "circle-color", colors["chart-1"])}, [colors])Severity has its own scale (severity-critical, -high, -elevated, -routine), apart from status and chart colors. tertiary-foreground is for metadata that should recede further than muted text and still reads at 4.5:1.
Make your own theme
Themes are generated from palette definitions in OKLCH. Add an entry, run npm run tokens, and the generator builds both modes, tunes lightness until every text pair passes contrast, and fails the build if it cannot.
// tokens/themes.ts{ name: "harbor", label: "Harbor", description: "Deep teal for logistics.", brand: "verdigris", gray: "slate", radius: "md", font: "inter", ai: ["verdigris", "sky", "iris"], chart: ["sky", "moss", "ember", "plum"],}Starting from a brand color or a logo? Create your own theme in the browser. The generator runs the same code, shows the result in light and dark, and exports the CSS, the Figma tokens and the entry above.