Skip to contentVibraUI
Data display

Code block

A plain code panel with line numbers, highlighted lines, and a copy button — no highlighter.

Dependency-free by design: no shiki, no prism, nothing to load and nothing to theme. It renders a pre/code pair, one row per line, with the numbers in a left column of select-none spans that are also aria-hidden — so copying the block or reading it aloud gives the code and not the scaffolding. One trailing newline is dropped, which is how every editor ends a file, so there is no phantom last line. The body is a focusable, named region, since a block that scrolls has to be reachable without a mouse. Reach for a real highlighter only when the language actually matters to the reader.

Install

npx shadcn@latest add @vibra/code-block

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
codestring—The source. One trailing newline is dropped.
languagestring—Shown as a chip in the header, and used to name the scroll region.
titleReact.ReactNode—A file name or a caption; adds the header on its own.
showLineNumbersbooleanfalseAdds the unselectable gutter.
highlightLinesnumber[]—1-based line numbers to tint with the accent — the lines a reader is meant to look at.
maxHeightnumber—Pixels; the body scrolls past it.
copyablebooleantruePuts a copy button in the header, which is why a bare block still has one.
wrapbooleanfalseWraps long lines instead of scrolling them sideways.
classNamestring—Merged onto the div root; the remaining div props are spread onto it too.

Dependencies

Source

components/ui/code-block.tsx
import * as React from "react"

import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { CopyButton } from "@/components/ui/copy-button"

// `title` is content here — a file name shown in the header — not the HTML
// tooltip attribute.
export type CodeBlockProps = Omit<React.ComponentProps<"div">, "title"> & {
  code: string
  /** Shown as a chip in the header and used to name the scroll region. */
  language?: string
  /** A file name or a caption; adds the header on its own. */
  title?: React.ReactNode
  showLineNumbers?: boolean
  /** 1-based line numbers to tint — the lines a reader is meant to look at. */
  highlightLines?: number[]
  /** Pixels; the body scrolls past it. */
  maxHeight?: number
  copyable?: boolean
  wrap?: boolean
}

/**
 * A plain, dependency-free code panel: no syntax highlighting, just a
 * monospaced block with an optional gutter, highlighted lines, and a copy button.
 */
function CodeBlock({
  className,
  code,
  language,
  title,
  showLineNumbers = false,
  highlightLines,
  maxHeight,
  copyable = true,
  wrap = false,
  ...props
}: CodeBlockProps) {
  // One trailing newline is how every editor ends a file; rendering it would
  // add a blank numbered line that is not in the source.
  const lines = code.replace(/\n$/, "").split("\n")
  const highlighted = new Set(highlightLines)
  const hasHeader = Boolean(title) || Boolean(language) || copyable
  const gutterWidth = `${String(lines.length).length + 1}ch`

  return (
    <div
      data-slot="code-block"
      data-wrap={wrap || undefined}
      data-line-numbers={showLineNumbers || undefined}
      className={cn("overflow-hidden rounded-lg border bg-muted/40", className)}
      {...props}
    >
      {hasHeader ? (
        <div
          data-slot="code-block-header"
          className="flex h-9 items-center gap-2 border-b px-3 text-xs text-muted-foreground"
        >
          {title ? (
            <span data-slot="code-block-title" className="truncate font-medium text-foreground">
              {title}
            </span>
          ) : null}
          {language ? (
            <Badge variant="outline" className="font-mono">
              {language}
            </Badge>
          ) : null}
          {copyable ? (
            <CopyButton value={code} size="icon-xs" className="-me-1.5 ms-auto" />
          ) : null}
        </div>
      ) : null}

      <div
        data-slot="code-block-body"
        // A block that scrolls has to be reachable without a mouse, and a
        // focusable scroller needs a name of its own.
        tabIndex={0}
        role="region"
        aria-label={typeof title === "string" ? title : language ? `${language} code` : "Code"}
        className="overflow-auto focus-ring"
        style={maxHeight === undefined ? undefined : { maxHeight: `${maxHeight}px` }}
      >
        <pre className={cn("py-2 font-mono text-label leading-6", wrap ? "w-full" : "w-fit min-w-full")}>
          <code data-slot="code-block-code" className={cn("block", wrap && "whitespace-pre-wrap")}>
            {lines.map((line, index) => {
              const number = index + 1
              return (
                <span
                  key={number}
                  data-slot="code-block-line"
                  data-line={number}
                  data-highlighted={highlighted.has(number) || undefined}
                  className={cn(
                    "flex min-h-6 px-3",
                    highlighted.has(number) && "bg-accent"
                  )}
                >
                  {showLineNumbers ? (
                    <span
                      data-slot="code-block-line-number"
                      // Out of the copy buffer and out of the reading order:
                      // the numbers are scaffolding, not part of the code.
                      aria-hidden="true"
                      className="me-4 shrink-0 text-end tabular-nums text-muted-foreground select-none"
                      style={{ width: gutterWidth }}
                    >
                      {number}
                    </span>
                  ) : null}
                  <span className={cn("min-w-0", wrap ? "break-words whitespace-pre-wrap" : "whitespace-pre")}>
                    {line}
                  </span>
                </span>
              )
            })}
          </code>
        </pre>
      </div>
    </div>
  )
}

export { CodeBlock }