specification
What each colour is for
SP Night is one file of colours and a layer of names on top of it. A theme file for any app is that second layer, written out in the app's own format. This page is the reference for what the colours mean and which rules a port has to hold. If you want to build one, start at contribute instead — this page is the lookup table it points back to.
The two files this page describes are published as data — /palette.json and /roles.json — and kept, with the tool that checks them, in sp-night/sp-night. Anything that reads them produces a theme that agrees with every port.
How it fits together
Colour flows in one direction, and each step only knows about the one before it. That is the whole architecture:
- 1 · palette 23 named colours per flavour, in a single JSON file. This is the only place a hex value is ever written down.
- 2 · roles
A second file that says what each colour is for —
ansi.blue,syntax.keyword,ui.cursor. A role names a palette key; it never contains a colour itself. - 3 · ports One repository per app, holding a finished theme file for every flavour. A port decides which role each of its config keys gets; it never decides a colour.
A worked example
Say you want Ghostty's ANSI blue. The port does not choose a blue — it says that this config key is the ANSI blue:
palette = 4 → ansi.blue and that resolves in two hops:
ansi.blue marginal #6e92de #7993cb #6e92de
and lands in the shipped theme file, for the flavour you have selected, as:
palette = 4=#6e92depalette = 4=#7993cbpalette = 4=#6e92de
The point of the middle hop is that nobody has to remember which blue is the ANSI blue.
Change ansi.blue once and every terminal, editor and status bar follows.
palette = 4 → marginal reaching past the roles, straight into the palette palette = 4 → ansi.blue asking for the role, letting it resolve The colours
Every flavour has the same 23 keys. Only the values change, so a port mapped once works for all of them.
Surfaces
Five stacked planes, from the deepest recess to the wiring that divides it.
| colour | what it is for |
|---|---|
| vao | the deepest recess — the free span under the MASP, a well of shadow |
| laje | the default surface — a concrete slab, the ground plane of everything |
| concreto | panels, cards and bars — exposed concrete |
| vidro | selection and elevation — glass reflecting the street |
| fiacao | borders and dividers — overhead wiring cutting across the sky |
Text
Four levels of foreground, each holding AA contrast on the surfaces it is used over. fg_vivo is the bright end: bold default text, and the terminal’s bright white.
| colour | what it is for |
|---|---|
| fg_vivo | lit text — terminal bright white, bold text |
| fg | primary text |
| fg_dim | secondary text and comments |
| fg_muted | disabled text, ornaments |
Accents
Eight signature colours. Hue and chroma carry the identity — never brightness.
| colour | what it is for |
|---|---|
| brasa | red — the MASP, brake lights on Avenida Paulista |
| sodio | orange — the sodium street lamp that turns the whole city this colour |
| taxi | yellow — traffic lights, taxis, a worn-out crosswalk |
| ibira | green — Ibirapuera park, a green light |
| estaiada | turquoise — the Ponte Estaiada lit above the Pinheiros river |
| sereno | cyan — the small hours’ damp air, a cold neon sign |
| marginal | blue — the Marginal expressway, the metro, the highway sign |
| temporal | purple — the sky right before the storm breaks |
Bright pairs
Six accents have a brighter twin, used for the terminal's bright colour slots: same hue, same saturation, 0.06 lighter in OKLCH. They are written into the palette rather than calculated on the fly, so they go through the same contrast audit as everything else.
Two accents have no terminal slot at all, on purpose — sodio and
estaiada. The sixteen slots already have owners, and these two are
identity colours rather than text colours.
The text ramp gets the same lift, for the same reason. fg_vivo is
fg 0.06 lighter, and it is what a target reaches for when it
separates default text from bold default text — Alacritty's
bright_foreground, kitty's bold font colour.
ui.fg_bright names it and
ansi.bright_white resolves to it, so bold text in a terminal is
genuinely brighter instead of the same hex twice.
ansi.white stays fg_dim: the ANSI table
keeps the two ends of the ramp, and fg remains the terminal's plain
foreground. Here 0.06 is also the ceiling — fg in
noite already sits at OKLCH lightness 0.88, and asking for more
starts draining its chroma.
Two kinds of accent
The most common way to get this theme wrong is to use the orange everywhere. There are two accent roles and they are not interchangeable:
ui.accent → sodio
Identity. Terminals, status bars, the cursor. This is the colour people recognise the theme by, and it should stay rare enough to mean something.
ui.accent_alt → marginal
System widgets. Selection and focus in GTK, Qt and KDE apps. A file manager should still look like a file manager — its selection highlight is blue, not orange.
Assignment rules
Syntax colours are assigned by meaning, and the same meaning keeps the same colour in every language. A keyword is blue in Go, in Rust and in CSS.
| role | colour |
|---|---|
| keyword, conditional, repeat | marginal |
| function, method, constructor, operator | sereno |
| type, namespace | temporal |
| macro, parameter, escape | estaiada |
| constant, number, boolean | sodio |
| builtin, attribute | taxi |
| string, character | ibira |
| variable, property, field | fg |
| punctuation, comment | fg_dim |
| tag | brasa |
| deprecated | fg_muted |
Variables stay fg on purpose. If everything is coloured then nothing
is — the hierarchy comes from what is left plain.
A diff has exactly one pattern: added is green, modified is yellow, removed is red. Everything structural — hunk headers, renames, untracked files — stays neutral. Never purple, blue or orange inside a diff.
Contrast policy
Every foreground is measured against every surface it can legally appear on: 74 pairs per flavour. A gate that fails stops the build; a warn is reported and looked at.
| against | applies to | minimum | severity | why |
|---|---|---|---|---|
| vao | text and accents | 4.5:1 | gate | the deepest background — floats, popups, the terminal |
| laje | text and accents | 4.5:1 | gate | the default surface; everything readable sits here |
| concreto | text and accents | 4.5:1 | gate | panels, cards and bars |
| vidro | text and accents | 3.0:1 | gate | selection and elevation — short-lived, never body text |
| fg_dim | on laje | 4.5:1 | gate | comments and secondary text: comfortable where you read, legible elsewhere |
| fg_dim | on vao, concreto, vidro | 3.0:1 | gate | comments and secondary text: comfortable where you read, legible elsewhere |
| fg_muted | on vao, laje, concreto, vidro | 3.0:1 | warn | ornaments and disabled text: reported, not blocking |
| fiacao | on vao, laje | 1.5:1 | warn | borders are structure, not text |
fg_dim is the delicate one, and it is why the table has two rows for
it: a comment you cannot read is a comment you delete, so it holds full AA on
laje where you actually read, and only has to stay legible elsewhere.
Accent separation
Passing contrast against the background is not enough. Two accents can both be perfectly
readable and still be impossible to tell apart from each other. That happened here:
estaiada measured 8.68:1 and sereno 8.63:1,
both fine, both the same colour to the eye.
So there is a second rule, measured in OKLCH:
ΔE < 0.10 ⟹ ΔL ≥ 0.04
In words: if two accents are close in hue and saturation, they must be far enough apart in lightness. Colour-vision deficiency is simulated too — protanopia, deuteranopia and tritanopia — but as a diagnostic rather than a hard rule: the constraints already fight each other, and no mature theme solves this one completely.
Glossary
- flavour
- One complete variant of the theme — noite, garoa or jaragua. Same 23 names, different values.
- palette key
- The name of a colour, like sodio or laje. Keys are in Portuguese because each one is named after something in São Paulo.
- role
- What a colour is *for*, like syntax.keyword or ansi.blue. A role points at a palette key, so you can repaint every app by changing one line.
- target
- An app the theme is written for — Ghostty, eza. Each target is one port repository.
- contrast ratio
- How far apart two colours are in brightness, from 1:1 (identical) to 21:1 (black on white). 4.5:1 is the WCAG AA threshold for normal text.
- AA
- The middle accessibility level of WCAG, the web accessibility standard. Text at 4.5:1 or better passes.
- OKLCH
- A way of describing a colour as lightness, chroma (how saturated) and hue (which colour). Unlike hex or HSL, equal steps in it look equal to the eye.
- ΔE and ΔL
- The distance between two colours (ΔE) and the part of that distance that is pure lightness (ΔL). Used to keep two accents from collapsing into each other.