ryOS ryOS / Docs
GitHub Launch

Theme architecture (implementation)

This document describes how ryOS applies the four OS themes today and how to extend them without adding brittle, duplicated selectors.

Flow

  1. useThemeStore persists current (system7 | macosx | xp | win98) plus a per-theme darkModeByTheme map (localStorage key ryos:theme:dark), and calls applyRootThemeAttributes so <html> has:
    • data-os-theme — exact theme id (drives token blocks and Tailwind os-theme-* variants).
    • data-os-platformmac or windows (drives shared Luna + Classic rules and Tailwind os-mac / os-windows variants).
    • data-os-mac-chromeaqua or system7 when data-os-platform="mac"; attribute removed on Windows themes (see getOsMacChrome()).
    • data-os-color-schemedark only when the active theme advertises metadata.supportsDarkMode = true AND the user has dark mode enabled for that theme. Removed (and color-scheme reset to light) otherwise. Drives the :root[data-os-theme="…"][data-os-color-scheme="dark"] { … } token blocks in themes.css and Tailwind os-dark / os-mac-aqua-dark / os-theme-<id>-dark variants.
  1. src/styles/themes.css orchestrates the theme CSS layers. The concrete CSS lives under src/styles/themes/:
    • tokens.css — defaults, per-theme --os-* token blocks, selection utility, z-index scale.
    • platform.css — shared platform rules keyed by data-os-platform.
    • containment.css — reduced-motion rules and third-party/app containment such as Webamp resets.
    • aqua.css — macOS Aqua structural chrome and component rules.
    • windows.css — Windows structural chrome notes/landing area; most native widget chrome still comes from public/css/xp-custom.css and public/css/98-custom.css.
    • dark-aqua.css — Aqua dark token and structural overrides.
    • aqua-glass.css — Aqua Glass material overrides, imported after dark Aqua so glass can override both light and dark Aqua.
  2. Legacy Windows (public/css/xp-custom.css, 98-custom.css) is loaded only for xp / win98 by ensureLegacyCss in the theme store.
  3. TypeScript definitions in src/themes/*.ts hold canonical metadata (ThemeMetadata); use getThemeMetadata, isWindowsTheme, isMacTheme, isThemeWinXp, isThemeWin98, getOsPlatform, getOsMacChrome, or the useThemeFlags() hook instead of ad hoc current === "xp" chains in new code.
  4. OS_NATIVE_CHROME_SKIP_CLASS / OS_SHELL_TEXT_SCALE_CLASS (src/lib/themeChrome.ts) — use OS_NATIVE_CHROME_SKIP_CLASS on an ancestor so macOS Aqua global :where(…) typography chains skip your subtree (alongside legacy *-force-font classes). Use OS_SHELL_TEXT_SCALE_CLASS on shell wrappers outside WindowFrame so copy picks up --os-typography-window.

Visual values are intentionally not stored in src/themes/*.ts; CSS tokens in src/styles/themes/tokens.css are the visual source of truth. The TS theme registry is for metadata, layout behavior, dark-mode support, and wallpaper defaults.

CSS layers (order of precedence / mental model)

LayerSelector patternPurpose
Tokens:root[data-os-theme="…"] { --os-*: … }Per-theme palette, fonts, radii, shadows. Single source of truth for Tailwind bg-os-*, border-os-*, etc.
Family:root with data-os-platform "mac" or "windows"Rules that are identical for all themes on that platform (for example Windows menu font forcing, Webamp range resets, Spotlight button resets).
Structureclass + theme, e.g. .window-material-brushedmetal, .aqua-buttonChrome that does not fit a single variable.
Containment#webamp, app-specific escape hatchesIsolate third-party or special UIs from global form styling.

When you need the same declaration for XP and Win98, add it under data-os-platform="windows" (or extend tokens if it is truly per-theme). Avoid new paired :root[data-os-theme="xp"], :root[data-os-theme="win98"] lists.

Menu typography tokens

Radix menubar / dropdown items use:

  • --os-font-ui — always from the active theme token block.
  • --os-menu-item-font-size11px on Windows family, 13px on macosx, inherit elsewhere.
  • --os-menu-subtrigger-font-size11px on Windows, 12px on macosx.

Defined across src/styles/themes/tokens.css (defaults and macOS tokens) and src/styles/themes/platform.css (Windows platform sizing).

macOS Aqua window vs. shell copy

Typography tokens (--os-typography-*) live on the theme root. window-body (always on WindowFrame content) uses --os-typography-window. Surfaces outside frames—desktop shell, portaled dialog innards—should not rely on removed global div/p rules. Add class os-shell-text-scale on a shell wrapper so children inherit --os-typography-window (see themes.css).

Tailwind variants

tailwind.config.js registers:
  • os-mac::root[data-os-platform="mac"] &
  • os-windows::root[data-os-platform="windows"] &
  • os-mac-aqua::root[data-os-mac-chrome="aqua"] & (Mac OS X only)
  • os-mac-system7::root[data-os-mac-chrome="system7"] &
  • os-theme-system7:, os-theme-macosx:, os-theme-xp:, os-theme-win98: → exact theme
  • os-dark::root[data-os-color-scheme="dark"] & (only attached when the active theme supports dark mode)
  • os-mac-aqua-dark::root[data-os-mac-chrome="aqua"][data-os-color-scheme="dark"] &
  • os-theme-<id>-dark::root[data-os-theme="<id>"][data-os-color-scheme="dark"] &

Use these for small, theme-scoped utilities instead of growing cn(...) theme branches.

CSS branch vs React branch

Prefer CSS branches for static visual differences:

  • Tokenized colors, borders, text colors, separators, radii, and shadows should use --os-* tokens or Tailwind utilities like bg-os-window-bg, border-os-window, and text-os-text-primary.
  • Small theme-scoped states should use root-attribute variants such as os-windows:, os-mac-aqua:, os-mac-system7:, and os-mac-aqua-dark:.
  • Shared surfaces should use helpers such as osCardClassName, osDrawerSurfaceClassName, osToolbarSurfaceClassName, osAppSidebarSurfaceClassName, osSeparatorBorderClassName, osSubtleIconButtonClassName, and windowsBevelClassName.

Use React branches only when the DOM structure, event behavior, asset choice, layout math, or app behavior changes. Examples: choosing Dock vs taskbar layout, using a native Windows tab <menu>, picking a theme-specific icon path, or changing window-control placement.

Dark mode

  • Today only macosx carries dark tokens (metadata.supportsDarkMode: true); the other themes opt out and the toggle is hidden in Control Panels → Appearance.
  • Dark CSS lives in src/styles/themes/dark-aqua.css so removing the attribute is a complete revert. Token overrides plus chrome that bakes in light hues (menubar gloss, brushed metal, drawer, traffic lights, Spotlight pinstripe) are all in that layer — see comments there before adding new dark surfaces.
  • The user preference is per-theme (darkModeByTheme) so flipping back to a dark-supported theme remembers the previous choice. useThemeFlags() exposes supportsDarkMode and isDarkMode for component branches that genuinely need to differ from the token-driven path.
  • Settings sync ships the dark-mode map as part of the existing theme section payload ({ theme, darkMode } for new clients; old clients still get a plain string).

HTML defaults

index.html loads /theme-bootstrap-config.js before the inline first-paint script. That static config mirrors the TS registry fields needed before React starts: default theme, platform bucket, Mac chrome bucket, and dark-mode support. Keep it in sync with src/themes/bootstrapConfig.ts; tests/test-theme-bootstrap-config.test.ts verifies parity.

Adding a theme

  1. Add a metadata-only theme object in src/themes/<id>.ts and register it in src/themes/index.ts.
  1. Add light CSS tokens in src/styles/themes/tokens.css; add dark tokens only if metadata.supportsDarkMode is true.
  2. Add structural CSS in the appropriate layer (aqua.css, windows.css, or a new family file) and prefer platform/chrome attributes over paired exact-theme selectors.
  3. Update public/theme-bootstrap-config.js so first paint knows the theme id, platform, Mac chrome, and dark support.
  4. Add or update themed assets/icons only where the theme needs different files.
  5. Run bun test tests/test-theme-bootstrap-config.test.ts and a preview-build visual comparison across supported themes.

Migration notes

  • Prefer useThemeFlags() (isWindowsTheme, isWinXp, isWin98, isMacOSTheme, isAquaMenuChrome, macChrome, isMacAquaChrome, …) over duplicating OR-of-theme-id checks in components. Most dialogs, shared surfaces (LinkPreview, HtmlPreview, AppDrawer, fullscreen controls), AirDrop, applet store feeds, and app hooks now read theme through this hook.
  • Outside React (sync jobs, tool handlers): use useThemeStore.getState().current or isWindowsTheme(id) / isThemeWinXp. For useThemeStore.subscribe, keep the subscription on the store (e.g. cloud sync marking settings dirty on theme change).
  • isAquaMenuChrome means “Mac OS X Aqua menus” only; System 7 uses classic metrics shared with Windows for some menu padding patterns.
  • User-facing overview remains in Theme System; this file is for implementers.