Writing a theme
A theme is plain data. There’s no JSX, no React state, no build step beyond TypeScript. You author it as a typed object (or a function that returns one) and the library renders the desktop with it.
import type { OsTheme } from "@react-ui-os/core";
export const myTheme: OsTheme = { id: "my-theme", name: "My theme", palette: { … }, shape: { … }, motion: { … }, blur: { … }, wallpaper: { … }, chrome: { … }, customizable: { … },};See Themes overview for what each category contains.
Factory functions
Section titled “Factory functions”Themes that need assets the consumer provides (a wallpaper image, a brand accent) should expose a factory function, not a static object. This keeps the theme package free of bundled binary assets and lets every consumer point at their own paths.
export interface MyThemeOptions { wallpaperSrc?: string; accent?: string;}
export function createMyTheme(opts: MyThemeOptions = {}): OsTheme { return { id: "my-theme", name: "My theme", palette: { /* … */, accent: opts.accent ?? "#5cb6b9" }, wallpaper: { src: opts.wallpaperSrc, parallax: true, vignette: true }, // … };}Consumers then call it at boot:
const theme = createMyTheme({ wallpaperSrc: "/wallpaper.jpg" });<Desktop apps={apps} theme={theme} />;Asset ownership
Section titled “Asset ownership”The library does not bundle wallpaper images, fonts, or icons into themes. Reasons:
- Tree-shaking. A theme that bundles a 200 KB JPEG forces every consumer to ship it.
- Customization. Half the value of a theme is letting consumers swap the wallpaper for their own.
- Licensing. Assets and code have different license stories.
Consumers serve their own assets from public/ (or equivalent), pass the URL to the factory, and the theme writes it into wallpaper.src.
Customizable schema
Section titled “Customizable schema”Themes opt in to runtime tweaks by declaring customizable:
customizable: { "palette.accent": { kind: "color-from-palette", section: "Appearance", label: "Accent", options: ["#5cb6b9", "#7c66f5", "#a855f7", "#ec4899"], }, "chrome.dockPosition": { kind: "select", section: "Layout", label: "Dock position", options: [ { value: "bottom", label: "Bottom" }, { value: "left", label: "Left" }, { value: "hidden", label: "Hidden" }, ], },}The full reference is at Customizable schema.
Picking sensible defaults
Section titled “Picking sensible defaults”Some rules of thumb from theme-macos and theme-ubuntu:
- Surface colors with alpha.
rgba(20, 22, 32, 0.62)letsblur.surfaceactually frost something. Solid colors defeat the effect. - Open animations at 180-220 ms. Faster reads as snappy; slower reads as sluggish. Ubuntu runs 200 ms, matching the GNOME Shell window-open timing.
- Genie minimize at 280-320 ms. Long enough to read as physical, short enough not to drag.
- One blur value for surface, one for Spotlight. Spotlight typically wants a deeper blur because it floats over everything.
palette.borderatrgba(255,255,255,0.08-0.10). Hairlines should feel intentional but not bright.
Sizing the chrome to a platform
Section titled “Sizing the chrome to a platform”A clone usually wants its platform’s real chrome metrics, not the defaults. Optional chrome tokens size the dock and top bar; a "bar" dock derives its thickness from the tile size, so a larger-icon dock becomes a wider bar.
chrome: { dockStyle: "bar", dockTileSize: 40, // the dock tile / taskbar button (macOS 56, Windows 40, Ubuntu 56) dockIconScale: 0.6, // the icon as a fraction of the tile menuBarHeight: 30, // the top bar height (macOS 24, the GNOME bar ~30)},With these, Windows lands 24px icons in a 48px taskbar and Ubuntu ~46px icons in a ~64px dock. All three are optional; omit them to keep the shared defaults. Compact viewports shrink the tiles in step.
Fonts and per-platform icons
Section titled “Fonts and per-platform icons”theme.font sets the platform’s UI font stack, applied at the desktop root so the chrome inherits it. The consumer still loads any non-system webfont (a @font-face rule or a font-host link), the same asset-ownership split as wallpapers.
font: '"Ubuntu", "Ubuntu Sans", system-ui, sans-serif',chrome.iconStyle ("fluent", "macos", "gnome", or your own key) declares which icon variant the theme wants. An app that ships an icons[style] entry renders its platform-native icon under that theme; otherwise it falls back to its default icon. This is how one app carries a Fluent icon on Windows and an SF-style glyph on macOS without a component ever branching on the theme.
chrome: { iconStyle: "fluent";}Publishing
Section titled “Publishing”Themes are tiny, single-file packages. The Ubuntu theme is ~175 lines. A minimal package.json:
{ "name": "@my-org/theme-acme", "version": "1.0.0", "type": "module", "main": "./dist/index.js", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } }, "dependencies": { "@react-ui-os/core": "^1.0.0" }}Build with tsup (ESM + CJS + types) and you’re done.
See also
Section titled “See also”OsTheme: the full token reference.- Customizable schema: what runtime tweaks look like.
useTheme: how components consume tokens.