Skip to content
Component installs need the registry setup— the Base UI shadcn project, the @vegastack namespace and the Cloudflare Access service token.
VegaStack Design

Terminal

A dark mono command block with a phosphor prompt glyph and a fixed trailing copy action.

Status
stable
Since
0.1.0
Accessibility pattern
decorative prompt + real text

Last updated

Install
pnpm dlx shadcn@latest add @vegastack/button
✓ Installed 1 component

Install

Add Terminal from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.

pnpm dlx shadcn@latest add @vegastack/terminal

The same command installs the registry items it composes: @vegastack/copy-button, @vegastack/use-overflow.

Usage

import { Terminal } from "@/components/ui/terminal";

<Terminal
  title="Install"
  lines={[
    "pnpm dlx shadcn@latest add @vegastack/button",
    { output: "✓ Installed 1 component" },
  ]}
/>;

Terminal self-scopes to the marketing dark ground (.vs-marketing) — it reads correctly even embedded in a light-theme docs page (like this one), with no MarketingSurface wrapper required.

Anatomy

Terminal is a compound component. Every exported part, with the data-slot it renders (generated from the canonical source):

Terminal — data-slot="terminal" | "terminal-command-row" | "terminal-copy" | "terminal-header" | "terminal-line" | "terminal-prompt"
TerminalBody — data-slot="terminal-body"

Examples

Install
pnpm dlx shadcn@latest add @vegastack/button
✓ Installed 1 component

Multi-line, custom prompt

Each line is a plain string (a command) or { output: string } (dimmed, no prompt glyph). The prompt glyph — the sanctioned "terminal prompt glyph" --brand marker role — defaults to $ and is overridable.

Setup
pnpm install
pnpm run tokens:build
✓ tokens built: dist/theme.css
pnpm run registry:build

Copying

The command row composes the icon-only CopyButton at its inline end. The command pane scrolls independently, so the action remains visible even when a command is long. It copies every command line (not output lines), joined by newline, unless you pass an explicit copyValue; successful writes swap the copy icon for the standard success check.

API Reference

PropTypeDefaultDescription
lines*(string | TerminalLine)[]The lines to render, top to bottom. A plain string is shorthand for { command: string }. Content is fully caller-supplied (no timestamps, no randomness) — deterministic by construction, so it's VRT-stable.
aria-labelstringderived from `title`Accessible name for the scrollable command pane, which is a keyboard focus stop while it overflows. Overrides the default, which names the pane from the visible title. Mirrors ScrollArea: the label is intercepted here and applied to the focusable element, not to the outer block.
aria-labelledbystringthe visible title elementId of an element naming the scrollable command pane. Takes precedence over the visible title; ignored when aria-label is set.
copyValuestringderived from command linesValue copied by the trailing CopyButton. Defaults to every command line (not output lines) joined with newlines.
promptstring'$'The prompt glyph shown before each command line — the sanctioned "terminal prompt glyph" --brand marker role (audit 17-brand-direction §Color & surface).
titleReact.ReactNode'Terminal'Small mono uppercase label in the header bar.

Data attributes and CSS variables on Terminal

AttributeValues
data-slot"terminal" | "terminal-command-row" | "terminal-copy" | "terminal-header" | "terminal-line" | "terminal-prompt"

Accessibility

  • The prompt glyph is decorative (aria-hidden) — command text itself is real, readable text.
  • The composed CopyButton carries its own accessible name ("Copy command" / "Copied command") and announces the copy via a visually-hidden live region.
  • The command pane is keyboard-focusable while it actually scrolls, and not otherwise. A scrollable region that cannot be focused is unreachable without a pointer; a pane whose commands fit needs no tab stop, and adding one would put a dead stop in front of every keyboard user on a page full of one-line install snippets. The pane measures itself live (useOverflow), so the tab stop appears and disappears with the viewport width. Focus is shown as an inset outline: an outward one is not painted at all here, because the block's root is overflow-hidden and scroll-fade-x masks the pane to its own border box. A border tint is not an option either — forced-colors: active replaces border-color, which would leave the pane with no indicator in the forced palette.
  • The pane is always announced as a named group, labelled by the visible title — the name and the role do not move with the measurement, because a role that appeared and vanished under a screen reader as the window resized would be a worse defect than the tab stop it removed — so title="Install" reads as "Install, group". A bare focusable div would announce as an unnamed stop: it maps to the generic role, which prohibits naming, so an aria-label on it is not reliably exposed. group is deliberate rather than region, which is a landmark — several install snippets on one page should not add several landmarks. Override the name with aria-label or point aria-labelledby at your own element; both are applied to the pane, not the outer block.
  • Content is fully caller-supplied — no timestamps, no randomness — so the block is deterministic and safe for visual capture.
KeyAction
TabMove focus to the command pane when it scrolls (announced by its name), then on to the copy button.
/ Scroll a clipped command horizontally while the pane has focus.
Enter / SpaceCopy the command, when the copy button has focus.
ContractStates tested
Behaviourdefault, copied
Accessibilitybrowser-accessibility-test, focus-visible, keyboard, labeled, native-or-base-ui-semantics
Visualdefault

Do / Don't

Do
Use Terminal for install snippets and short command sequences with real, copyable text.
Don't
Put a Terminal inside a MarketingSurface expecting a visual change — it already self-scopes to the dark ground; nesting is harmless but redundant.

On this page