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
| Token | light | dark | brand | Used for |
|---|---|---|---|---|
| --background | #f6f7f9 | #0d1015 | #f5f1e8 | Page background. |
| --surface | #ffffff | #161a21 | #fbf8f1 | Cards, panels and fields on the background. |
| --surface-raised | #ffffff | #1d222b | #ffffff | Popovers, menus and dialogs. In dark it is lighter instead of relying on shadow alone. |
| --muted | #eef0f4 | #232833 | #ece5d5 | Subtle fill: tracks, zebra rows, item hover. |
| --foreground | #161b26 | #eceff4 | #1a2740 | Body text. |
| --muted-foreground | #5c6475 | #9ba4b4 | #5a6276 | Secondary text and captions; 4.5:1 on background, surface and muted. |
| --border | #dce0e7 | #2c323d | #ddd4c2 | Dividers and decorative card outlines. Not a control outline. |
| --border-control | #8a92a3 | #646d80 | #8a8270 | Outline of fields, checkboxes and radios: 3:1 on surface (WCAG 1.4.11). |
| --hairline | rgba(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 | #1a2740 | The one main action per area, and selection. |
| --primary-foreground | #ffffff | #ffffff | #ffffff | Text and icons on primary. |
| --secondary | #ffffff | #161a21 | #fbf8f1 | Secondary actions (surface with a hairline). |
| --secondary-foreground | #161b26 | #eceff4 | #1a2740 | Text on secondary. |
| --link | #2457d6 | #7aa2ff | #1a2740 | Links in running text, always underlined. |
| --accent | #2457d6 | #7aa2ff | #c8a24c | Attention: counters, pending items, active indicator. |
| --accent-foreground | #ffffff | #0d1015 | #1a2740 | Text on accent. |
| --accent-soft | #e8eefc | #1b2742 | #f3ebd6 | Selected and active item fill. |
| --ring | rgba(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 | #18794e | Success text and icons. |
| --warning | #a35200 | #f5a524 | #a35200 | Warning before a problem. |
| --destructive | #c9302c | #ff6369 | #c9302c | Errors and destructive actions in text and icons. |
| --destructive-solid | #c9302c | #c93a3f | #c9302c | Destructive button fill. Never looks like primary. |
| --destructive-foreground | #ffffff | #ffffff | #ffffff | Text on destructive-solid. |
| --material-thin | rgba(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-regular | rgba(255,255,255,0.72) | rgba(22,26,33,0.72) | rgba(251,248,241,0.72) | Header, toolbar, popover (with blur-regular). |
| --material-thick | rgba(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-edge | rgba(255,255,255,0.60) | rgba(255,255,255,0.08) | rgba(255,255,255,0.60) | Light top edge of a material. |
| --scrim | rgba(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 background | Minimum | light | dark | brand |
|---|---|---|---|---|
| foreground on background | 4.5:1 | 16.1:1 | 16.5:1 | 13.2:1 |
| muted-foreground on background | 4.5:1 | 5.5:1 | 7.6:1 | 5.4:1 |
| foreground on surface | 4.5:1 | 17.2:1 | 15.1:1 | 14.1:1 |
| muted-foreground on surface | 4.5:1 | 5.9:1 | 6.9:1 | 5.7:1 |
| foreground on surface-raised | 4.5:1 | 17.2:1 | 13.8:1 | 14.9:1 |
| muted-foreground on surface-raised | 4.5:1 | 5.9:1 | 6.4:1 | 6.1:1 |
| foreground on muted | 4.5:1 | 15.1:1 | 12.8:1 | 11.9:1 |
| muted-foreground on muted | 4.5:1 | 5.2:1 | 5.9:1 | 4.9:1 |
| primary-foreground on primary | 4.5:1 | 6.2:1 | 5.0:1 | 14.9:1 |
| secondary-foreground on secondary | 4.5:1 | 17.2:1 | 15.1:1 | 14.1:1 |
| accent-foreground on accent | 4.5:1 | 6.2:1 | 7.7:1 | 6.2:1 |
| foreground on accent-soft | 4.5:1 | 14.8:1 | 12.9:1 | 12.5:1 |
| destructive-foreground on destructive-solid | 4.5:1 | 5.3:1 | 5.0:1 | 5.3:1 |
| link on surface | 4.5:1 | 6.2:1 | 7.0:1 | 14.1:1 |
| link on background | 4.5:1 | 5.7:1 | 7.7:1 | 13.2:1 |
| destructive on surface | 4.5:1 | 5.3:1 | 6.0:1 | 5.0:1 |
| success on surface | 4.5:1 | 5.4:1 | 9.3:1 | 5.1:1 |
| warning on surface | 4.5:1 | 5.6:1 | 8.5:1 | 5.3:1 |
| border-control on surface | 3:1 | 3.1:1 | 3.4:1 | 3.6:1 |
| foreground on material-thick | 4.5:1 | 17.1:1 | 15.3:1 | 13.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.
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.
Depth and material
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.
| Duration | Value |
|---|---|
| --duration-press | 140ms |
| --duration-release | 520ms |
| --duration-fast | 200ms |
| --duration-base | 380ms |
| --duration-spring | 560ms |
| --duration-sheet | 700ms |
| --duration-stagger | 50ms |
| --duration-enter | 900ms |
| --duration-enter-stagger | 90ms |
| Easing | Value |
|---|---|
| ease-out | cubic-bezier(0.2, 0.8, 0.2, 1) |
| ease-cinematic | cubic-bezier(0.16, 1, 0.3, 1) |
| ease-spring-critical | linear(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-bouncy | linear(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,visibilityoropacity. - 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.