Skip to content
goodtabs UI
BrowseBrowse components

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

PropTypeDefaultDescription
titlestringCentered in the title bar, for example a path or shell name.
maxWidthstringConstrain the window, for example "480px".
promptstringOn 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