Custom theme
A Caret theme is a deeply-nested object with token leaves. Most consumers only override colors.accent and symbols.anchor, but the full surface is open. This page walks through derivation: brand color → palette → theme → applied.
A minimal override
For most CLIs, a single accent color is the entire theme. Set it once at startup; caret.theme.set merges with the default.
import { caret } from './caret'
caret.theme.set({
colors: {
accent: { default: '#FF6B35' },
},
})Now every ^, every prompt prefix, every spinner braille glyph paints with your brand orange.
Derive a palette from a single color
Caret's color tokens are powered by Helmlab. The same library generates a Tailwind-style 50–950 scale from any brand color, with gamut mapping and WCAG-aware lightness steps:
import { Helmlab } from 'helmlab'
const hl = new Helmlab()
const scale = hl.semanticScale('#FF6B35')
// → { '50': '#fff7f3', ..., '500': '#FF6B35', ..., '950': '#3a1505' }
caret.theme.set({
colors: {
accent: {
default: scale['500'],
muted: scale['600'],
emphasized: scale['400'],
},
},
})Enforce WCAG contrast
For inline UI like badges, buttons, or selected items, you need contrast against the surface. Helmlab's ensureContrast nudges a color until it hits a target ratio:
import { Helmlab } from 'helmlab'
const hl = new Helmlab()
const safeAccent = hl.ensureContrast('#FF6B35', '#0a0a0a', 4.5)
// minimum 4.5:1 against the canvas — meets WCAG AA for normal textFull theme shape
| Top-level key | What it covers |
|---|---|
| colors | accent, semantic states, fg, dim — see /docs/concepts/tokens |
| motion | durations, frame rates — see /docs/concepts/motion |
| symbols | brand glyph, state, marker, structure — usually leave alone |
| spacing | gap, indent — usually leave alone |
| typography | tracking widths used by tracking() helper |
symbols via setTheme, but the result stops being a Caret CLI — recognizability collapses, spec portability breaks. Pick a different brand color, not a different glyph.Distributing a theme
For teams running multiple CLIs that should share a brand, ship the theme as a plain object from your shared package:
// Shared theme — pure data, no Caret import.
// Each consuming CLI keeps its own copy of Caret in ./caret/.
export const acmeTheme = {
colors: {
accent: { default: '#FF6B35' },
},
motion: {
duration: { default: 180 },
},
} as constimport { acmeTheme } from '@acme/cli-theme'
import type { PartialTheme } from '../caret/theme/types.js'
import { caret } from '../caret/index.js'
caret.theme.set(acmeTheme satisfies PartialTheme)Next
Read Theme for runtime precedence, or Tokens for the full token reference.