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
useThemeStorepersistscurrent(system7|macosx|xp|win98) plus a per-themedarkModeByThememap (localStorage keyryos:theme:dark), and callsapplyRootThemeAttributesso<html>has:data-os-theme— exact theme id (drives token blocks and Tailwindos-theme-*variants).data-os-platform—macorwindows(drives shared Luna + Classic rules and Tailwindos-mac/os-windowsvariants).data-os-mac-chrome—aquaorsystem7whendata-os-platform="mac"; attribute removed on Windows themes (seegetOsMacChrome()).data-os-color-scheme—darkonly when the active theme advertisesmetadata.supportsDarkMode = trueAND the user has dark mode enabled for that theme. Removed (andcolor-schemereset tolight) otherwise. Drives the:root[data-os-theme="…"][data-os-color-scheme="dark"] { … }token blocks inthemes.cssand Tailwindos-dark/os-mac-aqua-dark/os-theme-<id>-darkvariants.
src/styles/themes.cssorchestrates the theme CSS layers. The concrete CSS lives undersrc/styles/themes/:tokens.css— defaults, per-theme--os-*token blocks, selection utility, z-index scale.platform.css— shared platform rules keyed bydata-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 frompublic/css/xp-custom.cssandpublic/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.
- Legacy Windows (
public/css/xp-custom.css,98-custom.css) is loaded only forxp/win98byensureLegacyCssin the theme store. - TypeScript definitions in
src/themes/*.tshold canonical metadata (ThemeMetadata); usegetThemeMetadata,isWindowsTheme,isMacTheme,isThemeWinXp,isThemeWin98,getOsPlatform,getOsMacChrome, or theuseThemeFlags()hook instead of ad hoccurrent === "xp"chains in new code. OS_NATIVE_CHROME_SKIP_CLASS/OS_SHELL_TEXT_SCALE_CLASS(src/lib/themeChrome.ts) — useOS_NATIVE_CHROME_SKIP_CLASSon an ancestor so macOS Aqua global:where(…)typography chains skip your subtree (alongside legacy*-force-fontclasses). UseOS_SHELL_TEXT_SCALE_CLASSon shell wrappers outsideWindowFrameso 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)
| Layer | Selector pattern | Purpose |
|---|---|---|
| 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). |
| Structure | class + theme, e.g. .window-material-brushedmetal, .aqua-button | Chrome that does not fit a single variable. |
| Containment | #webamp, app-specific escape hatches | Isolate 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-size—11pxon Windows family,13pxonmacosx,inheritelsewhere.--os-menu-subtrigger-font-size—11pxon Windows,12pxonmacosx.
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 themeos-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 likebg-os-window-bg,border-os-window, andtext-os-text-primary.
- Small theme-scoped states should use root-attribute variants such as
os-windows:,os-mac-aqua:,os-mac-system7:, andos-mac-aqua-dark:. - Shared surfaces should use helpers such as
osCardClassName,osDrawerSurfaceClassName,osToolbarSurfaceClassName,osAppSidebarSurfaceClassName,osSeparatorBorderClassName,osSubtleIconButtonClassName, andwindowsBevelClassName.
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
macosxcarries 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.cssso 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()exposessupportsDarkModeandisDarkModefor component branches that genuinely need to differ from the token-driven path. - Settings sync ships the dark-mode map as part of the existing
themesection 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
- Add a metadata-only theme object in
src/themes/<id>.tsand register it insrc/themes/index.ts.
- Add light CSS tokens in
src/styles/themes/tokens.css; add dark tokens only ifmetadata.supportsDarkModeis true. - 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. - Update
public/theme-bootstrap-config.jsso first paint knows the theme id, platform, Mac chrome, and dark support. - Add or update themed assets/icons only where the theme needs different files.
- Run
bun test tests/test-theme-bootstrap-config.test.tsand 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().currentorisWindowsTheme(id)/isThemeWinXp. ForuseThemeStore.subscribe, keep the subscription on the store (e.g. cloud sync marking settings dirty on theme change). isAquaMenuChromemeans “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.