Repository navigation
Theme anatomy
Every theme lives in its own folder under themes/<id>/. The id must be lowercase kebab-case and must match the folder name exactly — it's also used as the [data-theme='<id>'] CSS selector and the CSS namespace for animations.
themes/<id>/
├── manifest.json
├── theme.css
├── thumbnail.png (or .jpg)
└── assets/ (optional)
| Field | Required | Notes |
|---|---|---|
| id | yes | Lowercase kebab-case, matches the folder name |
| name | yes | Display name in the Theme Store. Keep it short; a trademark-safe/altered name is fine if inspired by a brand, film, or game |
| author | yes | Your GitHub handle |
| version | yes | Plain X.Y.Z, no pre-release suffix. Must be bumped on every change on a already-published theme |
| description | yes | The store's search anchor — see Making-a-Theme#naming--description |
| mode | yes | "dark" or "light" — must match the color-scheme |
| minAppVersion | optional | set to 1.51.0 or later, Required if the theme uses assets/, or otherwise depends on a newer app feature |
| changelog | optional | Object keyed by version, e.g. { "1.0.1": ["Fixed X", "Tweaked Y"] } — shown in the store's What's new |
Example (from template/manifest.json):
{
"id": "template",
"name": "Template",
"author": "your-github-handle",
"version": "1.0.0",
"description": "Starting point for a community theme. Copy this folder, rename, recolour.",
"mode": "dark",
"tags": ["template"],
"minAppVersion": "1.51.0", /* only add when using assets */
"changelog": {
"1.0.0": ["Initial release."]
}
}
Free-form CSS scoped under [data-theme='<id>']. The recommended starting point is recolouring the semantic tokens documented in Design-Tokens — that alone recolours the whole app — but you're not limited to them: any selectors, structure, @media, and @keyframes are fair game.
Themes can also react to live app state via attributes on that same root element:
[data-theme='<id>'][data-playing='true'] { /* ... */ }
Available state attributes: data-playing, data-fullscreen, data-sidebar-collapsed, data-lyrics-open.
color-scheme must be declared directly on the theme root (not as a custom property) and must match manifest.mode — it drives OS-level form/scrollbar theming and the store's dark/light filter.
A 16:9 screenshot of Psysonic with your theme applied, at least 1280×720 (aspect ratio 1.5–1.85, source file ≤ 6 MB). You don't need to resize or convert it — CI optimises it into a thumbnail.webp automatically on merge.
No screenshot yet? Generate a quick placeholder:
node scripts/make-thumbnail.mjs themes/<your-id>/thumbnail.png "#15171e" 1280 720
For images or fonts you'd rather ship as files than inline as data: URIs. See Local-Assets for the rules.
Not part of a theme folder — this is the single auto-generated index the app actually reads over the CDN. It's regenerated from every theme's manifest.json on merge to main. Never edit it by hand. See Registry & Versioning.