Skip to content

Theme System

The silvery theme system transforms a 22-color terminal palette into 33 semantic tokens for UI consumption. The pipeline flows in one direction:

ColorPalette (22) → deriveTheme() → Theme (33) → resolveThemeColor() → ANSI output

Components never reference raw colors directly. They use $token strings (color="$primary") that resolve against the active theme at render time. This decouples UI code from any specific palette.

ColorPalette (22 Colors)

The universal terminal color format. Every modern terminal emulator uses this shape — Ghostty, Kitty, Alacritty, iTerm2, WezTerm, and others all export/import these 22 fields.

Fields

16 ANSI colors (indices 0--15):

FieldANSI IndexDescription
black0Normal black
red1Normal red
green2Normal green
yellow3Normal yellow
blue4Normal blue
magenta5Normal magenta
cyan6Normal cyan
white7Normal white
brightBlack8Bright black
brightRed9Bright red
brightGreen10Bright green
brightYellow11Bright yellow
brightBlue12Bright blue
brightMagenta13Bright magenta
brightCyan14Bright cyan
brightWhite15Bright white

6 special colors:

FieldDescription
foregroundDefault text color
backgroundDefault background color
cursorColorCursor block/line color
cursorTextText rendered under the cursor
selectionBackgroundBackground of selected text
selectionForegroundText color of selected text

Optional metadata:

FieldTypeDescription
namestringHuman-readable palette name
darkbooleanWhether this is a dark palette
primarystringSemantic primary accent override (hex, e.g. #89b4fa)

When primary is set, deriveTheme() uses it instead of inferring from ANSI slots. Builder APIs (createTheme().primary(), quickTheme(), autoGenerateTheme()) set this automatically. Built-in palettes leave it unset and rely on the default ANSI slot mapping.

Type Definition

typescript
interface ColorPalette {
  name?: string
  dark?: boolean
  primary?: string

  // 16 ANSI palette — all required hex strings (#RRGGBB)
  black: string
  red: string
  green: string
  yellow: string
  blue: string
  magenta: string
  cyan: string
  white: string
  brightBlack: string
  brightRed: string
  brightGreen: string
  brightYellow: string
  brightBlue: string
  brightMagenta: string
  brightCyan: string
  brightWhite: string

  // 6 special colors — all required hex strings
  foreground: string
  background: string
  cursorColor: string
  cursorText: string
  selectionBackground: string
  selectionForeground: string
}

Theme (33 Semantic Tokens)

The Theme interface is what UI components consume. Every property name is lowercase with no hyphens (e.g., surfacebg, not surface-bg). All color values are hex strings in truecolor mode, or ANSI color names in ANSI 16 mode.

Pairing Conventions

Tokens follow two pairing conventions depending on their role:

Surface pairs$name is text, $name-bg is background:

Token pairPurpose
$muted / $muted-bgSecondary text / hover surface
$surface / $surface-bgElevated content text / bg
$popover / $popover-bgFloating content text / bg
$inverse / $inverse-bgChrome area text / bg
$cursor / $cursor-bgText under cursor / cursor color
$selection / $selection-bgSelected text / selection bg

Accent pairs$name is area background, $name-fg is text on that area:

Token pairPurpose
$primary / $primary-fgBrand accent area
$secondary / $secondary-fgAlternate accent area
$accent / $accent-fgAttention/pop accent area
$error / $error-fgError/destructive area
$warning / $warning-fgWarning/caution area
$success / $success-fgSuccess/positive area
$info / $info-fgNeutral info area

5 standalone tokens:

TokenPurpose
$borderStructural dividers and borders
$inputborderInteractive control borders (inputs, buttons)
$focusborderFocus ring (always blue for accessibility)
$linkHyperlinks
$disabledfgDisabled/placeholder text

16 palette passthrough: $color0 through $color15 map to the palette array.

Complete Token Table

TokenPropertyCategoryPurpose
$bgbgRootDefault background
$fgfgRootDefault text
$mutedmutedSurfaceSecondary/muted text (~70% contrast)
$muted-bgmutedbgSurfaceMuted area background (hover state)
$surfacesurfaceSurfaceText on elevated surface
$surface-bgsurfacebgSurfaceElevated content area background
$popoverpopoverSurfaceText on floating content
$popover-bgpopoverbgSurfaceFloating content background
$inverseinverseSurfaceText on chrome area
$inverse-bginversebgSurfaceChrome area background (status/title bar)
$cursorcursorSurfaceText under cursor
$cursor-bgcursorbgSurfaceCursor color
$selectionselectionSurfaceText on selected items
$selection-bgselectionbgSurfaceSelected items background
$primaryprimaryAccentBrand accent area
$primary-fgprimaryfgAccentText on primary accent area
$secondarysecondaryAccentAlternate accent area
$secondary-fgsecondaryfgAccentText on secondary accent area
$accentaccentAccentAttention/pop accent area
$accent-fgaccentfgAccentText on accent area
$errorerrorAccentError/destructive area
$error-fgerrorfgAccentText on error area
$warningwarningAccentWarning/caution area
$warning-fgwarningfgAccentText on warning area
$successsuccessAccentSuccess/positive area
$success-fgsuccessfgAccentText on success area
$infoinfoAccentNeutral info area
$info-fginfofgAccentText on info area
$borderborderStandaloneStructural dividers
$inputborderinputborderStandaloneInteractive control borders
$focusborderfocusborderStandaloneFocus border (always blue)
$linklinkStandaloneHyperlinks
$disabledfgdisabledfgStandaloneDisabled/placeholder text

Type Definition

typescript
interface Theme {
  name: string

  // Root pair
  bg: string
  fg: string

  // 6 surface pairs (base = text, *bg = background)
  muted: string
  mutedbg: string
  surface: string
  surfacebg: string
  popover: string
  popoverbg: string
  inverse: string
  inversebg: string
  cursor: string
  cursorbg: string
  selection: string
  selectionbg: string

  // 7 accent pairs (base = area bg, *fg = text on area)
  primary: string
  primaryfg: string
  secondary: string
  secondaryfg: string
  accent: string
  accentfg: string
  error: string
  errorfg: string
  warning: string
  warningfg: string
  success: string
  successfg: string
  info: string
  infofg: string

  // 5 standalone tokens
  border: string
  inputborder: string
  focusborder: string
  link: string
  disabledfg: string

  // 16 ANSI colors ($color0--$color15)
  palette: string[]
}

deriveTheme()

Transforms a 22-color ColorPalette into a 33-token Theme.

typescript
function deriveTheme(palette: ColorPalette, mode?: "truecolor" | "ansi16", adjustments?: ThemeAdjustment[]): Theme

Parameters

ParameterTypeDefaultDescription
paletteColorPaletterequiredThe 22-color terminal palette
mode"truecolor" | "ansi16""truecolor"Derivation mode
adjustmentsThemeAdjustment[]undefinedOptional array to collect contrast adjustments made

Truecolor Mode

The default mode. Uses blending and contrast-aware adjustment to produce rich, harmonious themes.

Contrast targets — minimums that ensureContrast() enforces. Most themes exceed them without adjustment:

TargetRatioApplied toRationale
AA4.5:1Body text, muted text, accent-as-text, selectionWCAG AA for normal text
DIM3.0:1Disabled textIntentionally dim but still visible
FAINT1.5:1Borders, structural dividersFaint structural element
CONTROL3.0:1Input bordersWCAG 1.4.11 non-text minimum

Derivation rules:

TokenSourceContrast target
fgpalette.foreground ensured against popoverbgAA (4.5:1)
primarypalette.primary or yellow (dark) / blue (light)AA (4.5:1)
accentComplement of primaryAA (4.5:1)
secondaryBlend of primary and accent at 35%AA (4.5:1)
errorpalette.redAA (4.5:1)
warningpalette.yellowAA (4.5:1)
successpalette.greenAA (4.5:1)
infoBlend of fg and accent at 50%AA (4.5:1)
linkbrightBlue (dark) / blue (light)AA (4.5:1)
mutedfg blended 40% toward bg, against mutedbgAA (4.5:1)
disabledfgfg blended 50% toward bgDIM (3.0:1)
borderbg blended 15% toward fgFAINT (1.5:1)
inputborderbg blended 25% toward fgCONTROL (3.0:1)
surfacebgbg blended 5% toward fg--
popoverbgbg blended 8% toward fg--
mutedbgbg blended 4% toward fg--
inversebgfg blended 10% toward bg--
inversecontrastFg(inversebg) (black or white)--
selectionpalette.selectionForegroundAA (4.5:1)
cursorpalette.cursorTextAA (4.5:1)
focusborderSame as link--
*fg tokenscontrastFg(base) (black or white)--

The derivation uses a blend-first-then-ensure pattern: the initial blend sets the color's character from the palette's aesthetic, then ensureContrast() only adjusts lightness (preserving hue and saturation) if the ratio falls short.

Primary color inference: When palette.primary is not set, the primary defaults to palette.yellow for dark themes and palette.blue for light themes. Set palette.primary explicitly to override this.

ANSI 16 Mode

Direct mapping with no blending or hex math. Token values are ANSI color names rather than hex strings.

typescript
const theme = deriveTheme(palette, "ansi16")
// theme.primary === palette.yellow (dark) or palette.blue (light)
// theme.border === palette.brightBlack
// theme.fg === palette.foreground

ThemeAdjustment

When the optional adjustments array is passed, deriveTheme() records every contrast adjustment it makes:

typescript
interface ThemeAdjustment {
  token: string // Token name (e.g. "primary", "muted")
  from: string // Original color before adjustment
  to: string // Adjusted color
  against: string // Background used for contrast check
  target: number // Target contrast ratio
  ratioBefore: number // Contrast ratio before adjustment
  ratioAfter: number // Contrast ratio after adjustment
}

This is useful for debugging and for theme preview tooling.

resolveThemeColor()

Resolves a $token string against a Theme object.

typescript
function resolveThemeColor(color: string | undefined, theme: Theme): string | undefined

Resolution rules:

InputBehaviorExample
undefinedReturns undefined--
"$primary"Lookup theme.primary"#EBCB8B"
"$surface-bg"Strip hyphens, lookup theme.surfacebg"#323845"
"$color0"--"$color15"Index into theme.palette"#2E3440"
"#ff0000"Pass through unchanged"#ff0000"
"red"Pass through unchanged"red"
Unknown $tokenPass through as-is"$unknown" -> "$unknown"

Both $surfacebg and $surface-bg resolve identically — hyphens are stripped before lookup.

Built-in Palettes

The @silvery/theme package ships 38 palettes across 23 palette files, covering the most popular terminal and editor color schemes.

Palette Families

FamilyPalettesCount
Catppuccinmocha, frappe, macchiato, latte4
Nordnord1
Draculadracula1
Solarizeddark, light2
Tokyo Nighttokyo-night, storm, day3
One Darkone-dark1
Gruvboxdark, light2
Rose Pinerose-pine, moon, dawn3
Kanagawawave, dragon, lotus3
Everforestdark, light2
Monokaimonokai, monokai-pro2
Snazzysnazzy1
Materialdark, light2
Palenightpalenight1
Ayudark, mirage, light3
Nightfoxnightfox, dawnfox2
Horizonhorizon1
Moonflymoonfly1
Nightflynightfly1
Oxocarbondark, light2
Sonokaisonokai1
Edgedark, light2
Modusvivendi, operandi2

Using Palettes

typescript
import { builtinPalettes, getPaletteByName, deriveTheme } from "silvery/theme"

// List all palette names
const names = Object.keys(builtinPalettes)
// ["catppuccin-mocha", "catppuccin-frappe", ..., "modus-operandi"]

// Look up by name
const palette = getPaletteByName("catppuccin-mocha")
if (palette) {
  const theme = deriveTheme(palette)
}

// Import a specific palette directly
import { nord, catppuccinMocha } from "silvery/theme"
const nordTheme = deriveTheme(nord)

Pre-derived Themes

Four themes ship pre-derived for instant use:

ExportPaletteModePrimary
defaultDarkThemeNorddark#EBCB8B
defaultLightThemeCatppuccin Lattelight#1E66F5
ansi16DarkTheme(hardcoded)darkyellow
ansi16LightTheme(hardcoded)lightblue
typescript
import { defaultDarkTheme, defaultLightTheme, ansi16DarkTheme, ansi16LightTheme, getThemeByName } from "silvery/theme"

// Look up by name
const theme = getThemeByName("dark-truecolor") // defaultDarkTheme
const light = getThemeByName("light-ansi16") // ansi16LightTheme
const catppuccin = getThemeByName("catppuccin-mocha") // derived on access

ANSI 16 Fallback

Two hardcoded themes provide baseline support for terminals limited to 16 colors.

ansi16DarkTheme

Token values are ANSI color names (e.g., "yellow", "whiteBright", "gray") rather than hex strings. The palette array contains the 16 standard color names.

Key mappings:

  • primary = "yellow", accent = "blueBright"
  • fg = "whiteBright", muted = "white", disabledfg = "gray"
  • border / inputborder = "gray", focusborder / link = "blueBright"
  • error = "redBright", success = "greenBright", warning = "yellow"

ansi16LightTheme

Same structure, inverted for light backgrounds:

  • primary = "blue", accent = "cyan"
  • fg = "black", muted = "blackBright", disabledfg = "gray"
  • error = "red", success = "green"

When They Activate

ANSI 16 themes are used when:

  • deriveTheme(palette, "ansi16") is called explicitly
  • The detected color level is "basic" (only 16 colors supported)
  • No palette detection is available and the application falls back to safe defaults

Color Utilities

Low-level functions for color manipulation, available from silvery/theme or @silvery/theme.

Blending and Manipulation

typescript
import { blend, brighten, darken, desaturate, complement } from "silvery/theme"

blend("#2E3440", "#ECEFF4", 0.5) // midpoint between two colors
brighten("#2E3440", 0.1) // 10% toward white
darken("#ECEFF4", 0.1) // 10% toward black
desaturate("#BF616A", 0.4) // reduce saturation by 40%
complement("#EBCB8B") // 180-degree hue rotation
FunctionSignatureDescription
blend(a, b, t) => stringLinear RGB blend. t=0 returns a, t=1 returns b.
brighten(color, amount) => stringBlend toward white by amount (0--1).
darken(color, amount) => stringBlend toward black by amount (0--1).
desaturate(color, amount) => stringReduce saturation by amount (0--1) in HSL.
complement(color) => string180-degree hue rotation in HSL.

All functions accept hex strings (#RRGGBB). Non-hex inputs are returned unchanged.

Contrast

typescript
import { contrastFg, checkContrast, ensureContrast } from "silvery/theme"

contrastFg("#2E3440") // "#FFFFFF" (white text on dark bg)
contrastFg("#ECEFF4") // "#000000" (black text on light bg)

checkContrast("#FFFFFF", "#000000") // { ratio: 21, aa: true, aaa: true }
checkContrast("#777777", "#888888") // { ratio: ~1.3, aa: false, aaa: false }

ensureContrast("#FFAB91", "#FFFFFF", 4.5) // "#B35600" (darkened to meet AA)
ensureContrast("#5C9FFF", "#1A1A2E", 4.5) // "#5C9FFF" (already passes)
FunctionSignatureDescription
contrastFg(bg) => "#000000" | "#FFFFFF"Pick black or white for readability on bg.
checkContrast(fg, bg) => ContrastResult | nullWCAG 2.1 contrast ratio with AA/AAA pass/fail.
ensureContrast(color, against, minRatio) => stringAdjust lightness until the contrast target is met. Preserves hue and saturation.

ensureContrast uses binary search over lightness in HSL space. It returns the original color unchanged if the target is already met.

Conversion

typescript
import { hexToRgb, rgbToHex, hexToHsl, hslToHex, rgbToHsl } from "silvery/theme"

hexToRgb("#BF616A") // [191, 97, 106]
rgbToHex(191, 97, 106) // "#BF616A"
hexToHsl("#BF616A") // [354.3, 0.39, 0.56]
hslToHex(354.3, 0.39, 0.56) // "#BF616A"
rgbToHsl(191, 97, 106) // [354.3, 0.39, 0.56]

Usage in Components

Components reference theme tokens with the $ prefix. Resolution happens automatically within a ThemeProvider.

tsx
import { ThemeProvider, defaultDarkTheme, Box, Text } from "silvery"

function App() {
  return (
    <ThemeProvider theme={defaultDarkTheme}>
      <Text color="$primary">Deploy</Text>
      <Text color="$muted">3 files changed</Text>
      <Box backgroundColor="$surface-bg" borderStyle="single" borderColor="$border">
        <Text color="$success">All tests passed</Text>
      </Box>
    </ThemeProvider>
  )
}

ThemeProvider

Wraps the app (or a subtree) to enable $token resolution:

tsx
<ThemeProvider theme={defaultDarkTheme}>
  <App />
</ThemeProvider>

useTheme()

Read the current theme from any component:

tsx
import { useTheme } from "silvery/theme"

function StatusLine() {
  const theme = useTheme()
  const color = theme.primary // hex string
  return <Text color="$primary">Status</Text>
}

Returns defaultDarkTheme when no ThemeProvider is present.

Per-subtree Overrides

Use the theme prop on Box to override token resolution for a subtree:

tsx
<Box theme={lightTheme} borderStyle="single">
  {/* All $token references resolve against lightTheme here */}
  <Text color="$primary">Themed content</Text>
</Box>

See the Theming guide for more detail on $token shorthand, special values (inherit, mix(), $default), and backward-compatible aliases.

Usage in CLI (@silvery/ansi)

For non-React CLI output, use @silvery/ansi which provides the same theme token resolution without React:

typescript
import { createStyle } from "@silvery/ansi"

const s = createStyle({ theme })
s.primary("deploy") // resolves theme.primary -> hex -> ANSI
s.success("done") // resolves theme.success -> hex -> ANSI
s.muted("(3 files)") // resolves theme.muted -> hex -> ANSI
s.bold.red("error!") // standard chalk-compatible styling

Custom Palettes

Manual ColorPalette

Create a ColorPalette object with all 22 required hex fields:

typescript
import { deriveTheme } from "silvery/theme"
import type { ColorPalette } from "silvery/theme"

const myPalette: ColorPalette = {
  name: "my-palette",
  dark: true,
  black: "#1a1b26",
  red: "#f7768e",
  green: "#9ece6a",
  yellow: "#e0af68",
  blue: "#7aa2f7",
  magenta: "#bb9af7",
  cyan: "#7dcfff",
  white: "#a9b1d6",
  brightBlack: "#414868",
  brightRed: "#f7768e",
  brightGreen: "#9ece6a",
  brightYellow: "#e0af68",
  brightBlue: "#7aa2f7",
  brightMagenta: "#bb9af7",
  brightCyan: "#7dcfff",
  brightWhite: "#c0caf5",
  foreground: "#c0caf5",
  background: "#1a1b26",
  cursorColor: "#c0caf5",
  cursorText: "#1a1b26",
  selectionBackground: "#33467c",
  selectionForeground: "#c0caf5",
}

const theme = deriveTheme(myPalette)

Theme Builder

The chainable builder API generates a full ColorPalette from minimal input:

typescript
import { createTheme } from "silvery/theme"

// From just a background color
const theme = createTheme().bg("#1e1e2e").build()

// With foreground and primary
const theme = createTheme().bg("#1e1e2e").fg("#cdd6f4").primary("#89b4fa").build()

// From a built-in preset with an override
const theme = createTheme().preset("nord").primary("#A3BE8C").build()

// Force dark/light mode
const theme = createTheme().primary("#EBCB8B").dark().build()

Builder methods:

MethodDescription
.bg(color)Set background color
.fg(color)Set foreground color
.primary(color)Set primary accent color
.accent(color)Alias for .primary()
.dark()Force dark mode
.light()Force light mode
.color(name, value)Set any palette color by name
.palette(p)Set full palette at once
.preset(name)Load a built-in palette by name
.build()Derive the final Theme

quickTheme()

Create a theme from a single color:

typescript
import { quickTheme } from "silvery/theme"

quickTheme("#818cf8") // indigo primary, dark mode (default)
quickTheme("#818cf8", "light") // indigo primary, light mode
quickTheme("blue") // named color, dark mode
quickTheme("green", "dark") // named color, explicit dark

Supported named colors: red, orange, yellow, green, teal, cyan, blue, purple, pink, magenta, white.

autoGenerateTheme()

Generate a complete theme from a single hex color with automatic palette derivation:

typescript
import { autoGenerateTheme } from "silvery/theme"

const theme = autoGenerateTheme("#5E81AC", "dark")
const light = autoGenerateTheme("#E06C75", "light")

Uses HSL manipulation to derive complementary accents, surface ramps, and status colors from the primary.

fromColors()

Generate a full ColorPalette from 1--3 hex colors:

typescript
import { fromColors, deriveTheme } from "silvery/theme"

const palette = fromColors({
  background: "#1e1e2e",
  foreground: "#cdd6f4",
  primary: "#89b4fa",
  dark: true,
})
const theme = deriveTheme(palette)

At minimum, provide background or primary. Missing colors are generated via surface ramps and hue rotation.

Data Flow

Terminal palette file (Ghostty, Kitty, etc.)


    ┌──────────────┐
    │ ColorPalette │  22 hex colors — universal pivot format
    │   (Layer 1)  │
    └──────┬───────┘

     deriveTheme()    contrast targets, blending, contrastFg()


    ┌──────────────┐
    │    Theme     │  33 semantic tokens — what UI consumes
    │   (Layer 2)  │
    └──────┬───────┘

   resolveThemeColor()    "$primary" → "#EBCB8B"

           ├──► Component props      color="$primary"
           ├──► createStyle()        s.primary("text")
           └──► Programmatic access  useTheme().primary

Released under the MIT License.