Style
Terminal styling, layout utilities, and markdown theme for CLI output.
The style package provides ANSI-safe styling primitives, terminal capability awareness, text layout helpers, and a semantic markdown theme. It has zero runtime dependencies.
Install
bun add @crustjs/styleExports
Style Instance
| Export | Description |
|---|---|
style | Default auto-mode style instance |
createStyle(options?) | Create a configured style instance |
setGlobalColorMode(mode) | Override runtime color output for the default exports |
getGlobalColorMode() | Read the current runtime color override |
Modifiers
| Export | Description |
|---|---|
bold(text) | Bold text |
dim(text) | Dimmed text |
italic(text) | Italic text |
underline(text) | Underlined text |
inverse(text) | Inverted foreground/background |
hidden(text) | Hidden text |
strikethrough(text) | Strikethrough text |
Colors
Foreground: black, red, green, yellow, blue, magenta, cyan, white, gray, brightRed, brightGreen, brightYellow, brightBlue, brightMagenta, brightCyan, brightWhite
Background: bgBlack, bgRed, bgGreen, bgYellow, bgBlue, bgMagenta, bgCyan, bgWhite, bgBrightBlack, bgBrightRed, bgBrightGreen, bgBrightYellow, bgBrightBlue, bgBrightMagenta, bgBrightCyan, bgBrightWhite
Dynamic Colors (Truecolor)
Direct styling functions using 24-bit truecolor ANSI sequences:
| Export | Description |
|---|---|
rgb(text, r, g, b) | Apply truecolor foreground RGB color |
bgRgb(text, r, g, b) | Apply truecolor background RGB color |
hex(text, hexColor) | Apply truecolor foreground hex color (#RGB or #RRGGBB) |
bgHex(text, hexColor) | Apply truecolor background hex color |
ANSI pair factories for composition:
| Export | Description |
|---|---|
rgbCode(r, g, b) | Create foreground AnsiPair from RGB values |
bgRgbCode(r, g, b) | Create background AnsiPair from RGB values |
hexCode(hex) | Create foreground AnsiPair from hex string |
bgHexCode(hex) | Create background AnsiPair from hex string |
parseHex(hex) | Parse a hex color string into [r, g, b] tuple |
Hyperlinks (OSC 8)
| Export | Description |
|---|---|
link(text, url, options?) | Wrap text in OSC 8 hyperlink escape sequences |
linkCode(url, options?) | Create an OSC 8 open/close pair for composition |
Style Engine
| Export | Description |
|---|---|
applyStyle(text, pair) | Apply an ANSI pair with nesting-safe composition |
composeStyles(...pairs) | Compose multiple ANSI pairs into one |
Capability Detection
| Export | Description |
|---|---|
resolveColorCapability(mode, overrides?) | Resolve whether colors should be emitted |
resolveModifierCapability(mode, overrides?) | Resolve whether non-color modifiers (bold, italic, etc.) should be emitted |
resolveTrueColorCapability(mode, overrides?) | Resolve whether truecolor (24-bit) sequences should be emitted |
Text Utilities
| Export | Description |
|---|---|
visibleWidth(text) | Compute visible width (ANSI-aware, CJK-aware) |
wrapText(text, width, options?) | Wrap text by visible width with style continuity |
padStart(text, width, char?) | Left-pad to visible width |
padEnd(text, width, char?) | Right-pad to visible width |
center(text, width, char?) | Center-align to visible width |
To strip ANSI escape sequences from a string, use Bun's built-in Bun.stripANSI(text) function.
Block Helpers
| Export | Description |
|---|---|
unorderedList(items, options?) | Format an unordered list with bullet markers |
orderedList(items, options?) | Format an ordered list with number markers |
taskList(items, options?) | Format a task list with check markers |
table(headers, rows, options?) | Format a column-aligned table |
Markdown Theme
| Export | Description |
|---|---|
defaultTheme | Default auto-mode markdown theme |
createMarkdownTheme(options?) | Create a theme with optional overrides |
buildDefaultMarkdownTheme(style) | Build a default theme from a style instance |
Types
| Type | Description |
|---|---|
AnsiPair | Open/close ANSI escape sequence pair |
ColorMode | "auto" | "always" | "never" |
StyleOptions | Options for createStyle |
CapabilityOverrides | Injectable TTY/NO_COLOR overrides for testing |
TrueColorOverrides | Injectable COLORTERM/TERM overrides for truecolor testing |
HyperlinkOptions | Optional OSC 8 hyperlink parameters such as id |
StyleFn | (text: string) => string style function |
StyleInstance | Configured style object with all styling methods. Exposes enabled, colorsEnabled, and trueColorEnabled capability flags |
WrapOptions | Options for wrapText |
ColumnAlignment | "left" | "right" | "center" |
TableOptions | Options for table |
UnorderedListOptions | Options for unorderedList |
OrderedListOptions | Options for orderedList |
TaskListItem | { text: string; checked: boolean } |
TaskListOptions | Options for taskList |
MarkdownTheme | Semantic theme contract with 30 GFM slots |
PartialMarkdownTheme | Partial override type for theme customization |
ThemeSlotFn | (value: string) => string theme slot function |
CreateMarkdownThemeOptions | Options for createMarkdownTheme |
Usage
Basic Styling
import { bold, red, italic, style } from "@crustjs/style";
console.log(bold("Build succeeded"));
console.log(red("Error: missing argument"));
console.log(italic("note: check your config"));
console.log(style.bold.red("Critical failure"));By default:
- Color helpers respect
stdout.isTTYandNO_COLOR NO_COLORdisables color only when it is present and non-empty- Modifier helpers such as
bold()anditalic()are not disabled byNO_COLOR
Mode-Aware Styling
import { createStyle } from "@crustjs/style";
// Auto-detect terminal capabilities (default)
const s = createStyle();
console.log(s.bold("hello"));
// Force colors on
const color = createStyle({ mode: "always" });
console.log(color.red("always red"));
console.log(color.bold.red("always bold red"));
// Plain text output
const plain = createStyle({ mode: "never" });
console.log(plain.red("error")); // "error" (no ANSI)Runtime Color Overrides
The default style export and all named helpers (red, bold, etc.) re-resolve their color mode on every call, so you can override it globally at runtime with setGlobalColorMode / getGlobalColorMode:
import { getGlobalColorMode, setGlobalColorMode, style } from "@crustjs/style";
// Force colors on
setGlobalColorMode("always");
console.log(style.red("always red"));
// Disable colors but keep bold / italic / underline / hyperlinks
setGlobalColorMode("never");
console.log(style.bold.red("bold, but no red"));
// Revert to auto (respect TTY + NO_COLOR)
setGlobalColorMode(undefined);
// Read the current override (ColorMode | undefined)
getGlobalColorMode();Instances returned by createStyle() capture their mode at creation time and are not affected by setGlobalColorMode. The override only applies to the default style facade and the named re-exports.
For a scoped override that only lasts for a single CLI run, use noColorPlugin().
Composing Styles
import { applyStyle, composeStyles, boldCode, redCode } from "@crustjs/style";
const boldRed = composeStyles(boldCode, redCode);
console.log(applyStyle("critical", boldRed));Dynamic Colors (Truecolor)
Use any RGB or hex color via 24-bit truecolor ANSI sequences:
import { rgb, bgRgb, hex, bgHex } from "@crustjs/style";
// RGB values (0–255 per channel)
console.log(rgb("ocean", 0, 128, 255));
console.log(bgRgb("warning", 255, 128, 0));
// Hex colors (#RGB or #RRGGBB)
console.log(hex("error", "#ff0000"));
console.log(bgHex("highlight", "#ff8800"));Pair factories work with applyStyle and composeStyles:
import { rgbCode, hexCode, applyStyle, composeStyles, boldCode } from "@crustjs/style";
const coral = rgbCode(255, 127, 80);
console.log(applyStyle("coral text", coral));
const boldCoral = composeStyles(boldCode, hexCode("#ff7f50"));
console.log(applyStyle("bold coral", boldCoral));On createStyle instances, dynamic colors respect mode and truecolor detection:
import { createStyle } from "@crustjs/style";
const s = createStyle({ mode: "always" });
console.log(s.rgb("text", 255, 0, 0));
console.log(s.hex("text", "#ff0000"));In "auto" mode, dynamic colors are emitted only when the terminal supports truecolor and base colors are enabled — detected via COLORTERM=truecolor|24bit or TERM containing truecolor, 24bit, or -direct. When truecolor is not detected, dynamic color methods return plain text while standard 16-color methods continue to work normally.
Invalid inputs throw immediately: RangeError for out-of-range RGB values, TypeError for malformed hex strings.
Dynamic colors use truecolor (24-bit) ANSI sequences with no automatic fallback to 256 or 16 colors. On unsupported terminals, colors may be approximated or silently ignored — no runtime errors will occur.
Hyperlinks (OSC 8)
import { createStyle, linkCode, composeStyles, applyStyle, underlineCode } from "@crustjs/style";
const s = createStyle({ mode: "always" });
console.log(s.link("Crust docs", "https://crustjs.com"));
console.log(s.link("API reference", "https://crustjs.com/docs", { id: "docs-link" }));
const underlinedLink = composeStyles(linkCode("https://crustjs.com"), underlineCode);
console.log(applyStyle("Visit Crust", underlinedLink));In "auto" mode, hyperlinks are emitted when stdout is a TTY. They are not disabled by NO_COLOR, since OSC 8 links are not color sequences. In "never" mode, link helpers return plain text.
Terminal support for OSC 8 varies by emulator. Many modern terminals support it, but there is no reliable cross-terminal capability probe yet, so unsupported terminals may simply show plain text without clickable behavior.
Text Layout
import { visibleWidth, wrapText, padEnd, table } from "@crustjs/style";
// Measure and wrap styled text
const width = visibleWidth(styledText);
const wrapped = wrapText(longText, 60);
// Formatted table output
const output = table(
["Command", "Description"],
[
["build", "Build all packages"],
["test", "Run test suite"],
],
);
console.log(output);Markdown Theme
import { defaultTheme, createMarkdownTheme } from "@crustjs/style";
// Use the default theme
console.log(defaultTheme.heading1("Getting Started"));
console.log(defaultTheme.strong("important"));
console.log(defaultTheme.inlineCode("bun install"));
// Customize specific slots
const custom = createMarkdownTheme({
style: { mode: "always" },
overrides: {
heading1: (text) => `# ${text.toUpperCase()}`,
},
});The markdown theme controls how links look, but it does not create OSC 8 hyperlinks by itself. When your renderer has both the link label and destination URL, use style.link() or linkCode() to emit a clickable link.
Design
@crustjs/style is the presentation layer for Crust terminal output. It is parser-agnostic — markdown parsing, AST transformation, and syntax highlighting belong to separate consumer packages. This package provides the styling and layout primitives that those packages build on.
The architecture follows four layers:
- Styling Core — ANSI code mapping, nesting-safe application, and composition
- Capability Layer — Runtime color mode resolution (
auto/always/never) with truecolor detection - Layout Layer — ANSI-aware width, wrapping, padding, and block formatting
- Theme Layer — Semantic style slots for GFM constructs
The NO_COLOR environment variable and non-TTY detection are respected in auto mode for colors. NO_COLOR="" does not disable color, and non-color modifiers remain available even when NO_COLOR=1.