Terminal

A command line for your app: a title bar, a monospace scrollback and a prompt with ghost completion and a block cursor. Presentational parts on the theme's tokens; your app runs the commands, so it fits a dev console, a search console or a REPL alike.

Koala shellzsh
Koala shell 1.0
Type 'help' to see the commands. Tab completes, ↑ ↓ walk the history.
koala ~ $
Tab complete↑↓ historyEsc clear the line

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add terminal

Usage#

The parts paint; the loop is yours. A working shell is a list of lines, the text being typed, and a key handler that turns Enter into output.

usage.tsxTSX
import * as React from "react"import {  Terminal,  TerminalField,  TerminalInputLine,  TerminalLine,  TerminalPrompt,  TerminalScreen,  type TerminalTone,} from "@/components/ui/terminal"type Line = { id: number; text: string; tone: TerminalTone }export function Example() {  const [lines, setLines] = React.useState<Line[]>([])  const [input, setInput] = React.useState("")  const run = (command: string) => {    if (command === "clear") return setLines([])    setLines((l) => [      ...l,      { id: l.length, text: command, tone: "in" },      { id: l.length + 1, text: `you said: ${command}`, tone: "out" },    ])  }  return (    <Terminal className="h-80">      <TerminalScreen>        {lines.map((line) => (          <TerminalLine key={line.id} tone={line.tone}>            {line.tone === "in" && <TerminalPrompt prefix="app" />}            {line.text}          </TerminalLine>        ))}        <TerminalInputLine>          <TerminalPrompt prefix="app" />          <TerminalField            aria-label="Command"            value={input}            onChange={(e) => setInput(e.target.value)}            onKeyDown={(e) => {              if (e.key !== "Enter") return              run(input.trim())              setInput("")            }}          />        </TerminalInputLine>      </TerminalScreen>    </Terminal>  )}

Anatomy#

import {  Terminal,  TerminalBar,  TerminalControls,  TerminalField,  TerminalIndicator,  TerminalInputLine,  TerminalLine,  TerminalPrompt,  TerminalScreen,  TerminalTitle,  TerminalTitleTag,  TerminalTray,  TerminalWatermark,} from "@/components/ui/terminal"<Terminal>  {/* A faint stamp behind the scrollback */}  <TerminalWatermark />  {/* The title bar, and the drag handle */}  <TerminalBar>    <TerminalIndicator />    <TerminalTitle>      <TerminalTitleTag />    </TerminalTitle>    <TerminalControls />  </TerminalBar>  {/* The scrollback, a ScrollArea */}  <TerminalScreen>    <TerminalLine />    <TerminalInputLine>      <TerminalPrompt />      <TerminalField />    </TerminalInputLine>  </TerminalScreen>  {/* Live results, suggestions, a status line */}  <TerminalTray /></Terminal>

Line tones#

tone sets a line's ink. in is a command that was run, in the foreground ink; out (the default) and raw are output in the reading ink; dim is a comment in the meta grey; err and ok take the status hues' text step, which still reads on the light themes.

ci $ npm test
# 3 suites, 41 tests
PASS components/button.test.tsx
PASS components/terminal.test.tsx
FAIL components/chart.test.tsx: expected 12 bars, got 11
Tests: 1 failed, 40 passed, 41 total
Snapshots: 18 passed

Clickable lines#

Give a line onClick and it becomes a button: focusable, run by a click, Enter or Space, with a highlight that keeps the text on its column. For search results, a file listing, or a #N the user would otherwise have to type.

DISPATCH> units --active
# CALL SIGN STATUS
1 ADAM-12 Available
2 LINCOLN-4 En route
3 MARY-7 On scene
Click a unit, or Tab to it and press Enter.

Prompt, completion and cursor#

TerminalPrompt takes a prefix and a mark ("> " by default). TerminalField paints what is typed and a block cursor at the caret: it follows the arrow keys and sits over the character it is on. ghost is the grey completion after it; accepting it is your key handler's job (Tab or → here, at the end of the line). The cursor is a solid block while the field has focus and a hollow cell when it doesn't; drive cursorVisible from a timer to make it blink.

acme λ

A blink that restarts lit on every keystroke, and holds still under reduced motion:

function BlinkingField(props: Omit<TerminalFieldProps, "cursorVisible">) {  const [beat, setBeat] = React.useState({ value: props.value, lit: true })  if (beat.value !== props.value) setBeat({ value: props.value, lit: true })  React.useEffect(() => {    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return    const timer = setInterval(() => setBeat((b) => ({ ...b, lit: !b.lit })), 530)    return () => clearInterval(timer)  }, [props.value])  return <TerminalField cursorVisible={beat.lit} {...props} />}

Tray#

TerminalTray is the band under the scrollback, on a quieter tint: live results while the user types, suggestions, a status line. It holds Koala components (a Badge, a Kbd, a Command list) or clickable TerminalLines, as here.

Type a name: the matches show up under the scrollback.
FIND>
#1Ada LovelaceAL-1815
#2Alan TuringAT-1912
#3Hedy LamarrHL-1914
db #
postgres · 20 rows · 4 msEnter run

Window chrome#

TerminalBar carries the TerminalIndicator, a TerminalTitle (with an optional TerminalTitleTag) and TerminalControls, where buttons default to the 32px tier that fits the bar. What the controls do is yours: here they minimize and widen the window, and a double-click on the bar widens it too. movable gives the bar a grab cursor and dragging holds the grabbing one while your pointer handlers move the window. TerminalWatermark lays a faint stamp behind the scrollback.

Buildproduction
ci $ npm run build
▲ Next.js 16.3.6
Creating an optimized production build…
✓ Compiled successfully in 14.2s
Collecting page data and generating 412 static pages
✓ Done in 41.8s

API reference#

Terminal

The surface, a div: the overlay panel (popover fill, the xl shadow) every part reads its styles from. Every part is optional but the root. Forwards every div prop; every part merges className last. terminalVariants is the recipe.

PropTypeDefaultDescription
movablebooleanfalseThe bar shows a grab cursor. Moving the window is your pointer handler on TerminalBar.
draggingbooleanfalseThe grabbing cursor, and no text selection while it lasts.
Data attributeDescription
data-slot"terminal".
CSS variableDescription
--surfaceSet to var(--popover), so controls nested inside paint the panel's ground.

TerminalBar

The title bar, a div, and the drag handle: hang onPointerDown on it. Drop it for a console embedded in a page.

Data attributeDescription
data-slot"terminal-bar".

TerminalIndicator

The live dot before the title, a decorative span (aria-hidden).

Data attributeDescription
data-slot"terminal-indicator".

TerminalTitle

The title, a span that ends a long title in an ellipsis.

Data attributeDescription
data-slot"terminal-title".

TerminalTitleTag

The title's secondary label, a span after a middle dot. Goes inside TerminalTitle.

Data attributeDescription
data-slot"terminal-title-tag".

TerminalControls

The window buttons at the end of the bar, a div. Controls inside default to the sm tier (32px); an explicit size still wins.

Data attributeDescription
data-slot"terminal-controls".
data-control-size"sm".

TerminalScreen

The scrollback, on ScrollArea: takes its props but the axis (orientation and draggable are left out). Its content is a role="log", so assistive tech announces new lines.

PropTypeDefaultDescription
fadebooleanfalseFades the edge that still has content beyond it.
viewportRefReact.Ref<HTMLDivElement>-The scrolling element itself: use it to keep the scrollback at the bottom.
type"auto" | "always" | "scroll" | "hover""hover"When the scrollbars show, as on ScrollArea.
Data attributeDescription
data-slot"terminal-screen", on the log inside the scroller.

TerminalWatermark

A faint stamp behind the scrollback (a logo, a seal), a decorative img (aria-hidden, not draggable). Set its opacity with style. Forwards every img prop.

PropTypeDefaultDescription
altstring""Empty by default: the image is decorative.
Data attributeDescription
data-slot"terminal-watermark".

TerminalLine

One line of the scrollback, a div that wraps long text. It needs no Terminal around it. terminalLineVariants({ tone, interactive, className }) returns a line's classes for your own markup.

PropTypeDefaultDescription
tone"in" | "out" | "err" | "ok" | "dim" | "raw""out"The line's ink. The type is exported as TerminalTone.
onClick(e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>) => void-Makes the line a role="button" in the tab order, run by click, Enter or Space.
Data attributeDescription
data-slot"terminal-line".
data-toneThe line's tone.

TerminalInputLine

The prompt row, a div: the prompt and the field on one baseline. Keep it last in the screen so the prompt scrolls with the output, the way a shell does.

Data attributeDescription
data-slot"terminal-input-line".

TerminalPrompt

The prompt, a span: the prefix in brand ink, then the mark.

PropTypeDefaultDescription
prefix*React.ReactNode-What comes before the mark: the system, the host, the unit.
markReact.ReactNode"> "The prompt mark. Keep its trailing space.
Data attributeDescription
data-slot"terminal-prompt", with terminal-prompt-prefix and terminal-prompt-mark inside.

TerminalField

The prompt's field: painted text with a block cursor at the caret, under a transparent <input> that takes the keyboard. Every input prop goes to that input, className and ref included; spellcheck, autocomplete, autocapitalize and autocorrect are off unless you turn them on.

PropTypeDefaultDescription
value*string-The controlled text.
ghoststring-The grey completion painted after the caret when it is at the end. Your key handler accepts it, usually on Tab.
cursorVisiblebooleantrueThe blink beat while focused: drive it from a timer to blink, leave it true for a steady cursor. Out of focus the cursor is a still, hollow cell.
placeholderstring-Painted in the ghost's grey while the field is empty.
Data attributeDescription
data-slot"terminal-field" on the wrapper, "terminal-input" on the input, "terminal-cursor" on the cursor.
data-visibleOn the cursor: the cursorVisible beat.

TerminalTray

The band under the scrollback, a div: live results, suggestions, a status line. Drop it when nothing runs live.

Data attributeDescription
data-slot"terminal-tray".

FAQ#

Command filters a fixed list and runs the row you pick. Terminal runs whatever is typed and keeps a transcript. They combine: a Command list in the tray makes a fine live preview.