agentjob.ioHandbook
Design

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

  1. Material, not decoration. Glass is a layered material: wallpaper → blur → fill → stroke → specular → content. Nothing is "just transparent".
  2. Hierarchy through depth. Three levels only: subtle (in-page panels), regular (cards, sidebars), strong (overlays: dialogs, menus, sheets, toasts). Never stack strong on strong.
  3. 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.
  4. Restraint. One accent hue. Neutral grays carry a faint cool cast. No gradients on text. No glow.
  5. Physics-like motion. One spring easing, three durations. Press feedback scales, never darkens alone.
  6. Fallback parity. With backdrop-filter unavailable, reduced transparency, or forced colors, every surface degrades to a solid, opaque equivalent with identical layout.
  7. 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

TokenLightDarkNote
--gray-50 … --gray-950OKLCH, hue 250, chroma 0.004–0.012same scaleApple-like cool neutral. Replaces olive.
--accent-50 … --accent-950OKLCH hue 38.2 (brand orange)sameSingle 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

TokenLightDark
--glass-blur20px24px
--glass-blur-strong40px48px
--glass-saturate180%160%
--glass-fill-subtleoklch(100% 0 0 / 0.38)oklch(22% 0.01 250 / 0.42)
--glass-filloklch(100% 0 0 / 0.58)oklch(20% 0.01 250 / 0.60)
--glass-fill-strongoklch(100% 0 0 / 0.78)oklch(18% 0.01 250 / 0.80)
--glass-strokeoklch(100% 0 0 / 0.65)oklch(100% 0 0 / 0.10)
--glass-stroke-outeroklch(0% 0 0 / 0.06)oklch(0% 0 0 / 0.45)
--glass-specularlinear-gradient(180deg, oklch(100% 0 0 / 0.55), transparent 40%)… / 0.10 …
--glass-shadow0 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-strong0 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-tintvar(--primary)var(--primary)
--glass-noisedata-URI SVG feTurbulence, opacity 0.035opacity 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)

UtilityEffect
glassregular 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-subtlesame, with --glass-fill-subtle, no outer shadow.
glass-strongoverlays: --glass-blur-strong, --glass-fill-strong, --glass-shadow-strong.
glass-tintedmixes 12% --glass-tint into the fill (color-mix). Use for the active nav item, primary badges.
glass-interactivehover: fill +8% and translateY(-1px); active: scale(var(--press-scale)); focus-visible: 2px ring --ring offset 2px. All with --ease-spring/--duration-fast.
glass-navfloating bar preset: glass-strong + rounded-full + shadow tuned for top bars.
glass-insetrecessed surface for inputs: --glass-fill-subtle, inset 0 1px 2px oklch(0 0 0/.06), stroke --input.
wallpaperfull-bleed backdrop: layered radial gradients (accent 8%, gray, white) + noise, background-attachment: fixed on desktop only. Applied to body in saas and marketing.
pressactive:scale-[var(--press-scale)] with spring easing.
ease-spring, duration-fast/base/slowmotion helpers.
no-scrollbar, containerkept 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)

ComponentMaterialRadiusNotes
Buttonprimary = solid accent, pill; secondary = glass glass-interactive; outline = glass-subtle + stroke; ghost; destructive; link; new glass (explicit)rounded-fullSpinner start-aligned (me-1.5). Icon gap uses gap-1.5, not margins.
Cardglassrounded-2xlHeader/Content/Footer paddings 6/6/6; CardTitle text-base font-semibold.
Input, Textarea, Select trigger, InputOTP slotsglass-insetrounded-xlFocus: ring-2 ring-ring/50 border-ring.
Dialog, AlertDialog, Sheetglass-strongrounded-3xl (dialog), sheet: inner-edge radius rounded-s-3xl/rounded-e-3xl by sideBackdrop: 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, Tooltipglass-strongrounded-2xl (tooltip rounded-lg)Item hover: glass-tinted with rounded-lg inset by 4px.
Tabssegmented control: list glass-inset rounded-full p-1; active trigger glass rounded-full shadow slidingpillIndicator animates with spring.
SwitchiOS toggle: track h-7 w-12 rounded-full, thumb size-6 with shadow; checked = accentpill
Badgeglass-subtle pills; primary variant glass-tinted; status = success/info/warning/errorpillStatus colour comes from the semantic tokens, never the Tailwind palette.
Checkboxglass-inset box size-5 rounded-md; checked = solid accent + CheckIconrounded-mdHit area is the wrapping Label/card; keyboard and data-[checked] styling live in the primitive.
EmptyStateglass-subtle rounded-2xl panel: tinted icon disc, title, description, optional actionrounded-2xlCarries no copy of its own — every string is passed in by the app.
CopyButtonbutton variant of the call site; idle icon (default CopyIcon) swaps to CheckIcon text-successpillLabels are props; the reset timer is cleared on unmount and the copied state only appears when the clipboard write resolves.
Tablewrapper glass rounded-2xl overflow-clip; row hover bg-foreground/[.035]Header sticky with glass-subtle.
Toastglass-strong rounded-2xlPosition top-center on mobile, top-end on desktop.
Skeletonshimmer gradient over --glass-fill-subtle
Avatar, Progress, Accordion, Alert, Spinner, Label, Formkeep behavior; adopt tokens and logical props
LogoProduct 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: glass panel, rounded-3xl, inset m-3, fixed inset-inline-start. Collapsed width 80px, expanded 280px. Active item glass-tinted. Mobile: bottom floating glass-nav with 4 primary destinations + menu.
  • SaaS top area: no separate header; PageHeader renders title + optional actions in a glass-subtle strip only when the page scrolls (sticky).
  • Marketing nav: centered floating pill glass-nav that appears after 10px scroll; at top it is transparent with no border.
  • Auth pages: centered glass card, max-w-md, logo above, on wallpaper.
  • Docs (fumadocs): map --fd-* variables to our tokens; sidebar glass-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-visible ring 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-noise holds only the data-URI; its opacity is a second token, --glass-noise-opacity (0.035 light, 0.05 dark), so the same image can be reused at different strengths.
  • --radius-4xl is kept from upstream even though section 2.2 stops at 3xl, so existing rounded-4xl call 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.
  • container and no-scrollbar stay in each app's globals.css, their upstream location, and are not duplicated in glass.css.

Utilities (glass.css)

  • The materials write Tailwind's --tw-inset-shadow / --tw-shadow slots and rebuild the full box-shadow chain, so shadow-* and ring-* compose with the glass stroke instead of replacing it. Tailwind registers those custom properties with inherits: false, so nothing leaks into children.
  • Tailwind emits its core transition-* utilities after custom utilities. press therefore also transitions colour, border, shadow and opacity and must be used instead of transition-*, never alongside it. The same applies to glass-interactive.
  • glass-interactive's "+8% fill" hover step is color-mix(in oklab, var(--glass-fill), var(--foreground) 8%).

Components (packages/ui)

  • packages/ui/lib/direction.ts adds useDirection() 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 (header p-6 pb-4, content/footer p-6 pt-0) so header and content do not add up to 48px of gap.
  • Badge has no variant prop to extend, so the accent status (info, the default) carries glass-tinted and the other statuses are glass-subtle pills whose colour lives in the text.
  • Alert keeps a coloured fill per variant by combining glass-subtle glass-tinted with a per-element [--glass-tint:var(--success|--warning|--destructive)].
  • Table: glass rounded-2xl sits on an outer wrapper with an inner overflow-x-auto scroller. The sticky glass-subtle header is applied to <th>, not <thead> — pseudo-elements on a table-row-group are wrapped in anonymous rows. TableFooter uses a flat bg-foreground/[0.04] for the same reason.
  • Toast: the stack hover-bridge moved from ::after to a real child span, because glass-strong already owns ::before (specular) and ::after (noise); the root is overflow-visible so the bridge is not clipped. Default position is now top-right, whose viewport classes are centered on mobile and sm:end-4 on desktop.
  • Switch: the thumb animates inset-inline-start, not a transform, so it mirrors in RTL with no JavaScript.
  • Tabs: the sliding indicator is a Base UI Tabs.Indicator positioned with --active-tab-left / --active-tab-width. Those are measured pixel offsets, so the physical left is correct in both directions, and Base UI keeps the element hidden until they exist — no pre-hydration flash.
  • Sheet: side accepts start | end | top | bottom (default end) and still accepts left | right, which never mirror. The slide offset is an inline --sheet-x / --sheet-y derived from useDirection(); a caller's style callback is preserved.
  • Dialog, AlertDialog and Sheet animate with Base UI data-starting-style / data-ending-style transitions instead of the upstream tailwindcss-animate keyframes. Menus, popovers, tooltips and the select popup keep the upstream animate classes; only material and radius changed there. Centered dialogs keep the physical left-1/2 -translate-x-1/2 (true centering, not a start/end offset) and gained max-h-[calc(100dvh-2rem)] overflow-y-auto.
  • AlertDialogAction now renders the primary button variant so the confirming action is not the least prominent control in the dialog.
  • Avatar is rounded-full with a glass-subtle fallback; Progress is a h-2 glass-inset track.
  • Logo renders one wordmark, taken from label (the apps pass config.appName): the prop API is { withLabel?, label?, className? } and there is no per-locale wordmark yet. The mark's gradient uses the fixed SVG id app-mark.
  • Spinner and Label already used tokens and logical properties and are unchanged. chart.tsx is outside the section 4 mapping and was left as is.

On this page