Snippet card
A code block with a filename bar and a copy button, no syntax colours.
npx shadcn@latest add https://ui.goodtabs.co/r/snippet-card.json
With the @goodtabs namespace in components.json: npx shadcn@latest add @goodtabs/snippet-card
lib/hosts.ts
export function hostUrl(host: Host, path = "/") {
const base = process.env.NODE_ENV === "production"
? `https://${host}.goodtabs.co`
: `http://${host}.localhost:3000`;
return base + path;
}A short TypeScript function under its filename. The copy button confirms with a check.
With line numbers
components/tile.tsx
import { cn } from "@/lib/utils";type Props = React.ComponentProps<"div"> & { size?: "sm" | "default";};export function Tile({ size = "default", className, ...props }: Props) { return ( <div data-size={size} className={cn("rounded-sm bg-muted p-3", size === "sm" && "p-2", className)} {...props} /> );}Numbered lines in a capped block that scrolls inside the card.
A shell command
sh
npx shadcn@latest add @goodtabs/snippet-cardNo filename, so the language shows as mono meta instead.
Usage
import { SnippetCard } from "@/components/ui/snippet-card"
<SnippetCard filename="lib/utils.ts" code={source} lineNumbers />
<SnippetCard language="sh" code="npx shadcn@latest add @goodtabs/snippet-card" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| code | string | The code, as written. Newlines split it into lines. | |
| filename | string | Shown in the top bar, in mono. | |
| language | string | Shown in the bar when there is no filename, for example "sh". | |
| lineNumbers | boolean | false | Number every line down the left. |
| maxHeight | string | Cap the block and scroll inside it, for example "240px". |
Source
snippet-card/snippet-card.tsx
"use client";
import * as React from "react";
import { CheckIcon, CopyIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
type SnippetCardProps = Omit<React.ComponentProps<"div">, "children"> & {
/** The code, as written. Newlines split it into lines. */
code: string;
/** Shown in the top bar, in mono. */
filename?: string;
/** Shown in the top bar when there is no filename, for example "sh". */
language?: string;
/** Number every line down the left. */
lineNumbers?: boolean;
/** Cap the block and scroll inside it, for example "240px". */
maxHeight?: string;
};
/**
* A code block in a card: a bar with the filename and a copy button, the code below.
* No syntax colours, by design. The type and the frame do the work.
*/
export function SnippetCard({
code,
filename,
language,
lineNumbers = false,
maxHeight,
className,
...props
}: SnippetCardProps) {
const [copied, setCopied] = React.useState(false);
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
React.useEffect(() => () => clearTimeout(timer.current), []);
const copy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopied(true);
clearTimeout(timer.current);
timer.current = setTimeout(() => setCopied(false), 2000);
} catch {
// Clipboard can be blocked. The code is still on screen to select by hand.
}
};
const Icon = copied ? CheckIcon : CopyIcon;
const lines = code.replace(/\n$/, "").split("\n");
const width = String(lines.length).length;
const meta = filename ?? language;
return (
<div
data-slot="snippet-card"
className={cn("w-full overflow-hidden rounded-lg border bg-card text-card-foreground", className)}
{...props}
>
<div className="flex h-9 items-center justify-between gap-2 border-b bg-muted pr-1.5 pl-3">
<span className="truncate font-mono text-xs text-foreground/70">{meta}</span>
<Button
type="button"
variant="ghost"
size="icon-sm"
aria-label={copied ? "Copied" : "Copy code"}
data-copied={copied || undefined}
onClick={copy}
className="shrink-0 transition-colors"
>
<Icon aria-hidden="true" />
<span className="sr-only" aria-live="polite">
{copied ? "Copied to clipboard" : ""}
</span>
</Button>
</div>
<pre
tabIndex={0}
className="m-0 overflow-auto p-4 font-mono text-xs leading-relaxed outline-none focus-visible:ring-2 focus-visible:ring-ring/50 focus-visible:ring-inset"
style={{ maxHeight }}
>
<code>
{lineNumbers
? lines.map((line, i) => (
<span key={i} className="flex">
<span
aria-hidden="true"
className="shrink-0 pr-4 text-right text-muted-foreground select-none"
style={{ minWidth: `${width}ch` }}
>
{i + 1}
</span>
<span>{line}</span>
</span>
))
: code}
</code>
</pre>
</div>
);
}
For AI agents
Hand your agent this page, the markdown at /components/snippet-card/prompt.md, or the Copy AI prompt button above.
When to use
- Install commands, config files and short examples in docs and READMEs
- A code sample on a landing page where highlighting would add noise
- Anywhere people need to copy a block, not just read it
When not to use
- Long files or anything that needs syntax highlighting: use a highlighter and wrap its output
- Editable code: this is read only
Agent notes
- Client component because of the copy state; it takes no functions, so demos can stay server
- Pass the code as a plain string, it is not escaped or highlighted; a trailing newline is trimmed
- The pre is focusable so a capped or wide block scrolls with the keyboard
- Clipboard writes need a secure context; failures are silent and the code stays selectable