# Code block (Caseroom component)

Code with a filename bar and Copy, in a focusable scroll box that shades its edges. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

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

## 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`
- HTML: https://www.themes.dev/caseroom/r/html/code-block.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/code-block/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### code-block.astro

```astro
---
import { cn } from '@/lib/utils';
import { codeBlock as c } from './code-block.variants';

interface Props {
  code: string;
  /** Shown in the bar and names the scroll box, e.g. "astro.config.mjs" or "Terminal". */
  filename: string;
  id: string;
  class?: string;
}
const { code, filename, id, class: className } = Astro.props;
---

<figure data-code-block class={cn(c.root, className)}>
  <figcaption id={`${id}-name`} class={c.bar}>
    {filename}
    <span data-code-live class="sr-only" aria-live="polite"></span>
    <button type="button" data-code-copy class={c.copy}>Copy</button>
  </figcaption>
  <pre tabindex="0" aria-labelledby={`${id}-name`} class={c.pre}><code>{code}</code></pre>
</figure>

<script>
  import './code-block.client';
</script>
```

### code-block.client.ts

```tsx
// 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

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

```tsx
/** 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)]',
};
```
