Glass Design System
Apple-style liquid glass material contract for every surface of the template (tokens, utilities, components, motion, accessibility, RTL).
Glass Design System (contract)
This document is the binding contract between tooling/tailwind (tokens + utilities), packages/ui (components) and the apps (saas, marketing, docs). Every name below is stable API. Change it here first, then in code.
1. Principles
- Material, not decoration. Glass is a layered material: wallpaper → blur → fill → stroke → specular → content. Nothing is "just transparent".
- Hierarchy through depth. Three levels only:
subtle(in-page panels),regular(cards, sidebars),strong(overlays: dialogs, menus, sheets, toasts). Never stackstrongonstrong. - Legibility first. Text always sits on a fill ≥ 55% opacity in light and ≥ 60% in dark. Contrast targets: body 4.5:1, large 3:1, measured against the worst-case wallpaper region.
- Restraint. One accent hue. Neutral grays carry a faint cool cast. No gradients on text. No glow.
- Physics-like motion. One spring easing, three durations. Press feedback scales, never darkens alone.
- Fallback parity. With
backdrop-filterunavailable, reduced transparency, or forced colors, every surface degrades to a solid, opaque equivalent with identical layout. - RTL native. Arabic is the default locale. Only logical properties and logical Tailwind utilities (
ms-,me-,ps-,pe-,start-,end-,text-start). Mirrors: chevrons, progress, sheets, sidebars.
2. Tokens (tooling/tailwind/theme.css)
2.1 Neutral + accent scale
| Token | Light | Dark | Note |
|---|---|---|---|
--gray-50 … --gray-950 | OKLCH, hue 250, chroma 0.004–0.012 | same scale | Apple-like cool neutral. Replaces olive. |
--accent-50 … --accent-950 | OKLCH hue 38.2 (brand orange) | same | Single brand accent, ramped per step because a warm hue holds more chroma than a cool one. --accent-500 is the identity colour exactly (#E75724); --accent-600 is the solid-button step that keeps white labels at ≥ 4.5:1. Swappable by editing one hue variable --accent-h. |
2.2 Semantic (kept compatible with supastarter names)
--background, --foreground, --card, --card-foreground, --popover, --popover-foreground, --primary (= accent 600 light / accent 400 dark), --primary-foreground, --secondary, --secondary-foreground, --muted, --muted-foreground, --accent, --accent-foreground, --touch (alias of primary, kept for upstream compatibility), --touch-foreground, --success, --warning, --destructive (+ -foreground), --rating (rating stars and score meters; deliberately the brand hue, so a score reads as the product's own signal rather than an alert), --border, --input, --ring.
--radius: 1rem. Derived: sm 0.5rem, md 0.75rem, lg 1rem, xl 1.25rem, 2xl 1.5rem, 3xl 2rem, full.
2.3 Glass material
| Token | Light | Dark |
|---|---|---|
--glass-blur | 20px | 24px |
--glass-blur-strong | 40px | 48px |
--glass-saturate | 180% | 160% |
--glass-fill-subtle | oklch(100% 0 0 / 0.38) | oklch(22% 0.01 250 / 0.42) |
--glass-fill | oklch(100% 0 0 / 0.58) | oklch(20% 0.01 250 / 0.60) |
--glass-fill-strong | oklch(100% 0 0 / 0.78) | oklch(18% 0.01 250 / 0.80) |
--glass-stroke | oklch(100% 0 0 / 0.65) | oklch(100% 0 0 / 0.10) |
--glass-stroke-outer | oklch(0% 0 0 / 0.06) | oklch(0% 0 0 / 0.45) |
--glass-specular | linear-gradient(180deg, oklch(100% 0 0 / 0.55), transparent 40%) | … / 0.10 … |
--glass-shadow | 0 1px 2px oklch(0 0 0/.04), 0 12px 32px -8px oklch(0 0 0/.12) | 0 1px 2px oklch(0 0 0/.3), 0 16px 40px -8px oklch(0 0 0/.55) |
--glass-shadow-strong | 0 2px 4px oklch(0 0 0/.06), 0 24px 64px -12px oklch(0 0 0/.22) | 0 2px 6px oklch(0 0 0/.4), 0 32px 80px -12px oklch(0 0 0/.7) |
--glass-tint | var(--primary) | var(--primary) |
--glass-noise | data-URI SVG feTurbulence, opacity 0.035 | opacity 0.05 |
2.4 Motion
--ease-spring: cubic-bezier(0.32, 0.72, 0, 1); --ease-out: cubic-bezier(0.16, 1, 0.3, 1); --duration-fast: 160ms; --duration-base: 260ms; --duration-slow: 420ms; --press-scale: 0.97.
2.5 Typography
--font-sans: var(--font-plex-arabic), -apple-system, BlinkMacSystemFont, "SF Pro Text", "SF Arabic", system-ui, sans-serif. One family for headings and body (IBM Plex Sans Arabic via next/font/google, weights 400/500/600/700, subsets arabic, latin). Headings: tracking-tight in Latin only; Arabic never letter-spaced ([dir=rtl] { letter-spacing: 0 }).
3. Utilities (tooling/tailwind/glass.css, imported after theme.css)
| Utility | Effect |
|---|---|
glass | regular material: blur + saturate, --glass-fill, 1px --glass-stroke (inside, via box-shadow: inset 0 0 0 1px), outer hairline --glass-stroke-outer, --glass-shadow, specular top highlight via ::before, noise via ::after. position: relative; isolation: isolate; overflow: clip are set. |
glass-subtle | same, with --glass-fill-subtle, no outer shadow. |
glass-strong | overlays: --glass-blur-strong, --glass-fill-strong, --glass-shadow-strong. |
glass-tinted | mixes 12% --glass-tint into the fill (color-mix). Use for the active nav item, primary badges. |
glass-interactive | hover: fill +8% and translateY(-1px); active: scale(var(--press-scale)); focus-visible: 2px ring --ring offset 2px. All with --ease-spring/--duration-fast. |
glass-nav | floating bar preset: glass-strong + rounded-full + shadow tuned for top bars. |
glass-inset | recessed surface for inputs: --glass-fill-subtle, inset 0 1px 2px oklch(0 0 0/.06), stroke --input. |
wallpaper | full-bleed backdrop: layered radial gradients (accent 8%, gray, white) + noise, background-attachment: fixed on desktop only. Applied to body in saas and marketing. |
press | active:scale-[var(--press-scale)] with spring easing. |
ease-spring, duration-fast/base/slow | motion helpers. |
no-scrollbar, container | kept from upstream. |
Fallbacks (mandatory in glass.css):
@supports not (backdrop-filter: blur(1px)) { .glass, .glass-subtle, .glass-strong { background: var(--card); } }
@media (prefers-reduced-transparency: reduce) { … same … }
@media (prefers-reduced-motion: reduce) { .glass-interactive, .press { transition: none; transform: none; } }
@media (forced-colors: active) { .glass, … { background: Canvas; box-shadow: none; border: 1px solid CanvasText; } }4. Component mapping (packages/ui/components)
| Component | Material | Radius | Notes |
|---|---|---|---|
Button | primary = solid accent, pill; secondary = glass glass-interactive; outline = glass-subtle + stroke; ghost; destructive; link; new glass (explicit) | rounded-full | Spinner start-aligned (me-1.5). Icon gap uses gap-1.5, not margins. |
Card | glass | rounded-2xl | Header/Content/Footer paddings 6/6/6; CardTitle text-base font-semibold. |
Input, Textarea, Select trigger, InputOTP slots | glass-inset | rounded-xl | Focus: ring-2 ring-ring/50 border-ring. |
Dialog, AlertDialog, Sheet | glass-strong | rounded-3xl (dialog), sheet: inner-edge radius rounded-s-3xl/rounded-e-3xl by side | Backdrop: bg-black/20 backdrop-blur-sm (light) / bg-black/50 (dark). Enter/exit: scale 0.96→1 + fade, --duration-base, spring. |
DropdownMenu, Popover, Select content, Tooltip | glass-strong | rounded-2xl (tooltip rounded-lg) | Item hover: glass-tinted with rounded-lg inset by 4px. |
Tabs | segmented control: list glass-inset rounded-full p-1; active trigger glass rounded-full shadow sliding | pill | Indicator animates with spring. |
Switch | iOS toggle: track h-7 w-12 rounded-full, thumb size-6 with shadow; checked = accent | pill | |
Badge | glass-subtle pills; primary variant glass-tinted; status = success/info/warning/error | pill | Status colour comes from the semantic tokens, never the Tailwind palette. |
Checkbox | glass-inset box size-5 rounded-md; checked = solid accent + CheckIcon | rounded-md | Hit area is the wrapping Label/card; keyboard and data-[checked] styling live in the primitive. |
EmptyState | glass-subtle rounded-2xl panel: tinted icon disc, title, description, optional action | rounded-2xl | Carries no copy of its own — every string is passed in by the app. |
CopyButton | button variant of the call site; idle icon (default CopyIcon) swaps to CheckIcon text-success | pill | Labels are props; the reset timer is cleared on unmount and the copied state only appears when the clipboard write resolves. |
Table | wrapper glass rounded-2xl overflow-clip; row hover bg-foreground/[.035] | Header sticky with glass-subtle. | |
Toast | glass-strong rounded-2xl | Position top-center on mobile, top-end on desktop. | |
Skeleton | shimmer gradient over --glass-fill-subtle | ||
Avatar, Progress, Accordion, Alert, Spinner, Label, Form | keep behavior; adopt tokens and logical props | ||
Logo | Product mark + wordmark (see §6) |
Composites that need app data or translations stay in the apps. apps/saas/modules/shared/components owns PageHeader, Pagination, DataTableShell, QrPanel, and StatsTile; packages/ui never imports app aliases, @repo/i18n, or provider SDKs.
5. App shells
- Wallpaper on
body(saas,marketing). Content sits on glass; never place glass directly on a flat white body. - SaaS sidebar:
glasspanel,rounded-3xl, insetm-3, fixedinset-inline-start. Collapsed width 80px, expanded 280px. Active itemglass-tinted. Mobile: bottom floatingglass-navwith 4 primary destinations + menu. - SaaS top area: no separate header;
PageHeaderrenders title + optional actions in aglass-subtlestrip only when the page scrolls (sticky). - Marketing nav: centered floating pill
glass-navthat appears after 10px scroll; at top it is transparent with no border. - Auth pages: centered
glasscard,max-w-md, logo above, on wallpaper. - Docs (fumadocs): map
--fd-*variables to our tokens; sidebarglass-subtle; keep fumadocs layout.
6. Product mark
Wordmark: the product name (config.appName) in --font-sans 600, passed to the component as label. It is dir="auto" and carries no tracking utility, because the product name is Arabic and Arabic is never letter-spaced (a Latin name can be tracked through the className prop). Mark: rounded-square (radius 28%) accent gradient (accent-500 → accent-700) with a white daily loop — a 300° ring with the next step as a solid dot in the notch. The mark is an inline SVG in packages/ui/components/logo.tsx; no raster assets. Replace both with your own identity.
7. Accessibility checklist (per component PR)
- Contrast measured on light + dark wallpaper worst region.
-
focus-visiblering on every interactive glass element. - Reduced transparency / reduced motion / forced colors verified.
- RTL screenshot: sidebar, sheet, tabs indicator, chevrons, toast position.
- Touch targets ≥ 44×44 on mobile nav.
8. Implementation notes
Written while implementing sections 2–4 in tooling/tailwind and packages/ui. No
contract name was renamed; everything below is an implementation detail or a
documented deviation. The two stylesheets were compiled with Tailwind 4.3.3 to
confirm every token and utility above is emitted.
Tokens (theme.css)
--glass-noiseholds only the data-URI; its opacity is a second token,--glass-noise-opacity(0.035light,0.05dark), so the same image can be reused at different strengths.--radius-4xlis kept from upstream even though section 2.2 stops at3xl, so existingrounded-4xlcall sites keep working.- The Arabic letter-spacing reset is scoped to
[dir="rtl"] :where([class*="tracking-"]):not([dir="ltr"]):not([dir="ltr"] *)instead of a blanket[dir=rtl] { letter-spacing: 0 }, so Latin islands such as the wordmark (dir="ltr") keep their tracking. containerandno-scrollbarstay in each app'sglobals.css, their upstream location, and are not duplicated inglass.css.
Utilities (glass.css)
- The materials write Tailwind's
--tw-inset-shadow/--tw-shadowslots and rebuild the fullbox-shadowchain, soshadow-*andring-*compose with the glass stroke instead of replacing it. Tailwind registers those custom properties withinherits: false, so nothing leaks into children. - Tailwind emits its core
transition-*utilities after custom utilities.presstherefore also transitions colour, border, shadow and opacity and must be used instead oftransition-*, never alongside it. The same applies toglass-interactive. glass-interactive's "+8% fill" hover step iscolor-mix(in oklab, var(--glass-fill), var(--foreground) 8%).
Components (packages/ui)
packages/ui/lib/direction.tsaddsuseDirection()for the few places where a logical CSS property cannot express the value (a transform whose sign flips).Card: paddings stay on the 6 scale but keep the upstream top-padding collapse (headerp-6 pb-4, content/footerp-6 pt-0) so header and content do not add up to 48px of gap.Badgehas novariantprop to extend, so the accent status (info, the default) carriesglass-tintedand the other statuses areglass-subtlepills whose colour lives in the text.Alertkeeps a coloured fill per variant by combiningglass-subtle glass-tintedwith a per-element[--glass-tint:var(--success|--warning|--destructive)].Table:glass rounded-2xlsits on an outer wrapper with an inneroverflow-x-autoscroller. The stickyglass-subtleheader is applied to<th>, not<thead>— pseudo-elements on a table-row-group are wrapped in anonymous rows.TableFooteruses a flatbg-foreground/[0.04]for the same reason.Toast: the stack hover-bridge moved from::afterto a real child span, becauseglass-strongalready owns::before(specular) and::after(noise); the root isoverflow-visibleso the bridge is not clipped. Defaultpositionis nowtop-right, whose viewport classes are centered on mobile andsm:end-4on desktop.Switch: the thumb animatesinset-inline-start, not a transform, so it mirrors in RTL with no JavaScript.Tabs: the sliding indicator is a Base UITabs.Indicatorpositioned with--active-tab-left/--active-tab-width. Those are measured pixel offsets, so the physicalleftis correct in both directions, and Base UI keeps the elementhiddenuntil they exist — no pre-hydration flash.Sheet:sideacceptsstart | end | top | bottom(defaultend) and still acceptsleft | right, which never mirror. The slide offset is an inline--sheet-x/--sheet-yderived fromuseDirection(); a caller'sstylecallback is preserved.Dialog,AlertDialogandSheetanimate with Base UIdata-starting-style/data-ending-styletransitions instead of the upstreamtailwindcss-animatekeyframes. Menus, popovers, tooltips and the select popup keep the upstream animate classes; only material and radius changed there. Centered dialogs keep the physicalleft-1/2 -translate-x-1/2(true centering, not a start/end offset) and gainedmax-h-[calc(100dvh-2rem)] overflow-y-auto.AlertDialogActionnow renders theprimarybutton variant so the confirming action is not the least prominent control in the dialog.Avatarisrounded-fullwith aglass-subtlefallback;Progressis ah-2glass-insettrack.Logorenders one wordmark, taken fromlabel(the apps passconfig.appName): the prop API is{ withLabel?, label?, className? }and there is no per-locale wordmark yet. The mark's gradient uses the fixed SVG idapp-mark.SpinnerandLabelalready used tokens and logical properties and are unchanged.chart.tsxis outside the section 4 mapping and was left as is.
agentjob.io Design System
Official design system contract for agentjob.io — fast-hiring AI platform (tokens, bilingual rules, typography, components, and layout).
ADR Template
Template for Architecture Decision Records — context, decision, consequences with revenue impact, rejected alternatives, and references.