# Themes Drop a theme in here and it appears in the picker on the next page load. No install step, no rebuild, no restart — a theme is a block of CSS custom properties, and none of that needs compiling. **This directory is untracked**, exactly like `plugins/`. Only this file is in the repository. Your themes are yours; the platform's own live in `src/workspaces/theme/css/themes/`. --- ## The shape ``` themes/ my-theme/ theme.css required — the values theme.json required — the name a person reads assets/ optional — a wallpaper, a logo ``` The directory name **is** the theme's id. It is not repeated in the manifest, because an id that can disagree with where its files sit eventually will. ### `theme.json` ```json { "name": "My Theme", "family": "My Theme", "dark": true } ``` | | | |---|---| | `name` | required — what the picker shows | | `family` | optional — groups variants together, like `Duck Light` and `Duck Dark`. Defaults to `name` | | `dark` | optional — adds the `.dark` class. Set it for a dark theme; several hundred `dark:` utilities still resolve against it | ### `theme.css` Must target its own directory name, or nothing will happen and it will be baffling: ```css [data-theme='my-theme'] { --background: #1e1e2e; --foreground: #cdd6f4; /* …the rest */ } ``` The server checks this on discovery and reports it rather than letting you wonder. --- ## What to put in it **Twenty tokens are required.** They are the standard shadcn set, which is what every published palette already ships — Tokyo Night, Catppuccin, Gruvbox, Nord all come in this vocabulary, so porting one is mostly copy-paste. ``` background foreground · card card-foreground · popover popover-foreground primary primary-foreground · secondary secondary-foreground muted muted-foreground · accent accent-foreground destructive destructive-foreground · border input ring · radius ``` **Everything else is derived from those.** Panels, the header, the dock, glass, grid lines, scrollbars — all computed from `card`, `border`, `background` and `foreground`. A palette theme never mentions them. **Four more are worth setting**, because the standard set has no notion of them and the defaults are generic: `--success`, `--warning`, `--info`, `--busy`, each with a `-foreground`. `busy` means *running*, not *warning* — it is what a live job wears. **Anything else you want to change, you can.** Declaring a variable in your theme beats the derived default, because both land on `` and yours is more specific. That is how a theme gets a *texture* rather than just a palette: ```css [data-theme='my-theme'] { --panel-bg: rgba(255, 255, 255, 0.08); /* glass instead of a flat card */ --panel-blur: 20px; --panel-radius: 0; /* square panels */ --page-bg-src: '/themes/my-theme/assets/wallpaper.webp'; --logo-src: '/themes/my-theme/assets/logo.svg'; } ``` The full list of what you can override is in `src/workspaces/theme/README.md`, and `src/workspaces/theme/css/themes/duck-dark.css` is a worked example of a textured theme. **Assets are quoted paths, not `url(...)`.** The bundler resolves `url()` at build time and your files are served at runtime, so a `url()` token would break. The app wraps the path for you. --- ## Writing one Start from `src/workspaces/theme/css/themes/plain.css` rather than from a Duck theme. Plain is white, black and grey throughout, so every value you change is one you decided about, with nothing inherited from someone else's brand. Then: reload the page, open the picker or press `ctrl+shift+space`, and use `j`/`k` to browse. `l` applies the focused theme to the app behind the list, so you can watch your own theme against real screens. Editing `theme.css` and reloading is the whole loop. The stylesheet is cached for a minute. --- ## Checking your work Render the app under **Plain** first if something looks wrong under yours. Anything still coloured under Plain is a component painting itself rather than reading a token — that is a platform bug, not yours, and worth reporting. `bun run check:theme` counts hardcoded colours in the platform. It does not check themes, but it is why your theme has a chance of working: the number can only go down.