Drop a JSON file in termic's themes folder and it shows up in the theme picker as a first-class theme: chrome and terminal, coupled, exactly like the built-ins. There is no theme editor UI. The files are the interface, which makes themes trivially shareable and stowable from dotfiles.
~/.config/termic/themes/
($XDG_CONFIG_HOME/termic/themes/ if you set that variable. Same path on
every platform; release and dev builds share it.)
Themes are hand-authored, shareable config, so they live under .config
like other terminal tools (wezterm, starship, kitty), not in the app-owned
Application Support data dir, which makes them trivially stowable from
dotfiles.
When the folder is empty, termic seeds it with a README.md and an
example.json.sample (a complete theme, ready to cp to my-theme.json).
Neither ends in .json, so the picker ignores both. Delete them once you
have your own theme; they only come back if the folder is emptied. The
sources live in src-tauri/assets/themes/.
One JSON file per theme. The picker's "Open themes folder" row opens this directory (and creates it if needed). Themes are read at startup and re-read every time the picker opens, so the edit loop is: save the file, hover the picker, done. No restart, no watcher.
Two ways, same as the built-ins:
- Hover the theme icon (sun/moon) in the title bar. Custom themes list below the built-ins, with an "Open themes folder" row at the bottom.
- ⌘K, then "Change theme…". Arrow keys live-preview each theme; Enter commits, Escape rolls back to what you had.
The keyboard theme-cycle shortcut only cycles System / Light / Dark, so a custom theme sticks until you explicitly pick something else.
The theme's id is its file name (a file named rose-pine-moon.json becomes
custom:rose-pine-moon internally), so a file named claude.json can
never shadow the built-in Claude theme.
{
"name": "Rosé Pine Moon",
"colorScheme": "dark",
"ui": {
"bg": "#232136", "bg-1": "#2a273f", "bg-2": "#393552", "bg-3": "#44415a",
"fg": "#e0def4", "fg-dim": "#908caa", "fg-faint": "#6e6a86",
"border": "#56526e", "border-soft": "#393552",
"hover": "rgba(224,222,244,0.05)", "sel": "rgba(196,167,231,0.22)",
"accent": "#c4a7e7", "accent-soft": "rgba(196,167,231,0.22)", "accent-deep": "#6f5b9e",
"accent-fg": "#232136",
"ok": "#9ccfd8", "ok-fg": "#232136", "warn": "#f6c177", "err": "#eb6f92"
},
"terminal": {
"background": "#232136", "foreground": "#e0def4",
"cursor": "#c4a7e7", "selectionBackground": "#44415a",
"black": "#393552", "red": "#eb6f92", "green": "#3e8fb0", "yellow": "#f6c177",
"blue": "#9ccfd8", "magenta": "#c4a7e7", "cyan": "#ea9a97", "white": "#e0def4",
"brightBlack": "#6e6a86", "brightRed": "#eb6f92", "brightGreen": "#3e8fb0",
"brightYellow": "#f6c177", "brightBlue": "#9ccfd8", "brightMagenta": "#c4a7e7",
"brightCyan": "#ea9a97", "brightWhite": "#e0def4"
}
}| Field | Required | Values | Notes |
|---|---|---|---|
name |
yes | string | Picker label. Falls back to the file name if empty. |
colorScheme |
yes | "dark" or "light" |
Drives form controls, the editor's auto syntax theme, and the COLORFGBG env var agents use to pick their TUI theme. Anything other than "light" is treated as "dark". |
ui |
no | object | Chrome colors, see below. |
terminal |
no | object | xterm palette, see below. |
Unknown top-level fields are ignored (forward compatibility).
Every key is optional and maps to the --color-<key> CSS variable. Missing
keys fall back to the built-in palette matching your colorScheme (Dark+
for dark themes, Light for light ones), so a minimal theme can override
just bg and accent and still look coherent.
| Key | What it paints |
|---|---|
bg |
main content area |
bg-1 |
chrome: sidebar, title bar, tabs |
bg-2 |
nested cards, badges |
bg-3 |
hover and active surfaces |
fg |
primary text |
fg-dim |
muted text |
fg-faint |
faint icons, secondary text |
border |
standard borders |
border-soft |
hairline borders |
hover |
hover overlay (usually a low-alpha rgba) |
sel |
selection tint |
accent |
brand accent (buttons, active states) |
accent-soft |
accent-tinted backgrounds (usually low-alpha rgba) |
accent-deep |
accent for filled buttons |
accent-fg |
ink on a solid accent fill (count badges, filled CTAs). Defaults to white; set a dark ink if your accent is light |
ok |
success green |
ok-fg |
ink on a solid ok fill (toggle knobs). Defaults to white |
warn |
warning yellow |
err |
error red |
The --color-cli-* agent tint variables are not themeable in v1; they keep
their defaults.
Standard xterm.js ITheme keys, all optional: background, foreground,
cursor, cursorAccent, selectionBackground, selectionForeground,
selectionInactiveBackground, the ANSI 8 (black through white), and
the bright ANSI 8 (brightBlack through brightWhite).
Missing keys fall back to the built-in palette matching your colorScheme
(Dark+ for dark themes, Light for light ones), so a partial block still
yields a complete, readable ANSI 16.
Hex (#rgb, #rgba, #rrggbb, #rrggbbaa) and rgb() / rgba() /
hsl() / hsla() notation. Named CSS colors are not accepted.
Nothing about a theme file can break the app:
- A file that fails to parse is skipped (the rest still list). Check the
dev console / stderr for a
[themes] skipping ...line. - An unknown key or a malformed color value is dropped; the key falls back to its default.
- Deleting the active theme's file falls back to the Claude theme on the next picker open or launch.
- Two files with the same
nameboth list (their ids differ). The picker shows whatever the files say. - The active custom theme's payload is cached in localStorage so the first paint after launch uses it directly. The folder is re-read right after startup and reconciled (edits re-apply, deletions fall back).