Crust logoCrust

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/style

Exports

Style Instance

ExportDescription
styleDefault auto-mode style instance
createStyle(options?)Create a configured style instance

Modifiers

ExportDescription
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:

ExportDescription
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:

ExportDescription
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

Style Engine

ExportDescription
applyStyle(text, pair)Apply an ANSI pair with nesting-safe composition
composeStyles(...pairs)Compose multiple ANSI pairs into one

Capability Detection

ExportDescription
resolveCapability(mode, overrides?)Resolve whether colors should be emitted
resolveTrueColor(mode, overrides?)Resolve whether truecolor (24-bit) sequences should be emitted

Text Utilities

ExportDescription
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

ExportDescription
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

ExportDescription
defaultThemeDefault auto-mode markdown theme
createMarkdownTheme(options?)Create a theme with optional overrides
buildDefaultMarkdownTheme(style)Build a default theme from a style instance

Types

TypeDescription
AnsiPairOpen/close ANSI escape sequence pair
ColorMode"auto" | "always" | "never"
StyleOptionsOptions for createStyle
CapabilityOverridesInjectable TTY/NO_COLOR overrides for testing
TrueColorOverridesInjectable COLORTERM/TERM overrides for truecolor testing
StyleFn(text: string) => string style function
StyleInstanceConfigured style object with all styling methods
WrapOptionsOptions for wrapText
ColumnAlignment"left" | "right" | "center"
TableOptionsOptions for table
UnorderedListOptionsOptions for unorderedList
OrderedListOptionsOptions for orderedList
TaskListItem{ text: string; checked: boolean }
TaskListOptionsOptions for taskList
MarkdownThemeSemantic theme contract with 30 GFM slots
PartialMarkdownThemePartial override type for theme customization
ThemeSlotFn(value: string) => string theme slot function
CreateMarkdownThemeOptionsOptions 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"));

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)

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 — 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.

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()}`,
  },
});

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:

  1. Styling Core — ANSI code mapping, nesting-safe application, and composition
  2. Capability Layer — Runtime color mode resolution (auto/always/never) with truecolor detection
  3. Layout Layer — ANSI-aware width, wrapping, padding, and block formatting
  4. Theme Layer — Semantic style slots for GFM constructs

The NO_COLOR environment variable and non-TTY detection are respected in auto mode. All APIs are pure and side-effect free.

On this page