All docs
Authoring · Custom theme

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.

src/index.ts
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:

ts
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:

ts
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 text

Full theme shape

Top-level keyWhat it covers
colorsaccent, semantic states, fg, dim — see /docs/concepts/tokens
motiondurations, frame rates — see /docs/concepts/motion
symbolsbrand glyph, state, marker, structure — usually leave alone
spacinggap, indent — usually leave alone
typographytracking widths used by tracking() helper
Symbols are the brand
The manifesto rule: never customize the symbol set. You CAN override 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:

@acme/cli-theme/index.ts
// 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 const
apps/billing-cli/src/index.ts
import { 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.