Skip to content

Theming

Silvery provides a modern, progressively enhanced theme system with semantic color tokens. Themes work across terminal capability tiers — from ANSI 16 colors to full 24-bit truecolor — using the same token vocabulary.

Setup

Wrap your app in ThemeProvider with a theme object:

tsx
import { ThemeProvider, ansi16DarkTheme, Box, Text } from "@silvery/term"

function App() {
  return (
    <ThemeProvider theme={ansi16DarkTheme}>
      <Box borderStyle="single">
        <Text color="$primary">Hello</Text>
        <Text color="$text2">world</Text>
      </Box>
    </ThemeProvider>
  )
}

$token Shorthand

Any color prop on Box or Text that starts with $ resolves against the active theme:

PropComponentsExample
colorBox, Textcolor="$primary"
backgroundColorBox, TextbackgroundColor="$surface-bg"
borderColorBoxborderColor="$separator"
outlineColorBoxoutlineColor="$focusring"

Non-$ values pass through unchanged (color="red", color="#ff0000").

Default border color: When borderStyle or outlineStyle is set without an explicit color, the theme's $separator token is used automatically.

Token Reference

Brand (3 tokens)

TokenUseANSI 16 DarkTruecolor Dark
$primaryBrand tint, active indicators, headingsyellow#EBCB8B
$linkHyperlinks, referencesyellowBright#ECCC90
$controlInteractive chrome, shortcuts, input bordersyellow#B8A06E

Selection (3 tokens)

TokenUseANSI 16 DarkTruecolor Dark
$selectedSelection highlight backgroundcyan#88C0D0
$selectedfgText on selected backgroundblack#2E3440
$focusringKeyboard focus outline (always blue)blueBright#5E81AC

Text (4 tokens)

TokenUseANSI 16 DarkTruecolor Dark
$textPrimary content — headings, bodywhiteBright#ECEFF4
$text2Secondary — descriptions, metadatawhite#D8DEE9
$text3Tertiary — timestamps, hintsgray#7B88A1
$text4Quaternary — ghost text, decorativegray (+dim)#545E72

Surface (5 tokens)

TokenUseANSI 16 DarkTruecolor Dark
$bgDefault background(default)#2E3440
$defaultTerminal's actual default bg (SGR 49)(terminal)(terminal)
$surfaceDialogs, overlays, popoversblack#3B4252
$separatorDividers, borders, rulesgray#4C566A
$chromebgTitle bars, status bars (inverted bg)whiteBright#ECEFF4
$chromefgText on chrome areas (inverted fg)black#2E3440

$default is special — it's not a color value from the theme, but an instruction to use the terminal's actual default background (SGR 49). Use it when you want an overlay to be opaque without hardcoding a specific color. Unlike $bg (which is a theme-derived approximation), $default matches whatever the user configured in their terminal emulator.

Status (3 tokens)

TokenUseANSI 16 DarkTruecolor Dark
$errorDestructive, overdue, errorsredBright#BF616A
$warningCaution, unsaved changesyellow#EBCB8B
$successPositive, completed, savedgreenBright#A3BE8C

Content Palette (16 indexed colors)

For categorization — tags, calendar colors, chart series:

tsx
<Text color="$color5">purple tag</Text>
<Text color="$color1">red badge</Text>

$color0 through $color15 map to the theme's palette array. At ANSI 16 these are the standard terminal colors; at truecolor they are curated equal-weight hues designed for readability in both dark and light modes.

Progressive Enhancement

The same token vocabulary works across all terminal capability tiers:

ANSI 16 (baseline — every terminal)

Each token maps to one of the 16 standard colors. Differentiation comes from the bright variants (e.g., yellow vs yellowBright) and the dimColor attribute. No color derivation is possible — all tokens are independent.

256-color

Tokens can use the 216-color cube (indices 16–231) and the 24-shade gray ramp (indices 232–255). This enables 2-3 levels of tint/shade per hue.

Truecolor (24-bit)

Full derivation from a single primary hue. The token relationships become mathematical:

  • link = primary lightened 5%
  • control = primary at 70% lightness
  • selected = contrasting hue at 30% opacity over bg
  • text2 = text at 85% opacity
  • text3 = text at 50% opacity
  • text4 = text at 30% opacity
  • surface = bg lightened 5% (dark mode) or darkened 3% (light mode)
  • separator = text at 20% opacity
  • chromebg = text color (inverted for use as background on title bars)
  • chromefg = bg color (inverted for use as text on title bars)

generateTheme()

Generate a complete ANSI 16 theme from a primary color:

tsx
import { generateTheme } from "@silvery/term"

const theme = generateTheme("cyan", true) // primary=cyan, dark=true
const light = generateTheme("blue", false) // primary=blue, light mode

The function derives all 17 tokens from the primary color + dark/light preference:

  • Warm primaries (yellow, red, magenta, green, white) get cyan as the contrasting selection color
  • Cool primaries (cyan, blue) get yellow as the selection color
  • focusring is always blue (accessibility — must always be distinguishable)
  • warning equals primary (context always disambiguates via icons/labels)

Available primaries: yellow, cyan, magenta, green, red, blue, white.

Creating Custom Themes

Implement the Theme interface:

tsx
import { type Theme, ThemeProvider } from "@silvery/term"

const myTheme: Theme = {
  name: "my-theme",
  dark: true,

  primary: "#E0A526",
  link: "#E5B34A",
  control: "#B8871F",

  selected: "#4A90D9",
  selectedfg: "#1A1A1A",
  focusring: "#4A90D9",

  text: "#E8E8E8",
  text2: "#C0C0C0",
  text3: "#808080",
  text4: "#505050",

  bg: "#1A1A2E",
  surface: "#242440",
  separator: "#3A3A5A",

  error: "#E74C3C",
  warning: "#E0A526",
  success: "#2ECC71",

  palette: [
    /* 16 content colors */
  ],
}

Deriving Colors

When building truecolor themes, derive related tokens from a base color to maintain visual harmony:

typescript
// Lighten: mix toward white
function lighten(hex: string, amount: number): string {
  // Increase each RGB channel by amount% toward 255
}

// Darken: mix toward black
function darken(hex: string, amount: number): string {
  // Decrease each RGB channel by amount%
}

// Opacity: blend toward background
function withOpacity(fg: string, bg: string, opacity: number): string {
  // result = fg * opacity + bg * (1 - opacity)
}

// Contrast: pick black or white text for readability
function contrastFg(bg: string): string {
  // Calculate relative luminance (0.2126*R + 0.7152*G + 0.0722*B)
  // Return dark text if luminance > 0.5, light text otherwise
}

Recommended derivation from a single accent hue:

TokenAlgorithm
linklighten(primary, 5%)
controlwithOpacity(primary, bg, 0.7)
selectedPick contrasting hue, 30% over bg
selectedfgcontrastFg(selected)
text2withOpacity(text, bg, 0.85)
text3withOpacity(text, bg, 0.50)
text4withOpacity(text, bg, 0.30)
surfacelighten(bg, 5%) (dark) or darken(bg, 3%) (light)
separatorwithOpacity(text, bg, 0.20)
chromebgtext (inverted: text color becomes background)
chromefgbg or contrastFg(chromebg) (dark on light)

Per-Subtree Theme Override

Use the theme prop on Box to override $token resolution for an entire subtree:

tsx
const dimmedTheme: Theme = { ...baseTheme, selected: "gray", selectedfg: "white" }

<Box theme={dimmedTheme}>
  {/* All $selected references here resolve to "gray" */}
  <Text color="$selected">dimmed</Text>
</Box>
<Text color="$selected">normal</Text>

This works like CSS custom properties — the nearest ancestor Box with a theme prop determines token resolution for its descendants. Nested theme props cascade (innermost wins). When theme is undefined, tokens resolve against the root ThemeProvider theme.

The override happens during the content phase tree walk (no React re-renders). Cost is ~2ns per getActiveTheme() call — negligible.

useTheme() Hook

Read the current theme from any component:

tsx
import { useTheme } from "@silvery/term"

function StatusLine() {
  const theme = useTheme()
  return <Text color={theme.dark ? "$text" : "$text2"}>Status</Text>
}

Returns ansi16DarkTheme when no ThemeProvider is present.

resolveThemeColor()

For advanced use cases, resolve tokens programmatically:

tsx
import { resolveThemeColor, useTheme } from "@silvery/term"

function CustomComponent({ highlight }: { highlight?: string }) {
  const theme = useTheme()
  const color = resolveThemeColor(highlight, theme) ?? theme.text
  // ...
}

Backward Compatibility

Old token names from v1 are aliased automatically:

Old TokenResolves To
$accent$primary
$muted$text2
$raisedbg$surface
$background$bg
$border$separator

These aliases allow gradual migration. New code should use the v2 token names.

Built-in Themes

NameTierPrimaryMode
ansi16DarkThemeANSI 16yellowdark
ansi16LightThemeANSI 16bluelight
defaultDarkThemeTruecolor#EBCB8Bdark
defaultLightThemeTruecolor#0056B3light

Select by name at runtime:

tsx
import { getThemeByName } from "@silvery/term"

const theme = getThemeByName("dark-ansi16") // or "dark-truecolor", "light-ansi16", etc.

Released under the MIT License.