Skip to content

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. 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. 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. 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=#6e92de
palette = 4=#7993cb
palette = 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.