Theme format¶
A .peltontheme file is a zip container. The only fixed name inside is manifest.json; everything else is declared by the manifest, so you can organize files however you like. Extra files such as a README or LICENSE ride along and are ignored by the engine.
Nordish.peltontheme
├── manifest.json # (1)!
├── tokens/
│ ├── colors.json
│ └── type.json
├── css/
│ └── polish.css
├── assets/
│ ├── fonts/inter.woff2
│ └── preview.png
└── icons/
└── palette.svg
- Required, and the only fixed name in the container. Everything else is declared in here, so you can organize the remaining files however you like.
manifest.json¶
{
"manifestVersion": 1,
"id": "nordish",
"name": "Nordish",
"author": "Arne K.",
"version": "1.0.0",
"description": "An arctic, north-bluish theme.",
"license": "MIT",
"base": "dark",
"pelton": { "min": "1.0.8" },
"tokens": ["tokens/colors.json", "tokens/type.json"],
"css": ["css/polish.css"],
"preview": "assets/preview.png",
"icons": { "palette": "icons/palette.svg" }
}
| Field | Required | Meaning |
|---|---|---|
manifestVersion |
yes | Container format version, currently 1. A file with a newer version than the app understands is refused, since an unknown format cannot degrade gracefully. |
id |
no | Stable lowercase slug (a-z, 0-9, dashes). Names the install folder and keys update detection: importing a known id with a higher version offers an update instead of a duplicate. Defaults to a slug of name. |
name |
yes | Display name, free-form. |
author, version, description, homepage, license |
no | Shown in the gallery and import preview. |
base |
yes | light or dark. Which built-in token set fills everything the theme does not override, and what the OS-level color scheme reports. |
pelton |
no | { "min": "...", "max": "..." }, both optional. The app version range the theme was made for. Outside the range is a warning badge, never a block. |
tokens |
no | List of JSON files merged in order (later wins), or one inline object. |
css |
no | List of CSS files, concatenated in this order into one stylesheet. |
preview |
no | Screenshot shown in the theme gallery and import preview. |
icons |
no | Map of icon name to SVG file, see below. |
Tokens¶
Token files are flat maps of token name to CSS value:
{
"surface-base": "#2e3440",
"accent": "#88c0d0",
"radius-control": "7px",
"font-ui": "\"Inter\", system-ui, sans-serif"
}
Names are the app's token names without the -- prefix (a leading -- is also accepted). Only allowlisted tokens are themeable; an unknown name fails the import loudly rather than being ignored, so typos surface immediately.
The themeable surface:
| Group | Tokens |
|---|---|
| Surfaces | surface-base, surface-raised, surface-overlay, surface-sunken, surface-hover, selection-bg, selection-bg-strong |
| Text | text-primary, text-secondary, text-tertiary, text-inverse, link |
| Borders | border-subtle, border-default, border-strong, hairline |
| Accent | accent, accent-fg |
| Semantic | success, success-bg, warning, warning-bg, danger, danger-bg |
| Radii | radius-control, radius-card, radius-none |
| Fonts | font-ui, font-mono |
| Type | fz-meta, fz-label, fz-list, fz-body, fz-heading, fz-title, fw-regular, fw-medium, fw-semibold, fw-bold |
| Elevation | shadow-overlay |
Spacing and density tokens are deliberately not themeable: density is a user setting, and a theme fighting it would break layouts.
Values go through a safety check: no semicolons, braces, @ or url(). Everything colors, font stacks and shadows need still works, including color-mix().
CSS¶
CSS files are concatenated in manifest order into a single stylesheet that loads after the theme's token overrides, so "base file, then override file" layering behaves the way you would expect.
Rules for references:
url("assets/...")pointing at a file inside the container is the intended way to use fonts and images. The file is inlined at apply time, so it never touches the network.- Remote references (
url("https://..."), any@import) are flagged at import. The import dialog lists each one and asks whether to keep or strip them; strip is the default. Whatever you choose is baked into the installed files.
Size caps: 20 MB per container, 1 MB of CSS total, 5 MB per inlined asset.
Icons¶
The icons map replaces interface icons by name. Names are the Tabler icon names the app uses, in lowercase kebab, without the Icon prefix: IconPalette becomes palette.
- SVGs should draw with
currentColorso they follow the theme's text colors. - SVGs are sanitized at import and again on every load: script elements, event handler attributes,
href,foreignObjectand external references are rejected with an error naming the file. - Unknown names are accepted and ignored, so a theme can ship icons for future app versions.
- Coverage grows over time: the app routes icons through a themeable wrapper and converts call sites progressively.
Versioning¶
Two independent fields, two behaviors:
manifestVersionis the format version, for the engine. Newer than the app understands means the import is refused with a clear message.peltonis the app version range, for humans. Outside the range shows a warning at import and a badge in the gallery ("made for Pelton 1.0.8 or newer"), but the theme still installs and applies. This also covers the reverse case: when the app updates past a theme'smax, the badge appears without breaking anything.