A terminal UI framework for building responsive, interactive, component-based CLI applications in TypeScript.
- Component-based architecture — build UIs with reusable, composable components
- Built-in components — TextBox, SelectList, ProgressBar, InputField, Table, Spinner, Tabs, Modal, Checkbox
- Functional components —
createComponent()factory, no class boilerplate needed - Responsive layout — percentage and fixed sizing, relative positioning, flex layout (row/column) with gap, justify, alignItems
- Min/Max constraints —
minWidth,maxWidth,minHeight,maxHeightfor responsive layouts - Styling — borders (single/double/rounded), colors (named + hex), padding, bold/dim/underline/italic/strikethrough/inverse
- Styled segments — multiple styles within a single line via
segments(StyledSegment[]) - Text alignment —
align: "left" | "center" | "right"per line - Text overflow — hidden, ellipsis, wrap, wrap-word modes (segment-aware)
- Unicode-aware — correct rendering of emoji, CJK characters, and zero-width joiners
- Keyboard input — focus management (Tab/Shift+Tab), event bubbling, declarative
onKeyPress - Mouse support — click, scroll, hover detection with
mouseenter/mouseleaveevents - Scrolling — vertical and horizontal scrolling with keyboard control and visual indicators
- Overlays — z-index layering,
showOverlay()/hideOverlay()for modals - Reactive state — app-level state store with
setState()/connect()for automatic re-renders - Animation system — built-in
app.animate()with easing functions (linear, easeIn, easeOut, bounce, elastic) - Color utilities —
gradient(),lighten(),darken(),mix()for programmatic color manipulation - Theme system — design tokens (
primary,danger,surface, etc.) with dark/light presets - Focus visible —
focusStylefor automatic visual focus indicators - Lifecycle hooks —
onMount,onDestroy,onResize - Performance — double-buffered rendering, diff-based updates, partial re-renders on prop changes
npm install @pompidup/cligrid
# or
yarn add @pompidup/cligrid
# or
pnpm add @pompidup/cligridimport { App, createComponent } from "@pompidup/cligrid";
const app = new App({ alternateScreen: true });
const hello = createComponent({
id: "hello",
position: { x: 0, y: 0 },
width: "100%",
height: 3,
margin: 0,
style: {
border: { style: "rounded", fg: "cyan" },
fg: "cyan",
padding: { left: 2 },
},
props: {},
render: () => "Hello from cligrid!",
});
app.add(hello);
app.onKey("ctrl+c", () => {
app.stop();
process.exit(0);
});
app.start();Displays text with optional borders, padding, and overflow handling.
import { TextBox } from "@pompidup/cligrid";
const box = new TextBox({
id: "info",
position: { x: 0, y: 0 },
width: "50%",
height: 5,
props: { text: "Hello\nWorld" },
style: { border: { style: "single" }, overflow: "wrap-word" },
});Navigable list with keyboard support (Up/Down to move, Enter to select).
import { SelectList } from "@pompidup/cligrid";
const menu = new SelectList({
id: "menu",
position: { x: 0, y: 0 },
width: 30,
height: 6,
props: {
items: ["New Game", "Load Game", "Settings", "Quit"],
selectedIndex: 0,
},
style: { border: { style: "rounded" } },
onSelect: (index, item) => {
console.log(`Selected: ${item}`);
},
});Horizontal progress indicator.
import { ProgressBar } from "@pompidup/cligrid";
const bar = new ProgressBar({
id: "loading",
position: { x: 0, y: 0 },
width: 40,
height: 3,
props: { value: 0.65, label: "Loading" },
style: { border: { style: "single", fg: "green" } },
});
// Renders: Loading [████████░░░░░░] 65%Text input with cursor, placeholder, and callbacks.
import { InputField } from "@pompidup/cligrid";
const input = new InputField({
id: "name",
position: { x: 0, y: 0 },
width: 30,
height: 3,
props: { value: "", placeholder: "Enter your name..." },
style: { border: { style: "single" } },
onChange: (value) => { /* called on each keystroke */ },
onSubmit: (value) => { /* called on Enter */ },
});Data table with column alignment, headers, and vertical scrolling.
import { Table } from "@pompidup/cligrid";
const table = new Table({
id: "users",
position: { x: 0, y: 0 },
width: 60,
height: 10,
scrollable: true,
props: {
columns: [
{ key: "name", label: "Name", width: 20 },
{ key: "role", label: "Role", width: 15 },
{ key: "status", label: "Status", width: 10, align: "right" },
],
rows: [
{ name: "Alice", role: "Admin", status: "Active" },
{ name: "Bob", role: "User", status: "Idle" },
],
},
style: { border: { style: "single" } },
});Animated loading indicator with multiple styles.
import { Spinner } from "@pompidup/cligrid";
const spinner = new Spinner({
id: "loader",
position: { x: 0, y: 0 },
width: 30,
height: 1,
props: { label: "Loading...", style: "dots" },
// Styles: "dots" | "line" | "arc" | "bouncingBar"
});Tab navigation with Left/Right keyboard control.
import { Tabs } from "@pompidup/cligrid";
const tabs = new Tabs({
id: "nav",
position: { x: 0, y: 0 },
width: 40,
height: 1,
props: {
tabs: [
{ label: "Home", id: "home" },
{ label: "Settings", id: "settings" },
],
activeTab: "home",
},
onTabChange: (id) => { /* switch content */ },
});Dialog with title, body, navigable buttons, and overlay support.
import { Modal } from "@pompidup/cligrid";
const modal = new Modal({
id: "confirm",
position: { x: 10, y: 5 },
width: 40,
height: 10,
style: { border: { style: "double" } },
props: {
title: "Confirm",
body: "Are you sure?",
buttons: [
{ label: "OK", action: () => app.hideOverlay(modal) },
{ label: "Cancel", action: () => app.hideOverlay(modal) },
],
},
});
app.showOverlay(modal); // z-index 100, focus trapToggle input with Space/Enter keyboard control.
import { Checkbox } from "@pompidup/cligrid";
const checkbox = new Checkbox({
id: "agree",
position: { x: 0, y: 0 },
width: 30,
height: 1,
props: { checked: false, label: "I agree to the terms" },
onChange: (checked) => { /* handle toggle */ },
});width: 30, // 30 columns
width: "50%", // 50% of terminal width
height: "auto", // computed from contentminWidth: 20, // minimum 20 columns
maxWidth: "80%", // maximum 80% of terminal width
minHeight: 5,
maxHeight: 30,Position components relative to each other:
const sidebar = new TextBox({
id: "sidebar",
position: { x: 0, y: 0 },
width: 24,
height: "100%",
// ...
});
const content = createComponent({
id: "content",
position: {
x: { position: "right", relativeTo: "sidebar" },
y: { position: 0 },
},
width: "70%",
height: "100%",
// ...
});Relative positions: "right", "left" (x-axis), "top", "bottom" (y-axis).
Distribute children automatically in rows or columns:
const row = createComponent({
id: "row",
position: { x: 0, y: 0 },
width: "100%",
height: 10,
layout: "row",
gap: 1,
justifyContent: "space-between", // "start" | "center" | "end" | "space-between" | "space-around"
alignItems: "center", // "start" | "center" | "end" | "stretch"
children: [
createComponent({ id: "a", width: 20, height: "auto", /* ... */ }),
createComponent({ id: "b", width: "100%", height: "auto", flex: 1, /* ... */ }),
],
// ...
});style: {
border: {
style: "single" | "double" | "rounded" | "none",
fg: "cyan", // border color
},
fg: "white", // text foreground (named, "#RRGGBB", or theme token)
bg: "blue", // text background
bold: true,
dim: true,
underline: true,
italic: true,
strikethrough: true,
inverse: true,
padding: { top: 1, right: 2, bottom: 1, left: 2 },
overflow: "hidden" | "ellipsis" | "wrap" | "wrap-word",
}Colors: black, red, green, yellow, blue, magenta, cyan, white, bright variants (brightRed, ...), hex ("#FF00FF"), and theme tokens ("primary", "danger", etc.).
Apply different styles to parts of the same line using segments:
render: () => [
{
text: "",
segments: [
{ text: " INFO ", style: { bg: "blue", fg: "white", bold: true } },
{ text: " Server started on ", style: { fg: "white" } },
{ text: ":3000", style: { fg: "cyan", bold: true } },
],
},
]Align text within a component's content area:
render: () => [
{ text: "Left-aligned (default)" },
{ text: "Centered title", align: "center" },
{ text: "Right-aligned value", align: "right" },
]Register focusable components and navigate with Tab/Shift+Tab:
app.add(input1, true); // true = focusable
app.add(input2, true);
app.add(label, false); // not focusable (default)Use focusStyle for automatic visual focus indicators:
const comp = createComponent({
id: "panel",
focusStyle: { fg: "yellow", bold: true }, // applied when focused
// ...
});Global key handlers:
app.onKey("ctrl+c", () => { app.stop(); process.exit(0); });
app.onKey("ctrl+s", () => { /* save */ });Component-level key handlers:
const comp = createComponent({
id: "panel",
// ...
onKeyPress: (event, component) => {
if (event.key === "enter") {
event.handled = true; // stop bubbling
}
},
});Events bubble up from child to parent until event.handled = true.
Enable mouse tracking for click, scroll, and hover events:
const app = new App({ mouse: true });
const button = createComponent({
id: "btn",
// ...
});
button.on("click", (event) => { /* { x, y, button, type } */ });
button.on("mouseenter", () => { /* hover start */ });
button.on("mouseleave", () => { /* hover end */ });
button.on("scroll", (event) => { /* { direction: -1 | 1 } */ });Components receive hovered: boolean in RenderContext to adapt their rendering.
Animate component props with easing functions:
import { easeOut, bounce } from "@pompidup/cligrid";
app.animate(progressBar, { value: 1.0 }, {
duration: 2000,
easing: easeOut,
onComplete: () => { /* done */ },
});
// Frame-based callbacks
const unsubscribe = app.tick((dt) => { /* called each frame */ });Available easings: linear, easeIn, easeOut, easeInOut, bounce, elastic.
import { gradient, lighten, darken, mix } from "@pompidup/cligrid";
const colors = gradient("#FF0000", "#0000FF", 10); // 10-step gradient
const lighter = lighten("#333333", 0.3);
const darker = darken("#CCCCCC", 0.2);
const blended = mix("#FF0000", "#00FF00", 0.5);Use design tokens for consistent styling across components:
import { darkTheme, lightTheme } from "@pompidup/cligrid";
app.setTheme(darkTheme);
// Use token names as colors
const comp = createComponent({
id: "panel",
style: { fg: "primary", bg: "surface", border: { style: "single", fg: "border" } },
// ...
});
// Switch theme at runtime
app.setTheme(lightTheme);Tokens: primary, secondary, danger, success, warning, surface, text, border, muted, accent.
app.setState({ score: 0, level: 1 });
const display = createComponent({
id: "score",
props: { score: 0 },
render: (props) => `Score: ${props.score}`,
// ...
});
app.add(display);
app.connect(display, (state) => ({ score: state.score }));
// Later: auto-updates the component
app.setState({ score: 42 });Vertical and horizontal scrolling with keyboard and mouse support:
const list = createComponent({
id: "log",
scrollable: true, // Up/Down for vertical, Left/Right for horizontal
// ...
});
app.add(list, true); // focusable for keyboard control
// Programmatic scrolling
list.scrollTo(10); list.scrollBy(5); // vertical
list.scrollToX(10); list.scrollByX(5); // horizontalVisual scroll indicators (track + thumb) appear when content overflows.
app.showOverlay(modal); // z-index 100, saves previous focus
app.hideOverlay(modal); // restores focusconst comp = createComponent({
id: "comp",
// ...
onMount: () => { /* first render */ },
onDestroy: () => { /* removed */ },
});
comp.on("resize", (width, height) => { /* dimensions changed */ });The package includes 6 runnable demos showcasing the library features:
pnpm demo:dashboard # Interactive dashboard with navigation menu
pnpm demo:form # Form with input fields, select list, focus management
pnpm demo:advanced # State store, scrolling, overlays, live updates
pnpm demo:castle # ASCII art castle with lightning animation
pnpm demo:segments # Styled segments, text alignment, segment overflow
pnpm demo:showcase # v2 showcase: Table, Tabs, Spinner, Modal, Checkbox, themes, mouse| Export | Description |
|---|---|
App |
Main application orchestrator (manages rendering, input, focus, state, themes, mouse) |
createComponent() |
Functional component factory |
Component |
Base class for custom components |
Template |
Layout manager |
Renderer |
Terminal rendering engine |
| Export | Description |
|---|---|
TextBox |
Multi-line text display |
SelectList |
Navigable list with selection |
ProgressBar |
Horizontal progress indicator |
InputField |
Text input with cursor |
Table |
Data table with column alignment and scroll |
Spinner |
Animated loading indicator |
Tabs |
Tab navigation with keyboard control |
Modal |
Dialog with buttons and overlay support |
Checkbox |
Toggle input |
| Export | Description |
|---|---|
Animator |
Animation engine |
linear, easeIn, easeOut, easeInOut, bounce, elastic |
Easing functions |
gradient(), lighten(), darken(), mix() |
Color manipulation |
darkTheme, lightTheme, resolveThemeColor() |
Theme system |
| Export | Description |
|---|---|
stylize() |
Apply ANSI styles to text |
fgCode() / bgCode() |
Get ANSI color codes |
stringWidth() |
Unicode-aware string width |
InputManager |
Low-level keyboard and mouse input |
FocusManager |
Focus navigation |
ScreenBuffer |
Double-buffered screen |
Style, ComponentConfig, RenderContext, RenderOutput, RenderLine, StyledSegment, TextAlign, Position, Size, KeyEvent, MouseEvent, Theme, TerminalDimensions, StyleAttrs
MIT