Themes & colorschemes

Themes & colorschemes: :colorscheme to switch (with a live-preview picker), the 21 builtin themes, :customize to tweak options, and registering your own palette.

On this page

A theme is the editor's colour identity — syntax highlighting, gutter signs, the modeline, diff tints, popups, the canvas itself. In lattice a theme is a palette (a set of named colour roles like text, green, red, overlay) plus an element registry that maps every styled surface to a role. Swapping the palette recolours the whole editor at once; there is no per-surface theme wiring to maintain.

One model, two renderers. The TUI inherits the terminal's own background/foreground for the canvas (it can't repaint the terminal), so swapping to a light theme there recolours syntax and chrome but leaves the terminal's canvas. The GPUI renderer owns its canvas and recolours fully, light or dark.


Quick reference

You want…Do this
Switch to a known theme:colorscheme tokyonight-dark
Browse themes with live preview:colorscheme (no argument)
Vim short for the above:colo gruvbox-light
Tweak a config group in a buffer:customize ui
Pick a group/mode to customize:customize (no argument)
Toggle the icon palette:set ui.nerd_fonts / :set noui.nerd_fonts

Switching themes

:colorscheme <name> switches the active theme by name. The whole editor recolours immediately:

:colorscheme dracula-dark
:colo nord-light

:colo is the vim short. An unknown name echoes an error and leaves the current theme untouched — nothing changes until a real name lands. To browse instead of typing a name, run :colorscheme with no argument — the live-preview picker (next section).

The live-preview picker

:colorscheme with no argument opens a buffer-backed picker over every registered theme. As you move the selection, the whole editor recolours to that candidate — you see the real theme, not a swatch (the cross-editor convention: VSCode, Zed, and telescope all preview this way).

  • <CR> (accept) commits the highlighted theme.
  • <Esc> (cancel) restores the theme that was active when you opened the picker — byte-for-byte, no residue.

The picker is the way to shop for a theme; the explicit :colorscheme <name> form is the way to set one you already know.


The builtin catalog

Lattice ships 21 builtin themes. Catppuccin contributes three flavours; nine cross-editor families each ship a dark and a light variant.

Catppuccin (the default family)

NameTone
catppuccin-mochadark (default)
catppuccin-macchiatodark
catppuccin-lattelight

Cross-editor families (each -dark / -light)

FamilyDarkLight
Gruvboxgruvbox-darkgruvbox-light
Tokyo Nighttokyonight-darktokyonight-light
Draculadracula-darkdracula-light
Nordnord-darknord-light
Solarizedsolarized-darksolarized-light
Oneone-darkone-light
Everforesteverforest-darkeverforest-light
Rosé Pinerosepine-darkrosepine-light
Monokaimonokai-darkmonokai-light

The default on a fresh start is catppuccin-mocha. Markdown headings get distinct per-level colour and (on the GPUI renderer) per-level size — h1 down to h6 each pick up their own style, with the leading # markers left at base size.


Domain elements, not just syntax

Most styled things resolve through the syntax.* elements — keyword, string, comment, and so on. Some do not, and deliberately: a commit SHA is not a "link", the checked-out branch is not a "keyword", and a keybinding in a help page is not a "type". Those get their own elements so a theme can retune them without dragging a source-code colour along with them.

ElementWhere it shows
magit.shacommit SHAs in magit log, blame and rebase views
magit.branch.currentthe checked-out branch in a branch list
magit.ref.decorationthe (HEAD -> main, …) list after a log SHA
magit.rebase.verbpick / reword / squash / … in a rebase todo
magit.authorthe author column in a blame
help.keya key you press, in a help page (gr, <C-c>g)
help.commanda command you type (:magit-status)
help.actionan action id (action:magit-refresh)
help.literalany other inline literal — a path, a flag, a filename
transient.titlethe transient menu's title
transient.groupa group heading inside it (▸ Arguments)
transient.keythe key that fires a row — the most-scanned column
transient.key.inactivea row ruled out by the multi-key prefix typed so far
transient.descriptiona row's description column
transient.valuea flag's [x] state, or a variable's current value
transient.borderthe menu's border
picker.titlethe source name in a picker prompt (buffers)
picker.promptthe > marker where typing begins
picker.rootthe project root a rooted picker is scoped to
picker.countthe (3/40) match count and searching… status

The transient.* set exists because a menu row is three columns that mean different things — the key you press, what it does, and whether it is on. Until these were named, each renderer invented its own answer: the TUI hard-coded ANSI colours, so :colorscheme never reached the menu at all, and GPUI borrowed its popup border and cursor colours for five roles, which left keys and flags the same colour and descriptions painted in the border tone. One named palette now drives both.

The four picker.* elements cover the whole prompt line:

buffers  ~/src/lattice  > lat            (3/40)
───────  ─────────────  ─               ───────
 title        root     prompt            count

Until they were named this line was the last wholly un-themeable surface in the editor — the terminal wrote Cyan and DarkGray straight into its spans and the GPUI window borrowed its cursor and border colours, so :colorscheme could not reach the picker prompt at all. Borrowing is also what left picker.root — the ~/src/lattice that answers which checkout answered — sharing a tone with the count beside it, so the one piece of context the prompt carries read as chrome.

picker.prompt starts out the same colour as picker.title, because that is the relationship the hardcoded version had. It is a separate element so you can break that, not so you must. What you type stays at the default foreground on purpose: an accent there would make your own text compete with the chrome around it.

The four help.* elements are what make a help page scannable: keys are bold so the thing you are hunting for stands out, commands take the same colour as the : line you will type them on, actions are dimmer because an action id is machinery you meet rarely, and plain literals stay quiet so they don't compete with the three that carry meaning. Every one is retunable — if you want keys in green, that is one element override.


Customizing options

Themes set colours; many other surfaces (icons, separators, gutter behaviour) are typed options. :customize opens a type-aware editing view that writes back to your TOML config:

:customize ui          # every customizable option in the `ui` group
:customize lsp-mode     # what a mode contributes
:customize             # a navigation picker: groups + modes
  • :customize with no argument lists every option group and every mode that contributes options — pick one to drill in.
  • :customize <group> or :customize <mode> opens the focused view: each option shows its name, type, current value, default, and doc.
  • Following an option's edit link prefills the : command line with set NAME=VALUE, where you finish the value and accept.

For the concepts behind options (:set, layered resolution, where a value comes from) see Options. :colorscheme itself is not persisted across restarts yet — set it from your init to make it stick.


Registering your own theme

The theme catalog is open. A custom palette registered into it gains everything the builtins have: :colorscheme <your-name>, completion, and a slot in the live-preview picker.

Registration is the register_theme seam, reached from your Rust-WASM init (and, later, from a plugin via WIT). You supply a name and a palette that fills the same role-key vocabulary the builtins reference (text, overlay, subtext, green, red, orange, purple, cyan, the base/surface canvas family, the ansi.* chrome, …). Because every styled element resolves through those roles, a palette alone recolours the whole editor — you don't wire individual elements. Registering does not change the active theme; it just adds it to the catalog, ready for :colorscheme.

Re-registering an existing name replaces that theme's palette, so you can override a builtin in place by registering under its name.


  • ui.nerd_fonts — toggles the icon palette used by the file tree, oil, pickers, and gutter. on uses Nerd Fonts v3 glyphs (requires a patched terminal/GUI font like JetBrains Mono Nerd Font); off (the default) falls back to a BMP-block palette (◆ ≡ ◇ ■ ♪ ▶ ·) that renders in any monospace font. Flip it with :set ui.nerd_fonts / :set noui.nerd_fonts. Pick off if you see ? boxes.

See also

  • Options:set, :customize, the typed-option model.
  • Modeline — the status row whose colours follow the active theme.
  • Display & layout — gutter, whitespace, soft-wrap.