Codelet logoCodelet

Themes

The Theme interface, the two built-ins, rangi's own themes, and how to write one

A theme is a plain object: a name, a dark flag, a handful of chrome colors and a table of token colors. Nothing here reads a CSS variable or a class name — every color a workbench or an editor paints comes from the Theme object it was given.

#The Theme interface

interface TokenStyle {
  color?: string;
  fontWeight?: string;
  textDecoration?: string;
}

interface Theme {
  name: string;
  dark: boolean;
  bg: string;
  panel: string;
  line: string;
  fg: string;
  muted: string;
  accent: string;
  hover: string;
  selection: string;
  tokens: Record<string, TokenStyle>;
}
FieldWhat it colors
nameWhat a theme picker lists, and what a host persists to remember a choice
darkWhether this is a dark theme — sets the page's color scheme
bgThe editor's background
panelSidebar, panel, activity bar and status bar backgrounds
lineBorders between panes, and the gutter separators
fgThe default text color
mutedSecondary text — descriptions, line numbers, placeholders
accentFocus rings, active tabs, links, the current search match
hoverThe background a row or a button takes under the pointer
selectionSelected text and selected rows — an opaque color, not a translucent one
tokensSyntax colors, keyed by token type

selection has to be opaque. A translucent fill would blend into the active line's background in a live editor but sit flat in server-rendered markup, so the two would never quite match.

#lightTheme and darkTheme

Both built-ins are exported from codelet and from codelet/workbench:

import { lightTheme, darkTheme } from "codelet";
// or
import { lightTheme, darkTheme } from "codelet/workbench";

Use whichever entry you already import from. codelet/workbench also re-exports fontFamily and fontSize — the shell's own typography, which follows the viewport rather than the theme, so it isn't part of the Theme shape.

#The workbench

Pass theme to set the active theme, and themes for the set the reader may switch between:

import { Workbench, lightTheme, darkTheme } from "codelet/workbench";

const workbench = new Workbench({
  parent: document.getElementById("app")!,
  fs,
  theme: darkTheme,
  themes: [lightTheme, darkTheme],
  onTheme: (theme) => localStorage.setItem("theme", theme.name),
});

The theme control lives under the gear at the foot of the activity bar, and what themes holds decides what it says:

themesGear menu
Omitted, or oneNo theme row at all
TwoTheme: <the other one>, which swaps them
Three or moreColor Theme…, the same picker as "Preferences: Color Theme" in the palette

onTheme fires with the new theme every time the reader switches. workbench.setTheme(theme) changes it back from your own code — a no-op if theme is already the active one.

#The standalone Editor

The same theme option and setTheme method work on Editor, without any of the activity bar machinery — there's no chrome around a bare editor to put a picker in, so switching is entirely up to your own UI:

import { Editor, lightTheme, darkTheme } from "codelet";

const editor = new Editor({ parent, doc, theme: darkTheme });

let dark = true;
toggleButton.onclick = () => {
  dark = !dark;
  editor.setTheme(dark ? darkTheme : lightTheme);
};
Read more in Guide > Workbench#the Standalone Editor.

#codelet/themes

lightTheme and darkTheme are the only two built-ins in codelet and codelet/workbench. codelet/themes is a separate entry with rangi's own theme table converted to this shape, so a workbench that only wants the two built-ins never pays for the rest of it:

import { rangiThemes, fromRangi } from "codelet/themes";
import { lightTheme, darkTheme } from "codelet/workbench";

const themes = [lightTheme, darkTheme, ...rangiThemes];

fromRangi(theme, name?) converts a single rangi theme. rangiThemes is every theme rangi ships that has plain colors rather than CSS variables, already converted and sorted by name.

#Writing your own theme

A Theme is just an object, so building one is filling in the fields:

import type { Theme } from "codelet";

const sunset: Theme = {
  name: "Sunset",
  dark: true,
  bg: "#1b1024",
  panel: "#150d1d",
  line: "#3a2a4a",
  fg: "#f1e6ff",
  muted: "#a68fc2",
  accent: "#ff8a5c",
  hover: "#ffffff14",
  selection: "#4a2f63",
  tokens: {
    kwd: { color: "#ff6b9d" },
    str: { color: "#ffd479" },
    num: { color: "#ffd479" },
    cmnt: { color: "#7d6a94" },
    func: { color: "#7ec8ff" },
    type: { color: "#7ec8ff" },
    class: { color: "#7ec8ff" },
    bool: { color: "#ff8a5c" },
    var: { color: "#f1e6ff" },
    oper: { color: "#ff8a5c" },
    esc: { color: "#ffd479" },
    err: { color: "#ff5c5c" },
    section: { color: "#ff6b9d", fontWeight: "bold" },
  },
};

tokens is keyed by rangi's own token names: bool, class, cmnt, deleted, err, esc, func, insert, kwd, num, oper, section, str, type and var. A TokenStyle needs only colorfontWeight and textDecoration are there for the rare token that wants bold or underline on top of it, the way the built-in themes use fontWeight on section.

A token name you leave out is never wrong — it simply isn't colored, and that kind of syntax renders in fg like plain text. Most themes cover the dozen or so that carry real weight (kwd, str, num, cmnt, func, type) and leave the rest to the foreground.

Warning

Pass a stable theme object. The shell and the editor both cache the styles they build for a theme on that object's identity — build a new theme object on every render and each one adds a fresh style module to the page instead of reusing what's already there.