shadimage
mischief
exampleterminal-output-demosource: ok · refreshed

Terminal Output Demo

A worked example of Terminal Output: Streaming command output with stderr called out, an exit code, and scroll that follows without trapping you.

npx shadcn add "https://ui.tinkererslabs.com/r/terminal-output-demo.json"
free

Visual style

terminal-mono

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
lucide-react@^1.31.0

Registry dependencies

https://ui.tinkererslabs.com/r/terminal-output.json

Files3 · source cached 10/7/2026, 12:37:36 AM

components/demos/terminal-output-demo.tsx
"use client"

import * as React from "react"

import { DemoVariants } from "@/components/demos/demo-variants"
import { RestartButton } from "@/components/demos/restart-button"
import {
  TerminalOutput,
  type TerminalLine,
} from "@/registry/default/terminal-output/terminal-output"

const script: TerminalLine[] = [
  { text: "Packages: +214" },
  { text: "Progress: resolved 214, reused 210, downloaded 4" },
  { text: "" },
  { text: "dependencies:" },
  { text: "+ mischief-ui 0.7.1" },
  { text: "" },
  { text: "Done in 1.8s" },
]

const failure: TerminalLine[] = [
  {
    text: "src/total.ts(4,17): error TS2554: Expected 1 arguments, but got 2.",
    stream: "stderr",
  },
  { text: "", stream: "stderr" },
  { text: "Found 1 error in src/total.ts", stream: "stderr" },
]

function LiveRun() {
  const [count, setCount] = React.useState(0)
  const [runId, setRunId] = React.useState(0)

  React.useEffect(() => {
    if (count >= script.length) return

    const timer = setTimeout(() => setCount((shown) => shown + 1), 420)
    return () => clearTimeout(timer)
  }, [count, runId])

  const done = count >= script.length

  return (
    <div className="grid gap-4">
      <TerminalOutput
        command="pnpm install mischief-ui"
        cwd="~/projects/shop"
        output={script.slice(0, count)}
        running={!done}
        exitCode={done ? 0 : undefined}
        maxHeight="11rem"
      />

      {done ? (
        <RestartButton
          onClick={() => {
            setCount(0)
            setRunId((id) => id + 1)
          }}
        />
      ) : null}
    </div>
  )
}

export function TerminalOutputDemo() {
  return (
    <DemoVariants
      label="Command run"
      variants={[
        { id: "live", label: "Streaming", render: () => <LiveRun /> },
        {
          id: "failed",
          label: "Failed",
          render: () => (
            <TerminalOutput
              command="pnpm typecheck"
              output={failure}
              exitCode={2}
              maxHeight="11rem"
            />
          ),
        },
      ]}
    />
  )
}
components/demos/demo-variants.tsx
"use client"

import * as React from "react"

export type DemoVariant = {
  id: string
  label: string
  render: () => React.ReactNode
}

export function DemoVariants({
  variants,
  label = "Demo variant",
}: {
  variants: [DemoVariant, ...DemoVariant[]]
  label?: string
}) {
  const [activeId, setActiveId] = React.useState(variants[0].id)
  const active =
    variants.find((variant) => variant.id === activeId) ?? variants[0]

  return (
    <div className="grid w-full justify-items-center gap-6">
      {/* Centres whatever the variant renders. Without this a demo that caps
          its own width sits against the left edge while the switch below it
          is centred, which reads as a mistake. */}
      <div className="flex w-full justify-center">{active.render()}</div>

      <div
        role="group"
        aria-label={label}
        className="border-border bg-background/70 inline-flex flex-wrap justify-center gap-1 rounded-full border p-1"
      >
        {variants.map((variant) => {
          const isActive = variant.id === active.id

          return (
            <button
              key={variant.id}
              type="button"
              aria-pressed={isActive}
              className={
                isActive
                  ? "bg-foreground text-background rounded-full px-3 py-1.5 text-xs font-semibold"
                  : "text-muted-foreground hover:text-foreground rounded-full px-3 py-1.5 text-xs font-semibold"
              }
              onClick={() => setActiveId(variant.id)}
            >
              {variant.label}
            </button>
          )
        })}
      </div>
    </div>
  )
}
components/demos/restart-button.tsx
"use client"

import { RotateCcw } from "lucide-react"

export function RestartButton({ onClick }: { onClick: () => void }) {
  return (
    <button className="restore-button" type="button" onClick={onClick}>
      <RotateCcw aria-hidden="true" size={15} /> Replay
    </button>
  )
}

More from mischief

Browse all components in this library →