Skip to content
goodtabs UI
BrowseBrowse components

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-card

No 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

PropTypeDefaultDescription
codestringThe code, as written. Newlines split it into lines.
filenamestringShown in the top bar, in mono.
languagestringShown in the bar when there is no filename, for example "sh".
lineNumbersbooleanfalseNumber every line down the left.
maxHeightstringCap 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