Skip to content
Caseroom

Components · Display

Code block

Code with a filename bar and Copy, in a focusable scroll box that shades its edges.

0.29% of the 2,062 sites in The UI Index have a code block (6 sites).

astro.config.mjs
// astro.config.mjs
import { defineConfig } from 'astro/config';

export default defineConfig({
  site: 'https://northwind.example',
  trailingSlash: 'always',
});
Terminal
npx shadcn@latest add https://www.themes.dev/caseroom/r/code-block.json

Both previews render the same markup; the HTML tab below is that markup without a framework.

Install

React
npx shadcn@latest add https://www.themes.dev/caseroom/r/code-block.json
Astro
npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/code-block.json

Or copy the files below into your project. They need clsx, tailwind-merge, and the Caseroom theme. The HTML needs only the compiled CSS and the script.

code-block.client.ts
// Code block copy for Astro, HTML and React: [data-code-copy] copies its block's code and says "Copied" for a
// moment (also announced: the button's label changes, and a polite live region in the bar repeats it).
declare global {
  interface Window {
    __caseroomCode?: boolean;
  }
}

if (typeof window !== 'undefined' && !window.__caseroomCode) {
  window.__caseroomCode = true;
  document.addEventListener('click', async (e) => {
    const b = (e.target as Element).closest<HTMLButtonElement>('[data-code-copy]');
    if (!b) return;
    const code = b.closest('[data-code-block]')?.querySelector('code')?.textContent ?? '';
    try {
      await navigator.clipboard.writeText(code);
    } catch {
      return;
    }
    const live = b.closest('[data-code-block]')?.querySelector('[data-code-live]');
    b.textContent = 'Copied';
    if (live) live.textContent = 'Copied to the clipboard';
    setTimeout(() => {
      b.textContent = 'Copy';
      if (live) live.textContent = '';
    }, 1600);
  });
}

export {};
code-block.tsx
import { cn } from '@/lib/utils';
import './code-block.client';
import { codeBlock as c } from './code-block.variants';

export interface CodeBlockProps {
  code: string;
  /** Shown in the bar and names the scroll box, e.g. "astro.config.mjs" or "Terminal". */
  filename: string;
  id: string;
  className?: string;
}

/** Code with a filename bar and a Copy button, in a focusable scroll box. */
export function CodeBlock({ code, filename, id, className }: CodeBlockProps) {
  return (
    <figure data-code-block="" className={cn(c.root, className)}>
      <figcaption id={`${id}-name`} className={c.bar}>
        {filename}
        <span data-code-live="" className="sr-only" aria-live="polite"></span>
        <button type="button" data-code-copy="" className={c.copy}>Copy</button>
      </figcaption>
      <pre tabIndex={0} aria-labelledby={`${id}-name`} className={c.pre}><code>{code}</code></pre>
    </figure>
  );
}
code-block.variants.ts
/** A code block: a filename (or language) bar with a Copy button, then the code: lines wrap, a long block scrolls
 *  down. Black and white, monospace, no colours to keep in step with a theme. The scroll box is
 *  focusable and named by the filename, so keyboard users can scroll it. */
export const codeBlock = {
  root: 'w-full overflow-hidden rounded-lg border',
  bar: 'flex items-center justify-between gap-3 border-b bg-muted/50 px-4 py-2 font-mono text-xs text-muted-foreground',
  copy: 'inline-flex h-7 cursor-pointer items-center rounded-md border bg-background px-2.5 font-sans text-xs font-medium text-foreground transition-[background-color,transform] hover:bg-accent active:scale-[0.97] active:bg-pressed',
  // Lines wrap (a snippet reads better than a sideways scroll); long blocks scroll down.
  pre: 'm-0 max-h-[28rem] overflow-auto rounded-b-lg p-4 focus-visible:outline-offset-[-2px] font-mono text-[13px] leading-relaxed whitespace-pre-wrap [overflow-wrap:anywhere] [background:linear-gradient(to_right,var(--background)_50%,transparent)_left/2rem_100%_no-repeat_local,linear-gradient(to_left,var(--background)_50%,transparent)_right/2rem_100%_no-repeat_local,radial-gradient(farthest-side_at_0_50%,var(--tabs-shade,rgb(0_0_0/0.2)),transparent)_left/0.875rem_100%_no-repeat_scroll,radial-gradient(farthest-side_at_100%_50%,var(--tabs-shade,rgb(0_0_0/0.2)),transparent)_right/0.875rem_100%_no-repeat_scroll,var(--background)]',
};