Skip to content

TSS: Terminal Style Sheets

@termuijs/tss brings CSS-like theming to terminal apps. Define variables in named themes, write selectors with pseudo-classes, and swap themes at runtime without restarting the app.

Installation

·CODE
npm install @termuijs/tss

Syntax

TSS files look like CSS but target terminal widgets instead of HTML elements:

·CODE
@theme default {
  --primary: #00ff88;
  --bg: #0a0a0f;
  --text: #e8e8f0;
  --border: #2a2a45;
}

.container {
  border-color: var(--border);
  padding: 1;
}

.container:focused {
  border-color: var(--primary);
}

Selector syntax

TSS supports a subset of CSS selectors adapted for terminal widget trees:

SelectorMatches
.nameWidget with className="name"
BoxAll Box widget instances
Box {'>'} TextText that is a direct child of Box
.panel TextText anywhere inside .panel
.btn:hoverWidget in hover state
.btn:focusedWidget with keyboard focus
.btn:disabledWidget with disabled: true
.btn:activeWidget being pressed

resolveStyle(widgetType, className?, pseudo?) takes the widget type, an optional class name, and an optional pseudo state string:

·CODE
engine.resolveStyle('Box', 'btn', 'focused')
// → { borderColor: '#00ff88', ... }

Color formats

TSS accepts any terminal-compatible color notation:

·CODE
@theme custom {
  --a: #00ff88;           /* hex */
  --b: rgb(0, 255, 136);  /* rgb() */
  --c: green;             /* ANSI named color */
  --d: 46;                /* 256-color index */
}

Named colors map to ANSI 16-color codes. The terminal renderer picks the closest match when the terminal doesn't support truecolor.

ThemeEngine

·CODE
import { ThemeEngine } from '@termuijs/tss'

const engine = new ThemeEngine()

// Load from a TSS string
engine.load(tssString)

// Or load multiple sources at once
engine.loadAll([baseTheme, customOverrides])

// Switch the active theme
engine.setTheme('cyberpunk')

// Read a variable
engine.getVariable('--primary')  // → '#ff00ff'

// Resolve styles for a widget type, class, and pseudo state
const styles = engine.resolveStyle('Box', 'container', 'focused')
// → { borderColor: '#ff00ff', padding: 1 }

// Apply resolved styles to a widget
widget.applyStyles(styles)

// React to theme changes
engine.onChange(() => {
    // re-resolve and re-apply styles here
    widget.applyStyles(engine.resolveStyle('Box', 'container'))
    app.requestRender()
})

Error handling

TSS is forgiving by design; unknown selectors and missing variables degrade gracefully:

  • A selector that matches no widget produces an empty style object; no error is thrown.- A var(--unknown) reference resolves to undefined. The property is omitted from the resolved style object.- If setTheme(name) is called with a name that doesn't exist, the engine keeps the previous theme active and emits a warning to stderr.- Parse errors in a load() call throw a TSSParseError with a line number and message.

Subsequent load() calls continue to work.

·CODE
try {
    engine.load(userTSSInput)
} catch (err) {
    if (err instanceof TSSParseError) {
console.error('Theme parse error on line', err.line, ':', err.message)
    }
}

Built-in themes

Twelve themes ship with the package as TSS strings in BUILTIN_THEMES.

·CODE
import { BUILTIN_THEMES } from '@termuijs/tss'

engine.load(BUILTIN_THEMES.default)
ThemeDescription
defaultDark background with cyan accents
cyberpunkNeon magenta and cyan on deep navy
nordArctic, muted blues and grays
draculaDeep purple with pastel accents
gruvboxWarm earthy browns with muted accents
catppuccinWarm pastel palette
solarizedEthan Schoonover's solarized colors
tokyo-nightSoft blues and purples on near-black
solarizedLightSolarized palette on a warm light base
highContrastPure black with white text and vivid accents
everforestMuted greens on a soft dark base
rose-pineDusty rose, pine green, and gold on deep indigo

See Built-in Themes for the full theme guide including AutoThemeProvider and runtime switching with useTheme().

Hot-reload with TSSWatcher

During development, the watcher picks up .tss file changes and reloads automatically:

·CODE
import { TSSWatcher } from '@termuijs/tss'

const watcher = new TSSWatcher(engine, {
    dir: './themes',
    onReload: (filename) => console.log('Reloaded', filename),
})
watcher.start()

Mixins

Mixins let you extract reusable property groups and include them in any rule. Define a mixin with @mixin, then pull it in with @include. Rule-level properties override mixin properties when names conflict.

·CODE
@mixin focused-border {
  border-color: var(--primary);
  border-style: bold;
}

.panel {
  border-color: var(--border);
}

.panel:focused {
  @include focused-border;
}

A rule can include multiple mixins. The engine expands all includes at load time, so resolveStyle() sees a flat property map.

calc() expressions

Use calc() to compute numeric property values from variables and arithmetic. Supports +, -, *, /, and nested parentheses.

·CODE
.sidebar {
  width: calc(var(--sidebar-width) - 2);
}

.content {
  width: calc(var(--total-width) - var(--sidebar-width) - 4);
}

Variables used inside calc() must resolve to numbers. The engine throws at resolution time if a variable is non-numeric.

@import

Split large theme files into partials and import them. Paths are relative to the importing file. Circular imports are detected and skipped.

·CODE
@import './base.tss';
@import './tokens.tss';

Supported extensions: .tss, .json, .yaml, .yml. Path traversal outside the base directory is blocked automatically.

Size media queries

Wrap rules in @media blocks to apply styles only when the terminal meets a size condition. Units are character columns and rows.

·CODE
@media (min-width: 120) {
  .panel { width: 60; }
}

@media (min-width: 80) and (max-height: 40) {
  .sidebar { width: 20; }
}

Supported features: min-width, max-width, min-height, max-height. An unrecognized feature causes that block to not apply.

Nested rules

Write child selectors inside a parent block using &. The & expands to the parent selector at compile time.

·CODE
.card {
  border-color: var(--border);

  &:focused {
    border-color: var(--primary);
  }

  & Text {
    color: var(--text);
  }
}

Nesting is single-level. Deeply nested blocks are not supported.

Runtime variable overrides

Override a theme variable at runtime without reloading the full theme. The engine re-resolves dependent rules and fires onChange listeners.

·CODE
engine.setVariable('--primary', '#ff0000')

// Restore the theme-defined value
engine.clearVariable('--primary')

Overrides persist until cleared or until setTheme() is called, which resets all overrides to the new theme's values.

Color functions

Three color manipulation functions are available inside @theme variable definitions: lighten, darken, and alpha.

·CODE
@theme custom {
  --primary: #336699;
  --primary-dim: alpha(#336699, 0.5);
  --primary-light: lighten(#336699, 0.2);
  --primary-dark: darken(#336699, 0.2);
}

Amounts accept a plain decimal (0.2) or a percentage string (20%). alpha() returns an rgba() string; lighten and darken return hex.

See also