Every component is imported from "terminaltui" and returns a content block. Content blocks are always used inside an array:
content: [markdown("Hello"), card({ title: "World" })]Renders text with markdown formatting (bold, italic, inline code, code blocks). Not focusable.
markdown("This is **bold** and *italic* with `inline code`.")A bordered card. Focusable -- each card is an individual navigation target.
| Prop | Type | Description |
|---|---|---|
title |
string |
Card heading (required) |
subtitle |
string |
Secondary text (price, date, stars) |
body |
string |
Body text |
tags |
string[] |
Tags rendered as badges |
url |
string |
URL opened on Enter |
border |
BorderStyle |
Override default border style |
action |
CardAction |
Action on select |
CardAction supports onPress, navigate (page ID), params (route params), confirm (confirmation prompt), label, and style ("primary", "secondary", "danger").
card({
title: "My Project",
subtitle: "★ 200",
body: "A brief description of the project.",
tags: ["TypeScript", "Open Source"],
url: "https://github.com/user/repo",
action: { navigate: "project-detail", params: { id: "my-project" } },
})Vertical timeline with connected dots. Each item is focusable but display-only (no action on Enter). For browsable items, prefer card() blocks instead.
| Item Prop | Type | Description |
|---|---|---|
title |
string |
Entry heading |
subtitle |
string |
Organization or company |
period |
string |
Time range |
description |
string |
Details |
timeline([
{ title: "Senior Engineer", subtitle: "Acme Corp", period: "2023 -- present", description: "Platform team lead" },
{ title: "BS Computer Science", subtitle: "University", period: "2017 -- 2021" },
])A bordered data table. Not focusable.
table(
["Plan", "Price", "Features"],
[
["Free", "$0/mo", "Basic features"],
["Pro", "$10/mo", "Everything + priority support"],
]
)A styled list. Not focusable. Styles: "bullet" (default), "number", "dash", "check", "arrow".
list(["First item", "Second item", "Third item"], "check")Block quote with optional attribution. Not focusable.
quote("The best way to predict the future is to invent it.", "-- Alan Kay")Large hero section. Focusable if cta is set (opens URL on Enter).
| Prop | Type | Description |
|---|---|---|
title |
string |
Large heading |
subtitle |
string |
Description |
cta |
{ label, url } |
Call-to-action link |
art |
string |
Custom ASCII art |
hero({
title: "Welcome",
subtitle: "Build beautiful terminal apps.",
cta: { label: "Get Started", url: "https://example.com" },
})Grid of cards. Items use the same shape as card().
gallery([
{ title: "Photo 1", body: "Description", tags: ["nature"] },
{ title: "Photo 2", body: "Description", tags: ["urban"] },
])Tabbed content. Focusable as one block -- Enter cycles through tabs. Best for mutually exclusive views of the same data, not for organizing sequential page sections (use divider() for that).
tabs([
{ label: "Frontend", content: [list(["React", "Vue", "Svelte"], "check")] },
{ label: "Backend", content: [list(["Node.js", "Python", "Go"], "check")] },
])Collapsible sections. Each item is separately focusable. Enter toggles open/close.
accordion([
{ label: "What is terminaltui?", content: [markdown("A framework for building terminal websites.")] },
{ label: "How do I deploy?", content: [markdown("Run `terminaltui build` then `npm publish`.")] },
])A clickable link. Focusable -- opens URL in the user's browser on Enter.
Options: icon (single character displayed before the label).
link("GitHub", "https://github.com/user")
link("Email", "mailto:hello@example.com", { icon: "✉" })Progress bar with percentage. Max defaults to 100. Not focusable.
progressBar("Project Alpha", 7, 10)
progressBar("Completion", 65)skillBar(label, value) is a shorthand for progressBar(label, value, 100):
skillBar("TypeScript", 90)
skillBar("Rust", 75)Inline badge or tag. Not focusable. Color is a hex string.
badge("v2.0")
badge("NEW", "#50fa7b")Renders an image in the terminal. Not focusable.
Options: width (number), mode ("ascii", "braille", "blocks").
image("./logo.png")
image("./photo.jpg", { width: 60, mode: "braille" })Groups content under a titled section header with a divider line. Not focusable itself -- children inherit their own focusability.
section("Appetizers", [
card({ title: "Bruschetta", subtitle: "$12", body: "Toasted bread with tomatoes" }),
])Horizontal divider line. Not focusable. Styles: "solid", "dashed", "dotted", "double", "label". If the first argument is not a known style, it becomes a label automatically.
divider() // solid line
divider("dashed") // dashed line
divider("My Section") // labeled divider (auto-detected)Vertical whitespace. Defaults to 1 line. Not focusable.
spacer() // 1 blank line
spacer(3) // 3 blank linesAll inputs are focusable. In navigation mode, press Enter to start editing, Escape to return to navigation.
| Prop | Type | Description |
|---|---|---|
id |
string |
Unique input ID (required) |
label |
string |
Label text (required) |
placeholder |
string |
Placeholder text |
defaultValue |
string |
Initial value |
maxLength |
number |
Max character count |
validate |
(value) => string | null |
Return error message or null |
mask |
boolean |
Mask input (for passwords) |
transform |
(value) => string |
Transform input on change |
textInput({ id: "name", label: "Your Name", placeholder: "Enter name...", maxLength: 50 })
textInput({ id: "password", label: "Password", mask: true })Same as textInput plus rows (visible rows). Supports multi-line editing.
textArea({ id: "bio", label: "Bio", placeholder: "Tell us about yourself...", rows: 4, maxLength: 500 })Dropdown select. Enter opens the dropdown, arrow keys pick an option.
| Prop | Type | Description |
|---|---|---|
id |
string |
Unique input ID (required) |
label |
string |
Label text (required) |
options |
{ label, value }[] |
Options array (required) |
defaultValue |
string |
Initial selected value |
placeholder |
string |
Placeholder text |
onChange |
(value) => void |
Change handler |
select({
id: "color",
label: "Favorite Color",
options: [{ label: "Red", value: "red" }, { label: "Blue", value: "blue" }],
onChange: (val) => console.log("Selected:", val),
})Toggle checkbox. Enter or Space toggles.
checkbox({ id: "agree", label: "I agree to the terms", onChange: (val) => console.log(val) })Toggle switch with on/off labels. Enter or Space toggles.
toggle({ id: "dark", label: "Dark Mode", onLabel: "ON", offLabel: "OFF", defaultValue: true })Radio button group. Enter starts selection, arrow keys move between options.
radioGroup({
id: "plan",
label: "Select Plan",
options: [{ label: "Free", value: "free" }, { label: "Pro", value: "pro" }],
defaultValue: "free",
})Numeric input. Left/Right arrow keys change the value.
numberInput({ id: "qty", label: "Quantity", defaultValue: 1, min: 1, max: 99, step: 1 })Search with filtering. Type to filter, arrows to pick, Enter to select. Set action: "navigate" to navigate to the selected value as a page ID.
searchInput({
id: "search",
placeholder: "Search pages...",
items: [
{ label: "About", value: "about", keywords: ["bio", "info"] },
{ label: "Projects", value: "projects", keywords: ["work", "code"] },
],
action: "navigate",
})A pressable button. Enter triggers the action.
button({ label: "Submit", style: "primary", onPress: async () => { /* ... */ } })Styles: "primary", "secondary", "danger".
Groups input fields and a submit button. On submit, collects all field values by their id.
The onSubmit handler must return an ActionResult: { success: "..." }, { error: "..." }, or { info: "..." }.
form({
id: "contact",
onSubmit: async (data) => {
await sendEmail(data.name, data.email, data.message);
return { success: "Message sent!" };
},
fields: [
textInput({ id: "name", label: "Name" }),
textInput({ id: "email", label: "Email" }),
textArea({ id: "message", label: "Message", rows: 4 }),
button({ label: "Send", style: "primary" }),
],
})Layout components divide the terminal into panels. See layouts.md for full documentation.
Side-by-side panels. Each panel gets a width ("50%", 30, or omit for equal split).
columns([
panel({ width: "60%", content: [table(["Name", "Status"], [["nginx", "running"]])] }),
panel({ width: "40%", content: [progressBar("CPU", 45)] }),
])Vertically stacked panels with height ("50%", 10, or omit for equal split).
rows([
panel({ height: "40%", content: [markdown("## Top")] }),
panel({ height: "60%", content: [markdown("## Bottom")] }),
])N×M grid. cols: columns. gap: character gap (default 1). items: array of panel() configs.
grid({ cols: 2, items: [
panel({ title: "CPU", content: [progressBar("Usage", 45)] }),
panel({ title: "Mem", content: [progressBar("RAM", 72)] }),
]})Bordered content area with optional title, border, padding, width, height.
panel({ title: "Stats", border: "rounded", padding: 1, content: [progressBar("CPU", 45)] })12-column grid system. span is 1-12 (default: auto). Responsive breakpoints: xs, sm, md, lg.
row([
col([sidebar], { span: 3 }),
col([content], { span: 9 }),
], { gap: 1 })Centers content with optional maxWidth and padding.
container([row([...])], { maxWidth: 100, padding: 2 })Navigation: Arrow keys navigate spatially between focusable items in any layout. No configuration needed.
Reactive content block that re-renders when state changes. Optionally specify dependency keys to limit re-renders.
dynamic(() => markdown(`Count: ${state.get("count")}`))
dynamic(["count"], () => markdown(`Count: ${state.get("count")}`))Lazily-loaded async content with loading and fallback states.
asyncContent({
load: async () => {
const data = await fetchData();
return [card({ title: data.name, body: data.description })];
},
loading: "Loading data...",
fallback: [markdown("Failed to load.")],
})You can register custom component renderers using the componentRegistry:
import { componentRegistry } from "terminaltui";
import type { RenderContext } from "terminaltui";
componentRegistry.register("myWidget", (block, ctx: RenderContext) => {
// Return an array of ANSI-styled lines
return [` ${block.label}: ${block.value}`];
}, true); // true = focusableUse your custom component in content arrays by creating a block with a matching type:
content: [
{ type: "myWidget", label: "Score", value: "42" } as any,
]For architecture details and how to contribute new built-in components, see ARCHITECTURE.md.