cssints/tuiA terminal app writes its styles with cssints/tui, and the same Vite plugin compiles them to prop objects for terminal renderables, not to classes: there is no sheet and no cascade. @cssints/tui applies them (states, the mouse pointer, the caret, themes); its adapters are OpenTUI and @bomb.sh/tty. The design, the full property table and what is out of scope are in research/tui.md.
import * as import csscss from "cssints/tui" with { type: "cssints" };
const const t: Tokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>
t = import csscss.createTokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>(tokens: {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
} & ValidGroup<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>, name?: (path: readonly string[]) => string | TokenName): Tokens<...>
export createTokens
Tokens from an inline DTCG-shaped `const` object: `$type` is inherited through groups, a leaf is `{ $value }`, a value is
a DTCG value or a CSS string of the type's grammar, an alias is `"{group.token}"`. The variable is the kebab-case path
(`--color-bg`), or what the mapper returns for the path.createTokens({ color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
} & ValidGroup<{
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
}, "color", {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>
color: { $type: "color"$type: "color", bg: {
readonly $value: "#1e1e2e";
}
bg: { $value: "#1e1e2e"$value: "#1e1e2e" }, accent: {
readonly $value: "#89b4fa";
}
accent: { $value: "#89b4fa"$value: "#89b4fa" } } });
import csscss.createGlobalTheme<Tokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>, {
readonly color: {
readonly bg: "#eff1f5";
readonly accent: "#1e66f5";
};
}, string>(tokens: Tokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>, condition: QueryWrap | string, overrides: {
readonly color: {
readonly bg: "#eff1f5";
readonly accent: "#1e66f5";
};
} & CheckOverrides<...>, options?: {
...;
} | undefined): void
export createGlobalTheme
Re-declares the overridden tokens under a media or container condition (on `:root`), or under a selector, and the
aliases that depend on them. An unknown path and a value that is not of the token's type are type errors.
`layer` puts the rule in a top-level layer that `layers()` declares (the build checks that it does); `scope` (with a
selector) confines it to a subtree: `@scope (<scope>) { ... }`.createGlobalTheme(const t: Tokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>
t, "light", { color: {
readonly bg: "#eff1f5";
readonly accent: "#1e66f5";
} & CheckOverrides<Tokens<{
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
}, "color", {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>, {
readonly bg: "#eff1f5";
readonly accent: "#1e66f5";
}, Tokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>>
color: { bg: "#eff1f5"bg: "#eff1f5", accent: "#1e66f5"accent: "#1e66f5" } });
export const const button: css.TuiStylebutton = import csscss.const cn: (...styles: css.TuiStyle[]) => css.TuiStyleMerges styles in argument order: a later prop wins.cn(
import csscss.const border: <readonly ["#585b70"]>(style: Exclude<BorderWord, "none">, color_0: "#585b70") => css.TuiStyle (+1 overload)border("rounded", "#585b70"),
import csscss.const p: (...values: [css.Cells] | [css.Cells, css.Cells] | [css.Cells, css.Cells, css.Cells] | [css.Cells, css.Cells, css.Cells, css.Cells]) => css.TuiStylep(0, 1),
import csscss.const width: (value: css.Size) => css.TuiStylewidth(12),
import csscss.const bg: <readonly [Token<"color", true, "#1e1e2e">]>(parts_0: Token<"color", true, "#1e1e2e">) => css.TuiStyle`background-color`.bg(const t: Tokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>
t.color: Tokens<{
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
}, "color", {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>
color.bg: Token<"color", true, "#1e1e2e">bg),
import csscss.const cursor: (value: css.Cursor) => css.TuiStyleThe mouse pointer over the element: set on over, back to `default` on out.cursor("pointer"),
import csscss.const hover: (...styles: css.TuiStyle[]) => css.TuiStyleThe styles while the pointer is over the element (`onMouseOver` to `onMouseOut`).hover(import csscss.const borderColor: <readonly [Token<"color", true, "#89b4fa">]>(parts_0: Token<"color", true, "#89b4fa">) => css.TuiStyleborderColor(const t: Tokens<{
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}, undefined, {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>
t.color: Tokens<{
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
}, "color", {
readonly color: {
readonly $type: "color";
readonly bg: {
readonly $value: "#1e1e2e";
};
readonly accent: {
readonly $value: "#89b4fa";
};
};
}>
color.accent: Token<"color", true, "#89b4fa">accent)),
import csscss.const active: (...styles: css.TuiStyle[]) => css.TuiStyleThe styles while a mouse button is down on the element.active(import csscss.const borderStyle: (value: BorderWord) => css.TuiStyleborderStyle("double")),
);
import csscss.const width: (value: css.Size) => css.TuiStylewidth("10rem"); // error: a terminal counts cellsimport csscss.boxShadow("0 0 1px red"); // error: no such member in a terminalimport csscss.const color: <readonly ["blu"]>(parts_0: "blue" | "blueviolet" | "color: unexpected \"blu\" in \"blu\", grammar <color>") => css.TuiStylecolor("blu"); // error: not a colourexport const button = { border: true, borderStyle: "rounded", borderColor: "#585b70", paddingTop: 0, paddingRight: 1, paddingBottom: 0, paddingLeft: 1, width: 12, backgroundColor: "#1e1e2e", cursor: "pointer", ":hover": { borderColor: "#89b4fa" }, ":active": { border: true, borderStyle: "double" }, themes: { light: { backgroundColor: "#eff1f5", ":hover": { borderColor: "#1e66f5" } } }, vars: { backgroundColor: "color-bg", ":hover": { borderColor: "color-accent" } } };import { attach, setTheme, use } from "@cssints/tui/opentui";
<box {...use(button)}><text>Save</text></box>; // Solid or React: the spread is a ref that calls attach
<text {...use(() => (active() ? tabActive : tab))}>Home</text>; // a style that changes: an accessor
attach(new BoxRenderable(renderer, {}), button); // core
setTheme("light");cssints/tui gives the terminal members; a site that holds one of their values becomes an object literal. A web module is untouched (its sites never reach the terminal case), and one file may hold both. A Bun app is an SSR build (vite build --ssr), or a build of an environment of its own: the plugin replaces the sites in the ssr environment of a build too, since it evaluates them on a server of its own. That server resolves as the dev server's ssr environment does: with the app's resolve.alias and the conditions of its ssr environment, so a style module may import what an alias names (2026-10-10, cssints-qnif).color, backgroundColor/bg, borderColor, border(style, color?), borderStyle (solid, double, rounded, heavy, none), fontWeight (bold, normal, lighter for dim), fontStyle, textDecoration, textAlign, padding and margin (with p, m and the side names), gap, sizes, flex (Yoga), position and its edges, overflow, display (flex, none), opacity, zIndex, cursor, caretShape, caretColor, caretAnimation, and cn, hover, focus, active, createTokens, createGlobalTheme. Any other property is not a member, so a type error.N% is of the parent; auto where Yoga takes it. A CSS length is a type error and a build error (cssints: width: a terminal counts cells: write a number, not "10rem"): no unit is converted.color grammar in the types, read at build time and written as #rrggbb (#rrggbbaa when translucent). currentColor, system colours and light-dark() are build errors.cn() merges in argument order (a later prop wins). States wrap styles and do not nest; the runtime lays :hover, then :focus, then :active over the props."light"). Every colour token is resolved at build time, per theme, aliases and derived tokens included; the site holds the default values and, under themes, what each theme changes. setTheme(name) swaps them on every attached renderable; "" is the default. A theme under media() is not read.attach(renderable, style) sets the props (color is OpenTUI's fg, the text attributes are the bits of attributes, the caret is an editable's cursorStyle and cursorColor), follows over/out, down/up and focused/blurred, and sets the mouse pointer of cursor on over and default on out. A prop the renderable lacks is not set. A focused renderable draws colours of its own (a box focusedBorderColor, also while a descendant has the focus; an editable or a select focusedBackgroundColor and focusedTextColor): each follows the style's borderColor, backgroundColor and color in the current state, theme included, so a focused box draws its :focus border, not OpenTUI's #00AAFF (2026-10-10, cssints-pkpg). On an editable or a select, color is textColor. A text node (<span>, <a>, <b> of @opentui/solid: a TextNodeRenderable) takes the props (fg, bg, attributes) and the theme, not the states: it has no mouse events and no focus, so :hover belongs on the <text> around it; its theme listener stops when it is destroyed, as @opentui/solid does with what it removes (2026-10-10, cssints-5vnf). To change the style of a renderable, attach it again: attach(r, next) on a renderable it has attached swaps the style and keeps the states, the theme and the hooks; a prop only the old style set goes back to the renderable's own value (OpenTUI's default, or the element's own prop), the focused* colours follow, and under the mouse the pointer turns to the new cursor (or default). use() takes a style or an accessor, use(() => (active() ? a : b)): Solid calls a ref in a render effect, so the signals the accessor reads run it again and each run swaps the style; React calls the new ref of each render, which swaps it too and no longer stacks the hooks. The adapter imports no framework (2026-10-10, cssints-332g). resolve(style, state, theme) of @cssints/tui is the renderer-neutral part, for other adapters.@cssints/tui/bomb is for @bomb.sh/tty, where a frame is a list of ops: createTracker() gives ui.open(id, style, extra?) and ui.text(content, style, id?, extra?), the ops with the options of the style in the state of that element id, and ui.render(term, build, { pointer }), which reads the frame's pointerenter/pointerleave events and a press of pointer.down, draws the frame again when a state changed, and adds the OSC 22 pointer of cursor and the caret (DECSCUSR, OSC 12) of the element of ui.focus(id); ui.reset() gives them back on exit. Sizes are fixed, percent, fit, and grow for flexGrow; rounded is a corner radius; double and heavy are drawn single; margins, flexShrink, flexBasis, flexWrap, alignSelf, opacity and space-* are not drawn (the table is in research/tui.md). openProps(props) and textProps(props) are the plain mapping of resolve().import { close, createTerm } from "@bomb.sh/tty";
import { createTracker } from "@cssints/tui/bomb";
const term = await createTerm({ width: 80, height: 24 });
const ui = createTracker();
const build = () => [ui.open("save", button), ui.text("Save", label, "save"), close()];
process.stdout.write(ui.render(term, build, { pointer: { x, y, down } }).output);createSystemTheme(tokens, roles, options?) of @cssints/tui/system makes each colour token a role: a hue source (hue: an ANSI name, black … brightWhite, or the terminal's fg/bg) and contrast rules (on: the tokens it sits on, contrast: the WCAG 2 ratio). A text role keeps its source colour when it meets every rule, else takes the lightness of the strictest one (the reversed WCAG ratio, bisected in OKHSL at the source's hue and saturation, the saturation falling off toward black and white, the OKLCH chroma capped at the source's); a surface is placed at exactly contrast from its first on, toward the foreground, or is the terminal's background without one. followSystemTheme of each adapter asks the terminal (OSC 10, 11 and 4 for the 16 colours, the colour-scheme report), sets the theme with the fallback at once, resolves after the replies or 100 ms, applies a late reply when it comes, and rebuilds on an appearance change (mode 2031). Light or dark: the reported background, else the scheme report (CSI ? 997 n; OpenTUI's themeMode), else COLORFGBG, else dark. A terminal that reports its background only gets the app's hues (options.hues) placed for it; one that reports nothing gets ANSI indices and its default colours on OpenTUI (ansi:N, default, drawn by the terminal; a role's ansi picks the index), and the static theme of its appearance on bomb.sh, which has no indexed colour. A token without a role keeps the value of the static theme options.themes[appearance].// theme.ts: run time, not a cssints module
import { createSystemTheme } from "@cssints/tui/system";
import { t } from "./styles.ts";
export const system = createSystemTheme(t, {
color: {
bg: { surface: true },
card: { surface: true, on: [t.color.bg], contrast: 1.1 },
text: { hue: "fg", on: [t.color.bg, t.color.card], contrast: 7 },
accent: { hue: "blue", on: [t.color.bg, t.color.card], contrast: 3 },
},
}, { themes: { light: "light" } });
// OpenTUI: renderer.getPalette() and its theme_mode event
await followSystemTheme(await createCliRenderer(), system);
// @bomb.sh/tty: the queries are written, the replies taken out of stdin before input.scan()
const follow = followSystemTheme(system, { write: (s) => process.stdout.write(s) });
process.stdin.on("data", (bytes) => dispatch(input.scan(follow.feed(bytes)).events));themes, vars: the token of each colour prop set to a lone token (vars: { backgroundColor: "color-bg" }, the name without --). defineTheme(name, values, base) of @cssints/tui registers a run-time theme: a colour per token name, laid over the static theme base for every prop with a token; setTheme(name) selects it, and defineTheme of the theme in use resolves every attached renderable again. The system theme is one; static themes stay inline, since they need no registration and no import order. A colour that is not a lone token (color-mix() over one) keeps its static value.color grammar); bench/tui.ts is 184k instantiations (tsc) for 16 calls and a token set. No terminal HMR in this version. The system theme brings culori's rgb, oklab, oklch and okhsl modes to the app (45 kB unminified in sky-tui's bundle).