Terminal
terminal ui Terminal/command-line display with a macOS-style title bar, command history, prompt character, dark/light themes, auto-scroll, optional typing animation, and a line slot for custom formatting.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/terminal.json $ npx shadcn@latest add https://uipkge.dev/r/react/terminal.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/terminal.json $ bunx shadcn@latest add https://uipkge.dev/r/react/terminal.json Named registry:
npx shadcn@latest add @uipkge-react/terminal Installs to: components/ui/terminal/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
lines Command history to render. | TerminalLine[] | — | required |
title Window title shown in the title bar. Default 'bash'. | string | — | optional |
promptChar Prompt character. Default '$'. | string | — | optional |
theme Color theme. Default 'dark'. | 'dark' | 'light' | — | optional |
autoScroll Auto-scroll to bottom when new lines arrive. Default true. | boolean | — | optional |
typing Animate lines typing in one-by-one. Default false. | boolean | — | optional |
typingSpeed Typing speed in ms per line. Default 120. | number | — | optional |
maxHeight Max height before scrolling. Default '400px'. | string | — | optional |
Schema
Type aliases from this item's source — use them to shape the data you pass in.
TerminalLine interface TerminalLine {
/** Prompt prefix shown before the command. Omit for output-only lines. */
prompt?: string
/** Command text shown after the prompt. */
command?: string
/** Output lines rendered below the command. */
output?: string
/** Override the line type: 'command' renders prompt+command, 'output' renders plain text. */
type?: 'command' | 'output'
} Files installed (2)
-
components/ui/terminal/Terminal.tsx 5.4 kB
'use client' import * as React from 'react' import { cn } from '@/lib/utils' export interface TerminalLine { /** Prompt prefix shown before the command. Omit for output-only lines. */ prompt?: string /** Command text shown after the prompt. */ command?: string /** Output lines rendered below the command. */ output?: string /** Override the line type: 'command' renders prompt+command, 'output' renders plain text. */ type?: 'command' | 'output' } export interface TerminalProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'title'> { /** Command history to render. */ lines: TerminalLine[] /** Window title shown in the title bar. Default 'bash'. */ title?: string /** Prompt character. Default '$'. */ promptChar?: string /** Color theme. Default 'dark'. */ theme?: 'dark' | 'light' /** Auto-scroll to bottom when new lines arrive. Default true. */ autoScroll?: boolean /** Animate lines typing in one-by-one. Default false. */ typing?: boolean /** Typing speed in ms per line. Default 120. */ typingSpeed?: number /** Max height before scrolling. Default '400px'. */ maxHeight?: string } const Terminal = React.forwardRef<HTMLDivElement, TerminalProps>( ( { lines, title = 'bash', promptChar = '$', theme = 'dark', autoScroll = true, typing = false, typingSpeed = 120, maxHeight = '400px', className, ...props }, ref, ) => { const bodyRef = React.useRef<HTMLDivElement | null>(null) const [visibleCount, setVisibleCount] = React.useState(typing ? 0 : lines.length) const typingTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(null) const resolvedLines = React.useMemo( () => lines.map((l) => ({ ...l, type: l.type ?? (l.prompt || l.command ? 'command' : 'output'), prompt: l.prompt ?? (l.type === 'output' ? '' : promptChar), })), [lines, promptChar], ) const shownLines = resolvedLines.slice(0, visibleCount) const scrollToBottom = React.useCallback(() => { if (!autoScroll || !bodyRef.current) return bodyRef.current.scrollTop = bodyRef.current.scrollHeight }, [autoScroll]) // Reset/advance visible count when the lines array changes. React.useEffect(() => { if (typing) { // Reset typing animation when lines change. setVisibleCount(0) return } setVisibleCount(lines.length) // Scroll on the next paint once the new lines are rendered. const raf = requestAnimationFrame(scrollToBottom) return () => cancelAnimationFrame(raf) // eslint-disable-next-line react-hooks/exhaustive-deps }, [lines.length, typing]) // Drive the typing animation whenever visibleCount is below the total. React.useEffect(() => { if (!typing) return if (visibleCount >= lines.length) return typingTimerRef.current = setTimeout(() => { setVisibleCount((c) => c + 1) scrollToBottom() }, typingSpeed) return () => { if (typingTimerRef.current) { clearTimeout(typingTimerRef.current) typingTimerRef.current = null } } }, [typing, visibleCount, lines.length, typingSpeed, scrollToBottom]) // Initial scroll-to-bottom for the non-typing case. React.useEffect(() => { if (!typing) scrollToBottom() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( <div ref={ref} data-uipkge="" data-slot="terminal" data-theme={theme} className={cn( 'relative overflow-hidden rounded-lg border font-mono text-sm shadow-sm', theme === 'dark' ? 'border-zinc-800 bg-zinc-950 text-zinc-200' : 'border-zinc-200 bg-zinc-50 text-zinc-800', className, )} {...props} > {/* Title bar */} <div className={cn( 'flex items-center gap-2 border-b px-4 py-2.5', theme === 'dark' ? 'border-zinc-800 bg-zinc-900' : 'border-zinc-200 bg-zinc-100', )} > <div className="flex gap-1.5"> <span className="size-3 rounded-full bg-red-500" /> <span className="size-3 rounded-full bg-yellow-500" /> <span className="size-3 rounded-full bg-green-500" /> </div> <span className={cn('ml-2 text-xs', theme === 'dark' ? 'text-zinc-400' : 'text-zinc-500')}>{title}</span> </div> {/* Body */} <div ref={bodyRef} className="overflow-auto p-4 leading-relaxed" style={{ maxHeight }}> {shownLines.map((line, i) => ( <div key={i} data-slot="terminal-line" className="break-words whitespace-pre-wrap"> {line.type === 'command' && ( <div data-slot="terminal-command" className="flex flex-wrap items-baseline gap-x-1.5"> <span className={cn('shrink-0 font-semibold', theme === 'dark' ? 'text-green-400' : 'text-green-600')} > {line.prompt} </span> <span>{line.command}</span> </div> )} {line.output && ( <div data-slot="terminal-output" className="text-zinc-400"> {line.output} </div> )} </div> ))} </div> </div> ) }, ) Terminal.displayName = 'Terminal' export { Terminal } -
components/ui/terminal/index.ts 0.1 kB
export { Terminal, type TerminalProps, type TerminalLine } from './Terminal'
Raw manifest: https://uipkge.dev/r/react/terminal.json