Terminal frame
A terminal window with prompt lines, coloured with the code tokens.
npx shadcn@latest add https://ui.goodtabs.co/r/terminal-frame.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/terminal-frame
~/goodtabs
npx shadcn add @goodtabs/hero
Fetching registry
Added components/ui/hero.tsx
A command and two lines of output in a narrow window.
Build output
zsh
npm run build
Compiled successfully in 4.2s
Generating static pages (128/128)
Route (app) Size
/ 3.1 kB
/components/[name] 8.4 kB
A longer session ending in an empty prompt.
Usage
import { TerminalFrame, TerminalLine } from "@/components/ui/terminal-frame"
<TerminalFrame title="~/app">
<TerminalLine prompt="$">npm run dev</TerminalLine>
<TerminalLine kind="string">Ready in 312ms</TerminalLine>
</TerminalFrame>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | Centered in the title bar, for example a path or shell name. | |
| maxWidth | string | Constrain the window, for example "480px". | |
| prompt | string | On TerminalLine: a leading marker such as $, hidden from assistive tech. | |
| kind | "plain" | "name" | "keyword" | "string" | "comment" | On TerminalLine: picks the matching --code-* colour. |
Source
terminal-frame/terminal-frame.tsx
import * as React from "react";
import { cn } from "@/lib/utils";
type TerminalFrameProps = Omit<React.ComponentProps<"div">, "title"> & {
/** Shown in the title bar, for example a path or a shell name. */
title?: string;
/** Constrain the window, for example "480px". */
maxWidth?: string;
};
/**
* A terminal window. A darker body than a code window, with the --code-*
* tokens for output. Use TerminalLine for one line with an optional prompt.
*/
export function TerminalFrame({ title, maxWidth, className, style, children, ...props }: TerminalFrameProps) {
return (
<div
className={cn("w-full overflow-hidden rounded-lg border bg-muted/40", className)}
style={{ maxWidth, ...style }}
{...props}
>
<div className="flex h-9 items-center gap-2 border-b px-3">
<div className="flex items-center gap-1.5" aria-hidden="true">
<span className="size-2.5 rounded-full bg-muted-foreground/30" />
<span className="size-2.5 rounded-full bg-muted-foreground/30" />
<span className="size-2.5 rounded-full bg-muted-foreground/30" />
</div>
{title && <span className="mx-auto truncate pr-10 font-mono text-xs text-foreground/70">{title}</span>}
</div>
<div
tabIndex={0}
role="region"
aria-label={title ?? "Terminal"}
className="flex flex-col gap-1 overflow-x-auto p-4 font-mono text-xs/5 whitespace-pre text-(--code-plain)"
>
{children}
</div>
</div>
);
}
const tone = {
plain: "text-(--code-plain)",
name: "text-(--code-name)",
keyword: "text-(--code-keyword)",
string: "text-(--code-string)",
comment: "text-(--code-comment)",
} as const;
/** One line of a terminal. Set prompt to show a leading marker on a command. */
export function TerminalLine({
prompt,
kind = "plain",
className,
children,
...props
}: React.ComponentProps<"div"> & { prompt?: string; kind?: keyof typeof tone }) {
return (
<div className={cn(tone[kind], kind === "comment" && "italic", className)} {...props}>
{prompt && (
<span aria-hidden="true" className="mr-2 text-(--code-keyword) select-none">
{prompt}
</span>
)}
{children}
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/terminal-frame/prompt.md, or the Copy AI prompt button above.
When to use
- Showing a command and its output on a landing page or in docs
- Beside a screenshot to show the install step
When not to use
- A real, interactive terminal
- Code people copy; there is no copy button
Agent notes
- Server component; each line is a TerminalLine and whitespace is preserved
- Colours come from the --code-* tokens, which flip with the theme
- Window dots use bg-muted-foreground/30, never traffic light colours
- The body scrolls horizontally rather than wrapping