How to write your own theme
This guide shows how to add a colour theme of your own. It assumes you know where your configuration directory is and can edit a TOML file.
1. Find where themes go
turbo-js -list-themes
The last line tells you the directory — ~/.config/turbo-js/themes on Linux, ~/Library/Application Support/turbo-js/themes on macOS. Create it:
mkdir -p ~/.config/turbo-js/themes
2. Start from an existing theme
The quickest start is to inherit from one that already works and override only what you want:
# ~/.config/turbo-js/themes/mine.toml
name = "Mine"
description = "Turbo Classic, but the comments are readable."
inherits = "turbo-classic"
[colors]
"syntax.comment" = { fg = "#8a8a8a", italic = true }
"syntax.string" = { fg = "#87d7af" }
Everything you do not set is taken from turbo-classic.
Inherit from a theme whose ground is the same as yours. The colours you leave out were chosen against the background of the theme you inherit from, so a dark theme built on turbo-classic will show, here and there, a colour picked for Borland navy. If you are writing a dark theme, inherit from turbo-dark, cappuccino, catppuccin-frappe, cobalt, darcula or monochrome-dark; if a light one, from borland-light, catppuccin-latte, intellij-light or monochrome-light. That is also why the eleven themes that ship in the binary each state their palette in full rather than inheriting most of it — a test holds them to it, because a shipped theme is one the project is answerable for.
3. Use it
turbo-js -theme mine main.js
Or from inside the editor: Options ▸ Theme…, which lists every theme it can find.
4. Iterate
Edit the file, then restart the editor. There is no live reload.
If the theme fails to load, Turbo JS falls back to the default rather than refusing to start. To see why it failed:
turbo-js -list-themes
A broken theme is listed with the parse error beside it — an unknown colour name is an error, not a silent fallback, so a typo is pointed at rather than quietly repainting half the screen.
5. Check it stays readable
turbo-core, the library the themes ship in, holds every one of them to five measured rules, and they are worth applying to your own. Its test suite runs them.
| Rule | Why |
|---|---|
| The cursor is at least 64 apart from the line it sits on, in its strongest channel | A terminal draws its cursor over the cell; one that blends in cannot be found |
| The cursor is never a plain reversal of that line | A terminal that draws its cursor by inverting the cell would invert it back into invisibility |
| The current line is at least 16 from the page | turbo-dark once used ten, which is no highlight at all |
| Text you have to read is at least 64 from its background | Furniture — the desktop, a shadow, a scrollbar trough, a disabled entry — is exempt: it exists to recede |
| Comments read at 4.5:1 or better against their background, by WCAG relative luminance | A comment is prose, read word by word. turbo-classic drew them in #808080 on its navy: 128 channel values apart, so the rule above waved it through, and 4.05:1 to read, which is below the W3C's floor for body text. |
The contrast rule is applied to every shipped theme but the two Catppuccin ones, and the exemption is written where it is made: their colours are somebody else's published palette, faithfully copied, and Catppuccin puts comments at 2.87:1 in Frappé and 2.83:1 in Latte. A theme called Catppuccin that is not those exact values is a different theme wearing a borrowed name, so the fix — if anyone wants one — is upstream.
It is applied to comments and to nothing else. syntax.punctuation is quieter still in several themes and stays that way: punctuation is recognised by shape, not read.
A sixth rule catches the mistake no measurement finds: two syntax classes a reader meets side by side must not be drawn identically. turbo-classic once painted syntax.link the same lime as syntax.string, so a Markdown link and an inline code span were the same thing on screen — every colour readable, every key set, and the two simply equal. Both monochromes pass this rule with no hue at all, by using bold, italic and underline instead.
Variants
Override a shipped theme rather than adding one. Name your file after it — turbo-classic.toml — and yours wins. The embedded one is not replaced, so deleting your file brings it back.
Start from scratch. Leave inherits out. Set at least default; every key you do not set falls back along the dots to it, so a theme with one line is still a usable theme.
Colour only the syntax. One key does it:
[colors]
syntax = { fg = "silver" }
syntax.keyword, syntax.string and the rest all fall back to it.
Keep the terminal's own colours. Use default as a colour value:
[colors]
"editor.text" = { fg = "default", bg = "default" }
Test it in a checkout without installing it. Point the editor at any directory:
TURBO_JS_THEME_DIR=./my-themes turbo-js -theme mine main.js
The cursor is hard to see. editor.cursor does two things: its background is sent to the terminal as the cursor's own colour, and it also paints the cell underneath as a fallback for terminals that ignore that. Set it to something loud:
[colors]
"editor.cursor" = { fg = "#000000", bg = "#ff8700" }
Two things make a cursor colour a bad one, and the test suite rejects both: a plain reversal of the line — which terminals that draw their cursor by inverting the cell turn back into invisibility — and anything less than 64 channel values away from the line it sits on.
The cursor's line is hard to find. That is editor.currentline, and it is a different key. It has to differ from editor.text by at least 16 channel values to count as a highlight at all.
JavaScript types and builtins look alike. In turbo-classic a capitalised name such as Point is syntax.type, and console is syntax.builtin; both are aqua on navy, and only the builtin is bold. If that is too close for you, give one of them a colour of its own:
[colors]
"syntax.type" = { fg = "yellow" }
"syntax.builtin" = { fg = "aqua", bold = true }
Markdown and HTML look plain. Five keys belong to the markup languages and — but for syntax.attribute, which a @decorator takes — have no equivalent in JavaScript, so a theme written before they existed does not set them:
[colors]
"syntax.heading" = { fg = "white", bold = true }
"syntax.tag" = { fg = "aqua" }
"syntax.attribute" = { fg = "yellow" }
"syntax.emphasis" = { fg = "fuchsia", bold = true }
"syntax.link" = { fg = "aqua", underline = true }
Give syntax.link a different colour from syntax.string: a link and an inline code span sit side by side in most prose, and sharing a colour makes them one blur. The shipped turbo-classic had exactly that fault until it was looked at on a real terminal.
The project tree looks flat. It has four keys of its own, and none of them falls back to list:
[colors]
"tree.text" = { fg = "silver", bg = "navy" }
"tree.directory" = { fg = "white", bg = "navy", bold = true }
"tree.selected" = { fg = "black", bg = "aqua" }
"tree.unfocused" = { fg = "black", bg = "gray" }
Give tree.text the same background as window.body, so the tree looks like part of its window, and make tree.selected clearly different from it — the test suite holds every shipped theme to at least 64 channel values between the two, because a highlight the same colour as the page is no highlight.
Terminal windows look wrong. They have two keys of their own, and neither falls back to editor:
[colors]
"terminal.text" = { fg = "silver", bg = "black" }
"terminal.cursor" = { fg = "black", bg = "aqua" }
terminal.text is what a shell's output gets when it names no colour of its own — set it to something close to a real terminal rather than to your editor background, or less and htop will look out of place. A program that does name its colours keeps them either way.
See also
- Every key you may set, and every colour name: theme file reference
- Which class each piece of JavaScript gets: Languages coloured
- Why the format is TOML with two kinds of inheritance: Design decisions
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
|