Display

Code Block

A self-contained, copyable code panel with its own regex syntax highlighter — line numbers, line highlighting, tabs and a wrap toggle, zero external highlighting engine.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { Check, Copy, WrapText } from "lucide-react"
import { cn } from "@/lib/utils"

/* ------------------------------------------------------------------ */
/* Minimal regex tokenizer — tsx/ts/js/jsx, json, bash. No external    */
/* highlighting engine, no dangerouslySetInnerHTML: every line is cut  */
/* into tokens and re-joining every token.text reproduces the input.   */
/* ------------------------------------------------------------------ */

type TokenType = "comment" | "string" | "number" | "keyword" | "function" | "plain"

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/code-block.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "CodeBlock" component. Self-contained:
write your own minimal regex tokenizer, no Shiki/Prism/highlight.js, no
dangerouslySetInnerHTML.

Contract
- Export a forwardRef div. Props: code?: string, tabs?: { label: string,
  code: string, language?: string }[] (tabs takes priority over code when
  both are given), language = "tsx", filename?: string,
  showLineNumbers = false, highlightLines?: number[] (1-based),
  maxHeight?: number | string, wrap = false, copyable = true, caption?: string.
- Highlighter: a tokenizeLine(line, language) function (~60 lines) that cuts
  one line into { text, type } tokens where type is comment | string |
  number | keyword | function | plain, using ONE regex per language built
  from named capture groups (comment, string, number, ident), executed in a
  loop that always appends the untouched gap before each match as a "plain"
  token. This guarantees every token.text concatenated back together
  reproduces the input line exactly — self-test that invariant by hand.
  Support tsx/ts/js/jsx (// comments, "/'/` strings, JS keyword set,
  identifier-followed-by-"(" => function), json (no comments, "-strings
  only, true/false/null as keywords) and bash/sh (# comments, "/' strings,
  shell keyword set). Unknown languages fall back to one plain token —
  document this as a real limitation, not a bug.
  An identifier is classified in this order: keyword set FIRST, then the
  "followed by (" call heuristic. Testing the paren first paints `if (`,
  `for (`, `while (`, `switch (`, `catch (` and `return (` with the function
  color — they are control flow, not calls.
  Wrap the rendered lines in a real <pre><code class="language-<lang>">: it is
  the semantic pair for source text, keeps line breaks on copy/paste, and lets
  assistive tech announce the block as code. The per-line rows stay flex divs
  inside it so the gutter and the highlight bar still work; give <pre> m-0 p-0
  because a surrounding prose stylesheet would otherwise add its own margins.

Behavior
- Tabs (when given) render a role="tablist" strip; each tab is role="tab"
  with aria-selected, roving tabIndex (0 on the active tab, -1 on the rest),
  and ArrowLeft/ArrowRight/Home/End move focus AND selection. The single
  content region below is role="tabpanel", aria-labelledby the active tab's
  id. Switching tabs resets the active index via the adjust-state-during-
  render pattern (compare a previous "tab labels joined" key during render,
  not a setState-in-effect) whenever the tab set itself changes.
- Copy button copies the ACTIVE tab's raw `code` string (never text read
  back out of the highlighted DOM). Try navigator.clipboard.writeText first;
  on any failure (unsupported API, insecure context, permission denial) fall
  back to a hidden-textarea + document.execCommand("copy"); if that also
  fails, show a visible "Copy failed — select the code manually." message
  (role="status") instead of failing silently. On success, swap the icon to
  a checkmark for 2 seconds (cleared with clearTimeout on unmount and on a
  fresh click), and mirror the state into an aria-live="polite" sr-only span.
- Wrap toggle button in the header always renders regardless of the `wrap`
  prop; `wrap` only sets the initial state. Toggling flips every line
  between whitespace-pre (default; overflows and the panel scrolls
  horizontally) and whitespace-pre-wrap break-words (reflows, no horizontal
  scroll).
- Line numbers render in a separate leading gutter span with select-none +
  aria-hidden="true" so a user drag-selecting the code never picks up the
  numbers, and screen readers never hear them.
- highlightLines marks matching 1-based line rows with a background tint
  plus a 2px accent bar pinned to the left edge of the row (aria-hidden,
  absolutely positioned) — both survive wrap and horizontal scroll.
- maxHeight (number | string) caps the content region and turns on
  overflow-auto; when set (or inside a tabpanel) the region gets tabIndex=0
  so keyboard users can focus it and scroll/read with arrow keys.

Rendering & styling
- Semantic tokens only: bg-card for the shell, bg-muted/40 for the header,
  border for dividers, text-muted-foreground for the filename/gutter/caption,
  bg-primary/10 + bg-primary for the highlighted-line tint and accent bar,
  text-destructive + bg-destructive/10 for the copy-failure banner. Token
  colors: comments -> text-muted-foreground, strings/numbers/keywords/
  function-names -> var(--chart-1) through var(--chart-5) (one each),
  everything else (punctuation, whitespace, plain identifiers) ->
  text-foreground. cn() merges consumer className throughout.
- Hover/focus transitions on the header buttons are motion-reduce:transition
  -none — there is no auto-playing animation here, so reduced motion only
  needs to drop the CSS transition, never a behavior.
- Accessible: focus-visible rings on every button, aria-pressed on the wrap
  toggle, aria-selected/aria-controls/role wiring on tabs, sr-only labels on
  icon-only buttons.

Customization levers
- Token palette: remap the 6-entry TOKEN_CLASS table to different
  var(--chart-N) slots (or add a 7th "attribute" category) without touching
  the tokenizer.
- Add a language: one entry in the LANGUAGES config (keyword set + comment
  style + whether backtick strings apply) — the tokenizer loop is unchanged.
- Density: shrink text-[13px]/leading-6 and the gutter's px-4 for a denser
  panel, or drop showLineNumbers entirely for prose-embedded snippets.
- Header layout: swap the plain filename <span> for a language badge, or
  hide the wrap button via a wrapper that only renders CodeBlock when a line
  is known to be short.
- Highlight color: bg-primary/10 + bg-primary reads as "this changed"; swap
  to bg-destructive/10 + bg-destructive for a "this is wrong" reading, or
  var(--chart-2) tones for "this is new".

Concepts

  • Line-preserving tokenizer — the regex loop always emits the untouched gap between matches as a "plain" token, which is what guarantees the tokens reconstruct the exact input line (no silently dropped characters).
  • Gutter selection safety — line numbers live in their own select-none span ahead of the code span, so a mouse drag across a line copies only the code, and aria-hidden keeps them out of the accessibility tree entirely.
  • Copy the source, not the DOM — the copy button always copies the plain code string it was given, never textContent read back out of the highlighted spans, so copied output can't pick up rendering artifacts.
  • Honest clipboard fallbacknavigator.clipboard failing (insecure context, permission denial, unsupported browser) falls through to a legacy execCommand copy, and only shows a visible failure message if that fails too — never a silent no-op.
  • Wrap is a real reflow, not a visual trick — toggling swaps whitespace-pre for whitespace-pre-wrap break-words on every line, trading horizontal scroll for reflowed lines.
  • Active-tab-scoped everything — highlighting, line numbers and copy all read from whichever tab is currently active; switching tabs resets to a fresh view without an effect-driven state cascade.

On This Page