Theming

Components use semantic Tailwind tokens backed by CSS variables, generated from typed data in registry/aedis/lib/tokens.ts. Light and dark are the default; the Aedis brand (navy, gold, cream) is an extra theme. Never edit a component to change how it looks.

Choosing a theme

Without data-theme, the theme follows the system. Set it on <html> to choose one.

<html data-theme="dark">   <!-- "light", "dark" or "brand" -->

Color

TokenlightdarkbrandUsed for
--background#f6f7f9#0d1015#f5f1e8Page background.
--surface#ffffff#161a21#fbf8f1Cards, panels and fields on the background.
--surface-raised#ffffff#1d222b#ffffffPopovers, menus and dialogs. In dark it is lighter instead of relying on shadow alone.
--muted#eef0f4#232833#ece5d5Subtle fill: tracks, zebra rows, item hover.
--foreground#161b26#eceff4#1a2740Body text.
--muted-foreground#5c6475#9ba4b4#5a6276Secondary text and captions; 4.5:1 on background, surface and muted.
--border#dce0e7#2c323d#ddd4c2Dividers and decorative card outlines. Not a control outline.
--border-control#8a92a3#646d80#8a8270Outline of fields, checkboxes and radios: 3:1 on surface (WCAG 1.4.11).
--hairlinergba(22,27,38,0.08)rgba(255,255,255,0.08)rgba(26,39,64,0.10)1 px translucent line between surfaces.
--primary#2457d6#3565e8#1a2740The one main action per area, and selection.
--primary-foreground#ffffff#ffffff#ffffffText and icons on primary.
--secondary#ffffff#161a21#fbf8f1Secondary actions (surface with a hairline).
--secondary-foreground#161b26#eceff4#1a2740Text on secondary.
--link#2457d6#7aa2ff#1a2740Links in running text, always underlined.
--accent#2457d6#7aa2ff#c8a24cAttention: counters, pending items, active indicator.
--accent-foreground#ffffff#0d1015#1a2740Text on accent.
--accent-soft#e8eefc#1b2742#f3ebd6Selected and active item fill.
--ringrgba(36,87,214,0.45)rgba(122,162,255,0.50)rgba(26,39,64,0.45)Outer focus ring; always paired with an inner ring in surface.
--success#18794e#3dd68c#18794eSuccess text and icons.
--warning#a35200#f5a524#a35200Warning before a problem.
--destructive#c9302c#ff6369#c9302cErrors and destructive actions in text and icons.
--destructive-solid#c9302c#c93a3f#c9302cDestructive button fill. Never looks like primary.
--destructive-foreground#ffffff#ffffff#ffffffText on destructive-solid.
--material-thinrgba(255,255,255,0.55)rgba(22,26,33,0.55)rgba(251,248,241,0.55)Translucent material for chips and floating controls (with blur-thin).
--material-regularrgba(255,255,255,0.72)rgba(22,26,33,0.72)rgba(251,248,241,0.72)Header, toolbar, popover (with blur-regular).
--material-thickrgba(255,255,255,0.86)rgba(22,26,33,0.86)rgba(251,248,241,0.86)Sheet, sidebar, large surfaces (with blur-thick).
--material-edgergba(255,255,255,0.60)rgba(255,255,255,0.08)rgba(255,255,255,0.60)Light top edge of a material.
--scrimrgba(13,16,21,0.32)rgba(0,0,0,0.55)rgba(26,39,64,0.32)Dims the page behind a modal.

Contrast

Every pair is checked by the test suite in every theme: 4.5:1 for text, 3:1 for control outlines. Translucent colors are measured over the page background.

Foreground on backgroundMinimumlightdarkbrand
foreground on background4.5:116.1:116.5:113.2:1
muted-foreground on background4.5:15.5:17.6:15.4:1
foreground on surface4.5:117.2:115.1:114.1:1
muted-foreground on surface4.5:15.9:16.9:15.7:1
foreground on surface-raised4.5:117.2:113.8:114.9:1
muted-foreground on surface-raised4.5:15.9:16.4:16.1:1
foreground on muted4.5:115.1:112.8:111.9:1
muted-foreground on muted4.5:15.2:15.9:14.9:1
primary-foreground on primary4.5:16.2:15.0:114.9:1
secondary-foreground on secondary4.5:117.2:115.1:114.1:1
accent-foreground on accent4.5:16.2:17.7:16.2:1
foreground on accent-soft4.5:114.8:112.9:112.5:1
destructive-foreground on destructive-solid4.5:15.3:15.0:15.3:1
link on surface4.5:16.2:17.0:114.1:1
link on background4.5:15.7:17.7:113.2:1
destructive on surface4.5:15.3:16.0:15.0:1
success on surface4.5:15.4:19.3:15.1:1
warning on surface4.5:15.6:18.5:15.3:1
border-control on surface3:13.1:13.4:13.6:1
foreground on material-thick4.5:117.1:115.3:113.9:1

Typography

The system font (San Francisco on Apple, Segoe UI Variable on Windows), in rem so it follows the reader's text size. Tracking tightens and leading drops as the size grows.

text-displaySecure foundations, fluid interface3rem / 1.05 / -0.025em / 700
text-title-1Secure foundations, fluid interface2.125rem / 1.12 / -0.02em / 700
text-title-2Secure foundations, fluid interface1.625rem / 1.18 / -0.015em / 600
text-title-3Secure foundations, fluid interface1.25rem / 1.25 / -0.01em / 600
text-headlineSecure foundations, fluid interface1.0625rem / 1.35 / -0.005em / 600
text-bodySecure foundations, fluid interface1rem / 1.5 / 0em / 400
text-calloutSecure foundations, fluid interface0.9375rem / 1.45 / 0em / 400
text-footnoteSecure foundations, fluid interface0.8125rem / 1.4 / 0.005em / 400
text-captionSecure foundations, fluid interface0.75rem / 1.35 / 0.01em / 500
text-codeSecure foundations, fluid interface0.875rem / 1.6 / 0em / 400

Shape

Concentric radii: inside a rounded surface, the inner radius is the outer one minus the padding. A rounded-lg (16px) card with 8px of padding holds a rounded-sm (8px) button.

rounded-xs · 6px
rounded-sm · 8px
rounded-md · 12px
rounded-lg · 16px
rounded-xl · 22px

Depth and material

shadow-elevation-1
shadow-elevation-2
shadow-elevation-3
shadow-elevation-4

material-thin, material-regular and material-thick are translucent with a backdrop blur (thin 12px, regular 20px, thick 28px) and a light top edge. They turn solid under prefers-reduced-transparency; borders become control outlines under prefers-contrast: more. Where content scrolls under floating chrome, scroll-edge and scroll-fade-y fade it instead of drawing a line.

Motion

Cinematic, never stiff: long soft entrances, short exits, every transition interruptible. Springs are sampled with linear(), so there is no dependency. press sinks to 0.96 on pointer-down; materialize grows popovers and dialogs out of their trigger from 0.86 with a 6px blur clearing. On a page, enter brings its children in one after another (90ms apart) as the page loads, and lift raises an interactive card 4 px on the spring with a deeper shadow. Under prefers-reduced-motion scale, blur and bounce turn into a short cross-fade. For code on a dark surface in any theme, such as a terminal on a light page, put terminal on the CodeBlock: it uses the dark theme's own colors.

DurationValue
--duration-press140ms
--duration-release520ms
--duration-fast200ms
--duration-base380ms
--duration-spring560ms
--duration-sheet700ms
--duration-stagger50ms
--duration-enter900ms
--duration-enter-stagger90ms
EasingValue
ease-outcubic-bezier(0.2, 0.8, 0.2, 1)
ease-cinematiccubic-bezier(0.16, 1, 0.3, 1)
ease-spring-criticallinear(0, 0.191 10%, 0.475 20%, 0.692 30%, 0.829 40%, 0.908 50%, 0.952 60%, 0.976 70%, 0.988 80%, 0.994 90%, 1)
ease-spring-bouncylinear(0, 0.208 10%, 0.537 20%, 0.787 30%, 0.93 40%, 0.993 50%, 1.013 60%, 1.014 70%, 1.01 80%, 1.005 90%, 1)

Custom themes

A theme is typed data, never CSS. @aedis/custom-theme validates it and applies it variable by variable through the CSSOM, so an imported or shared theme cannot inject rules, import a stylesheet, reach a url() or hide a safety cue.

  • Colors only as hex or oklch(), parsed and written back in canonical form; radius, blur and shadow strength only as numbers within a range.
  • Fonts from a closed list of system fonts: nothing is downloaded, so a font cannot be used to track who opened the page.
  • Every text and control pair must pass contrast AA in light and dark, and the destructive action can never look like the primary one; otherwise the theme is refused.
  • Themes change color and shape, never display, visibility or opacity.
  • Stored and shared themes are untrusted: size-limited (16 KB), validated again on load, hidden characters removed from the name.
import { applyTheme, parseThemeJson, themeToCss } from "@/lib/custom-theme";

const result = parseThemeJson(saved);          // untrusted: validated again
if (result.ok) applyTheme(result.theme, { mode: "dark" });

// Server rendering: only validated values reach the page.
<style nonce={nonce}>{themeToCss(theme)}</style>

Passes contrast AA in light and dark. This is the theme being previewed and saved.

Approval pending

The agent wants to send the report outside the company.

3 waitingDraft Autopilot Notify
x