Repository navigation
How Color Themes Work
Nibtree has 41 color themes: the built-in Nibtree Light and Dark, 12 more light, 22 more dark and 5 high contrast themes. The user picks one for light mode and one for dark mode. For the user side, see Color Themes.
People spend hours a day in an editor, and they care a lot about its colors. Most already have a favorite theme from their editor, and a git client that looks different from their editor feels foreign. High contrast themes also matter for people with low vision. Picking one theme per mode keeps the app right when macOS switches appearance at sunset.
Every color in the app is a CSS variable from the UI library's styles/colors.css, such as --panel, --text, --editor-bg, --diff-added, --tok-keyword and --term-red. src/lib/themes/tokens.ts lists all 73 of them in COLOR_TOKENS. A theme is one value for each token.
- The built-in themes are the sets in
colors.css, generated fromui/design-tokens/colors.json::rootfor light, and the dark set twice, under@media (prefers-color-scheme: dark) :root:not([data-theme="light"])and:root[data-theme="dark"]. They need no JavaScript. -
Every other theme is one generated
<style id="gm-color-theme">element with the rulehtml:root[data-color-theme="<id>"] { ... }.html:rootoutranks:root[data-theme], and the attribute scope means the rule only applies together with its attribute.
sequenceDiagram
participant P as ColorThemePicker
participant S as settings store
participant A as apply.ts
participant C as catalog.ts (lazy)
participant H as html element
participant W as watchers (terminal, preview)
P->>S: setColorTheme(mode, themeId)
S->>S: setPreference, save to settings.json
S->>A: applyColorTheme(colorMode, themeId)
alt built-in theme
A->>H: remove the style element
else other theme
A->>C: import("./catalog")
C-->>A: themeCss(themeId)
A->>H: write the style element
end
A->>H: set data-theme, data-color-theme, data-contrast
H-->>W: MutationObserver (watch.ts)
W->>W: read the new variables again
The themes live in the UI library (ui/web/src/lib/themes/), and their colors are data in ui/design-tokens/themes/. themeIndex.ts is small and always loaded: every theme's id, name and kind from index.json (light, dark, high-contrast-light, high-contrast-dark). Settings validation (pickThemeId) and the pickers (themeGroups) use it without loading any palette.
catalog.ts is a lazy chunk with the palettes (palettes.ts reads the JSON files). Each ThemeSpec holds the theme's published colors: editor background and foreground, accent, selection, the 16 ANSI colors, syntax colors and, where the theme has them, UI colors. deriveColors (derive.ts) builds all 73 tokens from that with the helpers in color.ts (mix, composite, ensureContrast, fitTint):
- Hints, accent and status colors are pushed until they reach their contrast target on every surface they sit on.
- Optional syntax colors:
constant(true) falls back to the keyword color,operator(=) to the text. - Diff tints are faded (
fitTint) until text on them stays readable. - High contrast themes get 7:1 text targets, syntax colors pushed to 7:1 and stronger diff tints.
Values are computed on demand, never cached.
settings.applyAppearance() runs on every preference change. It works out colorMode (effectiveMode: the Appearance setting, or macOS while it is System, watched with matchMedia) and calls applyColorTheme(mode, themeId) in apply.ts:
-
appliedKeyskips the work when mode and theme did not change, because font sliders callapplyAppearancemany times a second. - A request counter lets the newest call win over one still loading the catalog.
- The
<html>attributes change only after the new variables are in place, so watchers read the new colors.data-themeis now always set, also for System, so code reads one attribute. - If the catalog fails to load, the built-in theme of that mode is applied.
Most of the app reads the variables directly, so it follows at once: panels, CodeMirror (its tok-* classes), diffs and the merge tool. Code that copies colors out of CSS listens with watchTheme (watch.ts, a MutationObserver on data-theme, data-color-theme and data-contrast):
- The terminal (
terminalInstance.ts) sets xterm's theme fromcurrentTerminalTheme(). - The Markdown preview redraws its diagrams.
mermaid.tsuses mermaid's own themes for the built-ins and the theme's variables for the others.
views/settings/ColorThemePicker.svelte is a listbox per mode with grouped options ("Light" or "Dark", then "High contrast"). It imports the catalog on mount for the swatches (themeSwatch); the swatches live in the component's state, and a cancelled flag ignores an import that finishes after unmount. pickerMove handles the arrows, Home, End and the page keys. The "In use" badge shows on the picker of settings.colorMode.
| File | What it does |
|---|---|
ui/design-tokens/themes/*.json |
Theme list and published colors, for every platform |
ui/web/src/lib/themes/themeIndex.ts |
Ids, names, kinds, defaults, modes, groups, picker keys |
ui/web/src/lib/themes/catalog.ts, palettes.ts, derive.ts
|
Palettes, deriveColors, themeCss, themeSwatch
|
ui/web/src/lib/logic/color.ts |
Parsing, mixing, WCAG contrast, Lab distance |
ui/web/src/lib/themes/colorTokens.ts |
The color tokens |
ui/design-tokens/build/themes.ts |
Every theme's colors for macOS (GMThemes.swift, GMThemeColors*.swift) |
src/lib/themes/apply.ts, watch.ts
|
Putting a theme on the page; theme change watcher |
src/lib/stores/settings.svelte.ts |
lightColorTheme, darkColorTheme, colorMode, setColorTheme
|
src/lib/views/settings/ColorThemePicker.svelte |
The pickers in Settings > Editor |
ui/web/src/lib/styles/colors.css |
The built-in themes (generated) |
Two settings, not one. lightColorTheme and darkColorTheme follow macOS without asking. A light theme can never be saved as the dark one: pickThemeId falls back to the mode's default.
Derive, do not hand-write. 39 themes times 71 tokens would be thousands of values to keep right. Specs hold only what the theme publishes, and the derivation guarantees readable text.
One source for every platform. The token generator runs the same deriveColors and writes each theme's colors into Swift, so the native app needs no copy of the math.
The defaults cost nothing. The built-in themes live in colors.css, so the catalog chunk loads only for another theme or when Settings > Editor shows.
Colors come from the theme's own files. Islet Light and Dark use JetBrains' ManyIslandsLight.theme.json, ManyIslandsDark.theme.json and their editor schemes in intellij-community (selection and console colors inherited from the parent schemes). Light+ and Dark+ use light_plus.json and dark_plus.json in microsoft/vscode plus its built-in workbench defaults. Rounded panels are a layout setting: see How Rounded Panels Work. Its --frame token is derived here by frameColor().
Contrast is tested, not hoped for. Every theme must pass the same checks as the built-ins.
- The log graph's eight lane colors are one set for every theme (
graphLaneinui/design-tokens/palette.json). - The switch knob is hard-coded white (the library's
Switch.svelte). - The rich Markdown editor does not redraw its diagrams on a theme change;
richEditor.tshas nowatchTheme. - At startup,
colors.cssshows the built-in theme of the macOS appearance untilsettings.init()has read settings.json, and another theme waits for the catalog chunk, so the default colors can flash.
-
ui/web/src/lib/themes/catalog.test.ts: every listed theme has a palette and every token.colorsCss.test.ts: the built-ins matchcolors.cssexactly. For every theme: text at 4.5:1 (7:1 for high contrast) on the editor, panels and background, a visible selection that text stays readable on, diff tints that stand out and keep text readable, a--framethat stands apart from the panels, and button text at 3:1. -
themeIndex.test.ts(counts, unique ids, modes,pickThemeId, groups,pickerMove),apply.test.tsandcolor.test.ts.
- A new color token goes in
colors.json,colorTokens.tsandderiveColors; the tests fail until it does. - A new theme goes in
THEME_INDEXandSPECS, then in the list on Color Themes. - Retake
color-theme-pickers.png,color-theme-dracula.png,color-theme-solarized-light.pngandcolor-theme-high-contrast.pngwhen the look changes. - Settings keys are listed in Settings Reference.
These pages are generated from docs/wiki. To correct something, open a pull request against the source file; edits made here are overwritten.
Using Nibtree
- Getting Started
- Welcome Screen
- Workspaces
- Files Panel
- File Operations
- File Icons
- Rounded Panels
- Editor and Tabs
- Pin Reorder and Wrap Tabs
- Single Tab Title
- Split Editor
- New File and Unsaved Changes
- Unload Hidden Tabs
- Editing Code
- Code Appearance
- Markdown Editor
- Image and PDF Preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and Replace
- Changes and Commits
- Sign In Prompts
- Commit Box Layout
- Repository Actions
- Commit Options
- Ignoring Files
- Diffs
- Inline Diffs
- Staging View
- Revert a Change
- Resolving Conflicts
- Merge Tool
- Blame
- History and Log
- Merge Commits
- Branches and Tags
- Branches Popup
- Remotes
- Stashes
- Shelf
- Git Menu
- Git Dialogs
- Interactive Rebase
- Worktrees
- AI Agents
- AI Tasks
- AI Usage
- Keep Computer Awake
- Submodules
- Git LFS
- GitHub
- Git Console
- Terminal
- Terminal Features
- Scripts
- Navigation
- Menus
- Settings
- Settings Search
- Color Themes
- Settings Terminal and Automation
- Settings Files
- Updates
- Status Bar and Help
- Notifications
- Memory Use
- Clear Cache
- MCP and CLI
- Git Mergetool
- Keyboard Shortcuts
- Keyboard Shortcuts Editor
- Troubleshooting
- Troubleshooting Git
Developers
- Developer Guide
- Architecture
- Project Layout
- Backend
- How Folder Watching Works
- Folder Watching Bugs We Fixed
- How Workspace Files Work
- How the Preview Scheme Works
- Backend Services
- How Terminal Features Work
- Terminal Bugs We Fixed
- Frontend
- How the Rich Markdown Editor Works
- Rich Markdown Bugs We Fixed
- Git Menu Links
- Menu Keys and Routing
- Frontend Modules
- Blame Bugs We Fixed
- Project Color And Title Bar
- One Diff View
- Diff Bugs We Fixed
- Merge Tool Layouts
- Merge Tool Bugs We Fixed
- Color Theme Bugs We Fixed
- How the Connected Diff Works
- Rounded Panels Bugs We Fixed
- Update Bugs We Fixed
- Status Bar Bugs We Fixed
- MCP and CLI Bugs We Fixed
- Repository Actions Bugs We Fixed
- How the Changes Tab Works
- CodeMirror Patterns
- How the Cursor Works
- How Editor Features Work
- How the Path Bar Works
- Commands and Events
- Settings Reference
- Commands Workspace and Changes
- Commands Branches and Remotes
- Commands History and Extras
- Commands App and Tools
- Testing
- Test Suites
- Debugging
- Debugging with MCP
- Measuring Setting Memory
- How Terminal Memory Is Measured
- How Markdown Diagrams Work
- Releases and CI
- Publishing the Wiki
- Versioning and Changelog
- Browser Version Packages
- Browser Release Checks
- Docker Image
- Platforms and Signing
- Windows Support
- Windows Bugs We Fixed
- Windows MCP and CLI
- Docs and Screenshots
- UI Library
- UI Library MCP
- UI Library Bugs
- UI Library Migration
- Contributing
How features work
- Welcome screen
- Workspaces and folders
- Files panel
- File operations
- File icons
- Editor and tabs
- Pin, reorder and wrap tabs
- Single tab title
- Split editor
- New File and unsaved changes
- Unload hidden tabs
- Editing code
- Code appearance
- Markdown editor
- Image and PDF preview
- Search Everywhere
- Recent Files
- Navigation Bar
- Find and replace
- Changes and commits
- Sign-in prompts
- Commit box layout
- Repository actions
- Commit options
- Ignoring files
- Diffs
- Inline diffs
- Staging view
- Revert a change
- Resolving conflicts
- Merge tool
- Blame
- History and log
- Merge commits
- Commit tabs
- Branches and tags
- Branches popup
- Remotes
- Stashes
- Shelf
- Git menu
- Git dialogs
- Interactive rebase
- Worktrees
- AI agents
- AI usage
- Keep computer awake
- AI tasks
- Submodules
- Git LFS
- GitHub account
- Git Console
- Back and Forward
- Integrated terminal
- Scripts tool window
- Menu bar
- Settings
- Search in Settings
- Color themes
- Rounded panels
- Updates and release channels
- Status bar and help links
- Notifications
- Memory readout and memory log
- Clear Cache
- MCP server and command line tool
- git mergetool mode