saturation/uisaturation/ui
DocsComponentsBlocksPagesEmails
GitHub

Sections

IntroductionComponentsInstallationMCPThemingDesign

Components

Components

General

AvatarBadgeButtonKbdProgressSeparatorSkeletonSpin ResolveSpinnerSync ButtonTypography

Forms & Inputs

Address LookupCalendar PickerCheckboxComboboxDate PickerEmoji PickerFavicon SearchFieldInputInput GroupInput OTPRadio GroupSelectSliderSwitchTextareaToggleToggle Group

Data Display

AccordionAlertCardChartComparison SliderCredit CardData TableEmptyItemSaturation Credit CardTableTree

Navigation

BreadcrumbCommandMenubarNavigation MenuPaginationTabs

Overlays

CollapsibleContext MenuDialogDropdown MenuSheet

Layout

Button GroupFont ProviderWizard Split Layout

Feedback

Sonner

Animation & Effects

Animated GroupAnimated ListAnimated NumberBeamBlur FadeBorder TrailGlow EffectLiquid MetalLoading StateParallaxPixelProgressive BlurRippleSpotlightText EffectText Shimmer

Productivity

Agent ChatAI Chat InputCoding AgentFiltersFull CalendarKanbanNovel Editor
Docs/Design

Design

The Saturation design system specification — tokens, principles, and the complete component inventory.

This page is generated from DESIGN.md at the repo root. Fetch the raw spec (with YAML token frontmatter) at ui.saturation.io/design.md — designed for AI agents, design tools, and Tailwind/DTCG token export pipelines.

Saturation Design System

This document is the canonical specification for Saturation's visual identity. It is the source of truth that both humans and AI agents read to make consistent design decisions across the registry (ui.saturation.io), the marketing site (saturation.io), and the Saturation product app.

Overview

Saturation is production-grade tooling for film and video production. The design language reflects what production teams need from their software: calm, dense, and unobtrusive. The interface should disappear in favor of the work — schedules, budgets, call sheets, contacts. It is professional, not playful; precise, not decorative.

Voice. Direct, lower-case-friendly, technical without being clinical. Borrowed cues from production-craft tools (Avid, DaVinci, the call-sheet) more than from consumer SaaS.

Density. Dense by default. Production users live in tables and lists with hundreds of rows. Generous whitespace is reserved for marketing surfaces (saturation.io); product surfaces favor information density and predictable rhythms over breathing room.

Color space. Tokens in this file are written as #hex for sRGB compatibility with the @google/design.md tooling. The runtime canonical values live as oklch() in packages/react/src/styles/global.css for perceptually-uniform interpolation and dark-mode parity. When a token value disagrees, global.css wins. The hex values here are computed from oklch via scripts/oklch-to-hex.ts.

Two modes. Light mode tokens are the un-prefixed values above. Dark mode tokens are exposed as dark-* siblings in the same colors: map (dark-primary, dark-foreground, etc.) — the spec doesn't natively support multi-theme, so we use a prefix convention. Both light and dark are auto-synced from packages/react/src/styles/global.css by scripts/sync-design-tokens.ts. Canonical runtime values live in .dark of global.css as oklch.

State variants. Interaction states (hover, focus, active, disabled) are encoded as separate component recipes with name suffixes — button-primary-hover, input-focus, sidebar-item-active, etc. — matching the spec's idiomatic pattern from the Heritage and Totality Festival reference designs. Opacity-based hovers (e.g. bg-primary/75) are precomputed against the background surface and stored as 6-digit hex; the variants therefore look correct on a typical surface but may drift on inverted or colored surfaces.

Colors

The palette is built around a single high-saturation blue accent against a neutral zinc grayscale. There is no gratuitous color; chart palettes are the only place we reach beyond the system.

Semantic roles

  • Primary (#1B4DC4) — Saturation Blue. The single interactive accent. Used for primary buttons, links, focus rings, and selection states. Never decorative; if it's blue, it's interactive or selected.
  • Foreground (#09090B) — near-black. Body text and high-contrast UI.
  • Background (#FCFCFC) — off-white. The page floor.
  • Muted (#F4F4F5) / Muted-foreground (#71717B) — secondary surfaces and supporting text.
  • Border (#F4F4F5) — soft separators. Same value as muted for a cohesive flat aesthetic.
  • Input (#D4D4D8) — input borders, slightly stronger than border to communicate interactivity.
  • Destructive (#E7000B) — used only for irreversible destructive actions (delete, remove, force). Not for general errors — use form-level error states for those.
  • Accent (#F4F4F5) / Accent-foreground (#3F3F46) — hover states and subtle emphasis.

Charts

The chart palette is deliberately wide-gamut — purple, blue, orange, pink, gray — to maximize legibility across many concurrent series in budget and schedule visualizations. Never use chart colors for UI chrome.

Badges

A semantic badge palette (badge-red, badge-cyan, badge-amber, badge-green, badge-gray, badge-yellow, badge-blue, badge-purple) maps to status concepts in the product (e.g. amber = pending approval, green = confirmed, red = canceled). Badges have explicit dark-mode pairs because their saturation needs adjustment, not just inversion.

Dark mode

Dark mode is not an inversion — it is its own designed palette. Background drops to #09090B, primary brightens to #2158DD to maintain perceived saturation against the dark backdrop, and destructive shifts to #FF6467 for the same reason. See .dark in packages/react/src/styles/global.css.

Typography

Fonts are configurable per surface via the font-provider preset (next / gellix / marketing), which scopes --font-sans and --font-display over a subtree. The product app (next-web) renders in the system sans stack (ui-sans-serif, system-ui, sans-serif); it does not load Gellix. Marketing (saturation.io) loads Gellix and applies it to display headings via the marketing preset (system body, Gellix display); the gellix preset puts Gellix on body too. Geist Mono is used for code, numerical data, and timestamps on every surface. Georgia is a serif fallback for marketing prose only. The fontFamily values in the typography: block below name each role's default family in the registry/marketing context; the product overrides the sans roles to the system stack via the next preset.

Scale

The type scale is tight: nine primary roles. We do not multiply roles for marketing flourish.

  • display (3rem / 600) — marketing hero only. Not used in product.
  • h1 (1.875rem / 600) — page title in product; section title in marketing.
  • h2 (1.25rem / 500) — section heading.
  • h3 (1.125rem / 500) — subsection.
  • body (0.9375rem / 400) — default UI text and content body. Tighter than the marketing default.
  • body-lg (1rem / 400) — lead paragraphs and the page-description line under an h1.
  • label (0.875rem / 500) — buttons, form labels, table headers.
  • caption (0.75rem / 500) — metadata, timestamps, status text.
  • cell (0.8125rem / 400): grid and table cell + column header, the dense product workhorse. System sans, not used in marketing.
  • code / code-inline — Geist Mono. Inline code uses 0.8em to read as integrated prose, not a callout.

Letter-spacing

Headlines tighten (-0.025em / -0.02em) for optical density. Body and label text use the default. We never positive-letter-space ALL CAPS — uppercase is reserved for badge text and uses normal spacing.

Layout

The product is structured around a persistent left sidebar + main content split. The marketing site is more conventional — full-width sections with content centered.

Layout tokens

  • --header-height: 3.5rem (56px) — top app bar.
  • --sidebar-width: 18rem (288px) — primary navigation.
  • --sidebar-menu-width: 14rem (224px) — secondary nav within a section.
  • --spacing: 0.25rem (4px) — base unit. All Tailwind spacing utilities derive from this.

Container

The marketing surface uses .container-wrapper (mx-auto w-full max-w-screen-2xl, 1536px). The product surface does not constrain width — it fills the viewport, since most users dock the app full-width on a single monitor.

Breakpoints

Standard Tailwind: sm 640, md 768, lg 1024, xl 1280, 2xl 1536. Mobile is supported for marketing and read-only product views (call sheets); core production tooling targets desktop.

Density

Default row heights aim at ~36px (8 spacing units). Tables and list rows go to 32px (py-2) for high-density data. Avoid going below 28px — touch targets and click zones suffer.

Elevation & Depth

Saturation is a flat system. We use border separation and background color shifts before we reach for shadow.

  • Surface levels. background (page) → card/popover (elevated content) → accent (hover, selected). Levels are distinguished by a 1-2% lightness step in oklch, not by drop shadow.
  • Borders. All elevated surfaces have a 1px border color border. This is the primary separation cue.
  • Shadows. Reserved for floating overlays only — popovers, dialogs, dropdowns. Use the Tailwind shadow-md / shadow-lg defaults; do not author bespoke shadows.
  • Z-index. Defer to component primitives (Radix manages layering for portals). Do not introduce custom z-index outside of Radix layers.

Shapes

Radius is uniform and small. There are no circles outside of avatars and toggle dots, no ovals, no asymmetric corners.

  • rounded.sm (0.4rem) — small interactive elements: chips, tag-removal buttons, segmented controls.
  • rounded.md (0.675rem) — buttons, inputs, badges, dropdown items. The default for interactive UI.
  • rounded.lg (0.8rem) — cards, dialogs, popovers, the canonical --radius.
  • rounded.xl (1.05rem) — code blocks and large surface tiles.
  • rounded.full — avatars and the rare circular indicator.

The base --radius is 0.8rem and all other steps derive from it (Tailwind's --radius-sm/md/lg/xl formulas in global.css). Changing the base value rescales the entire system.

Components

Component recipes live in the YAML frontmatter under components: (hand-written, encodes design intent). The inventory below is auto-derived from lib/components.registry.ts by pnpm generate:design. Live previews and full prop tables are in the docs at /docs/components.

Component tokens compose from base tokens via {token.path} references — never hardcoded values. When adding a new component recipe, the rule is: if a property could conceivably theme, it must reference a token, not a literal.

Inventory

79 components across 9 categories. Last generated by pnpm generate:design.

Navigation (6)

ComponentDescription
breadcrumbDisplays the path to the current page location and allows navigation back.
commandFast, composable command menu for React. Perfect for search, quick actions, and Cmd+K interfaces.
menubarA horizontal menu bar for navigation. Perfect for application-style navigation.
navigation-menuA navigation menu with viewport, trigger, content, and link sub-components.
paginationA navigation component for navigating through pages of content.
tabsA set of layered sections of content—known as tab panels—displayed one at a time.

Overlays (5)

ComponentDescription
collapsibleAn expandable content container that can be toggled open or closed.
context-menuDisplays a menu triggered by right-click.
dialogA modal dialog that overlays the main content.
dropdown-menuDisplays a menu triggered by a button.
sheetA slide-over dialog that overlays the main content from one side of the screen.

Layout (3)

ComponentDescription
button-groupGroups related buttons together.
font-providerSwappable font-token layer. Wrap a subtree to scope --font-sans, --font-mono, --font-display, and --font-feature-settings via CSS-variable cascade. Distinct from the Typography primitives (T1-T4, P, etc.) — FontProvider controls which font those primitives render in.
wizard-split-layoutA two-column wizard layout with an interactive side and a responsive display panel.

Animation (16)

ComponentDescription
animated-groupAnimates a group of elements with staggered entry/exit.
animated-listA dynamic list that animates items in and out with spring physics. Supports scale, slide, fade, and bounce animation styles.
animated-numberAnimates a number value with smooth transitions.
beamAnimated traveling-glow border effect. Wraps any element with a multi-layer beam that respects border-radius, theme, and interactivity. Adapted from border-beam by Jakub Antalik (MIT).
blur-fadeBlur + fade entrance animation for individual elements. Supports scroll-triggered reveal and configurable direction.
border-trailAnimated border trail effect.
glow-effectAdds a glow effect around elements.
liquid-metalWebGL-powered liquid metallic effect with animated metaballs. Supports dark/light mode, configurable blob count, speed, hue cycling, and smooth fade-in.
loading-stateFull-screen splash loader with a glowing brand logo, fake-progress bar, and shimmer text. Pair with the useLoading hook for automatic progress management.
parallaxA scroll-driven parallax wrapper with GPU-accelerated transforms and reduced motion support.
pixelInteractive canvas-based pixel grid that animates on load with configurable reveal patterns. Supports dark/light mode colors and multiple animation directions.
progressive-blurA progressive blur effect that creates a smooth gradient blur transition using multiple backdrop-filter layers.
rippleAnimated concentric ripple rings that pulse behind content for emphasis. Pure CSS animation, no JS dependencies.
spotlightCreates a spotlight effect that follows cursor.
text-effectAnimated text reveal effects.
text-shimmerShimmering text effect for loading states or emphasis.

Do's and Don'ts

Notes on the spec format

The spec format does not currently encode borders, opacity modifiers, hover/focus/active states, transitions, or shadows. These must be inferred from prose:

  • button-secondary and button-outline have a 1px border (color: {colors.input} or {colors.border}). This is implicit in the variant name; the visible "outline" look comes from the border, not the background.
  • button-ghost has no background and no border — text-only, picks up {colors.accent} on hover.
  • All buttons use the spec's height: 2.25rem (36px) and borderRadius: rounded.md (10.8px). The rounded.md value is computed as calc(var(--radius) - 2px) from the runtime --radius: 0.8rem.
  • Hover states for primary/destructive buttons darken the background by ~10%.
  • Focus-visible adds a 2px outline using {colors.ring} with a 2px offset.

Do

  • Do use semantic tokens (bg-primary, text-muted-foreground) instead of palette indices (bg-blue-600, text-zinc-500).
  • Do use the radix-ui monorepo namespace imports (import { Slot } from "radix-ui"), not @radix-ui/react-* per-package imports.
  • Do use cn() to compose className conditionals and cva() for variant-driven components.
  • Do keep marketing density airy and product density tight. They share tokens; they differ in spacing rhythm.
  • Do edit packages/react/src/styles/global.css and this DESIGN.md together when changing a token. Run pnpm design:lint to catch drift.
  • Do treat oklch() in global.css as canonical and the hex values here as a derived export.

Don't

  • Don't hardcode color values in components. If you find yourself writing #hex or rgb() outside of global.css or DESIGN.md, you are introducing drift.
  • Don't introduce a NEW font. The system uses three families: system-sans (product UI), Gellix (marketing display and the gellix preset), and Geist Mono (code and numerics everywhere). Switch between them only via font-provider presets; never hardcode a font-family or a var(--font-gellix) literal in product code. Marketing motion graphics or one-off illustrations may use anything; the interface uses these families.
  • Don't add bespoke shadow values. Use the Tailwind defaults and only on floating overlays.
  • Don't reach for chart colors to brighten UI chrome.
  • Don't use destructive for general errors. It is reserved for irreversible actions.
  • Don't edit files in public/r/ or content/docs/components/ by hand — they are generated. See the registry workflow in .claude/rules/registry-workflow.md.
  • Don't add Tailwind classes that bypass tokens (e.g. bg-[#1B4DC4]). The arbitrary-value escape hatch should be exceedingly rare and accompanied by a comment explaining why a token won't do.
NextIntroduction

On This Page

  • Saturation Design System
  • Overview
  • Colors
  • Semantic roles
  • Charts
  • Badges
  • Dark mode
  • Typography
  • Scale
  • Letter-spacing
  • Layout
  • Layout tokens
  • Container
  • Breakpoints
  • Density
  • Elevation & Depth
  • Shapes
  • Components
  • Inventory
  • Navigation (6)
  • Overlays (5)
  • Layout (3)
  • Animation (16)
  • Do's and Don'ts
  • Notes on the spec format
  • Do
  • Don't