# CodeBlock (/docs/code-block)





`CodeBlock` renders a syntax-highlighted code block using [Shiki](https://shiki.style), spec'd off Mantine's `CodeHighlight`: a copy button, collapse/expand for long snippets, custom-icon support, and a general comment-notation-driven line/gutter mechanism. It's `"use client"` — highlighting is async, so the component renders a plain, unhighlighted fallback (same chrome, no layout shift) until it resolves.

```tsx
import { CodeBlock } from '@42-central/ui-react/code-block';
import '@42-central/ui-react/code-block/styles.css';

<CodeBlock
  language="tsx"
  code={`function greet(name) {
  console.log(\`Hello, \${name}!\`)
}`}
/>
```

<Callout type="info" title="Ship the stylesheet">
  `@42-central/ui-react/code-block/styles.css` is a separate, opt-in stylesheet — not part of the universal `styles.css`/`theme.css`/`colors.css` chain every consumer already imports. Only apps that render a `CodeBlock` need Shiki's token/gutter CSS, so it ships on its own.
</Callout>

## CodeBlock.Static [#codeblockstatic]

For a fixed, known-at-render-time `code` (the diff example below is one — it's actually rendered via `CodeBlock.Static`, not `CodeBlock`), `CodeBlock.Static` is an async React Server Component that `await`s the same `highlightCode` during the server render instead of `CodeBlock`'s client-side effect. On a statically-generated page, that bakes the highlighted markup straight into the build output: no client JS spent on Shiki, no loading flash. It renders the exact same toolbar/copy/collapse-expand chrome as `CodeBlock` — the only difference is where the highlighting happens, and that it can't react to `code` changing later (there's no "later" from a Server Component's perspective). Requires a React Server Components-capable framework (e.g. Next.js App Router); reach for `CodeBlock` itself for anything editable/dynamic, like the Playground below.

```tsx
import { CodeBlock } from '@42-central/ui-react/code-block';

// A Server Component — no "use client" needed here at all:
export default function Page() {
  return <CodeBlock.Static language="js" code="console.log('hi')" />;
}
```

## Playground [#playground]

<story.Playground />

### Full prop browser [#full-prop-browser]

<story.WithControl />

## Collapsing long snippets [#collapsing-long-snippets]

Set `collapsedLines` to always show only the first N lines, with a fade + "Show more" control revealing the rest. Collapse/expand can be controlled (`expanded`/`onExpandedChange`) or left uncontrolled (`defaultExpanded`).

<story.Preview variant="Collapsible" />

## Title [#title]

`title` adds a header row above the code — typically a filename. The toolbar moves into that header instead of floating over the code, so it never covers the first line.

<Preview align="block" code="&#x22;<CodeBlock.Static\n  title=\&#x22;greet.ts\&#x22;\n  language=\&#x22;ts\&#x22;\n  code={`export function greet(name: string) {\n  return \&#x22;Hello, \&#x22; + name\n}`}\n/>&#x22;">
  <CodeBlock.Static title="greet.ts" language="ts" code="&#x22;export function greet(name: string) {\n  return \&#x22;Hello, \&#x22; + name\n}&#x22;" />
</Preview>

## Line numbers [#line-numbers]

`lineNumbers()` (a tiny preset, not a dedicated prop) renders a real gutter node per line — not a `::before` pseudo-element — so it composes cleanly alongside any other `gutters` column.

<story.Preview variant="With line numbers" />

## Notation — a general mechanism, not dedicated features [#notation--a-general-mechanism-not-dedicated-features]

A line opts into styling or gutter content by carrying a comment marker — `// [!code tagName]`, or `// [!code tagName:payload]` for a value like a name or a number. `tags` maps a marker's tag to styling (`background`/`color`, the same theme-aware [`Color`](/docs/colors) tokens every colored component in the kit uses, plus `className`/`style` as the escape hatch); `gutters` maps tags to a stacked column rendered left of the code. There's no baked-in "highlight" or "diff" concept anywhere in `CodeBlock` — a diff view, a spotlighted zone, a redacted token, etc. are all just tags/gutters you author yourself (or generate).

<Callout type="info" title="No range shorthand">
  A marker always applies to exactly the line it's written on — there's no "applies to the next N lines" shorthand. A multi-line diff removal is the same marker repeated on every affected line, which is trivial once it's machine-generated (see `toDiffNotation` below) and no real burden by hand either, since the marker sits right next to the line it describes.
</Callout>

### Grammar reference [#grammar-reference]

`// [!code tagName]` or `// [!code tagName:payload]`. `tagName` matches `[A-Za-z][\w-]*` — a letter, then any run of letters, digits, underscores, or hyphens (`highlight`, `blame`, `tutorial-step` are all valid tag names; `2fast` isn't). `payload` is free-form text up to the closing `]`; escape a literal colon or bracket inside it with `\:`/`\]` — `// [!code note:ratio is 3\:4]` parses to tag `note`, payload `ratio is 3:4`.

A marker is only honored inside a comment style the parser recognizes, checked per physical line (never across lines). Detection is purely textual, independent of the `language` grammar — so the `//` markers `toDiffNotation` emits work on a CSS or Python sample too.

| Style                  | Example                                          |
| ---------------------- | ------------------------------------------------ |
| `//`                   | `const x = 1 // [!code highlight]`               |
| `#`                    | `x = 1  # [!code highlight]`                     |
| `;`                    | `(setq x 1) ; [!code highlight]`                 |
| `;;`                   | `(setq x 1) ;; [!code highlight]`                |
| `%`                    | `\section{Intro} % [!code highlight]`            |
| `%%`                   | `handle(X) -> X. %% [!code highlight]`           |
| `--`                   | `SELECT * FROM users -- [!code highlight]`       |
| `/* ... */`            | `/* [!code highlight] */`                        |
| `<!-- ... -->`         | `<!-- [!code highlight] -->`                     |
| `{/* ... */}` (JSX)    | `{/* [!code highlight] */}`                      |
| JSDoc `*` continuation | the 2nd+ line of a `/** ... */` block, see below |

The line-style openers (`//`, `#`, `;`/`;;`, `%`/`%%`, `--`) are found as the **rightmost** such token on the line, so `// TODO: fix // [!code todo]` finds the second `//` and leaves the first comment's own prose untouched. They also tolerate real prose before the marker in the same comment — `sum += x // keep this [!code highlight]` strips only the marker, leaving `sum += x // keep this`.

Block-style comments (`/* */`, `<!-- -->`, `{/* */}`) don't. The marker has to be the comment's **entire** content (surrounding whitespace aside), because removing an interior marker while preserving the closing `*/`/`-->`/`}` isn't a plain end-of-line trim. A marker mixed with prose inside one of those isn't recognized as notation at all and renders as literal text — reach for a line-style trailing comment instead when prose needs to sit next to a marker.

The JSDoc continuation form reaches a marker on the 2nd+ line of a `/** ... */` block, where the forms above can't:

```ts notation=false
/**
 * Formats a friendly greeting.
 * [!code highlight]
 */
function greet(name: string) {
  return `Hello, ${name}!`
}
```

<Callout type="info" title="A marker-only line disappears entirely">
  A line whose only content is a marker is spliced out of the render, not left behind as a blank line. When a marker shares a line with real code, only the marker (and the now-empty comment opener) is trimmed — the code before it stays.
</Callout>

```js notation=false
function total(items) {
  let sum = 0
  // [!code highlight]
  for (const item of items) {
    sum += item.price; // [!code highlight]
  }
  return sum
}
```

renders as:

```js
function total(items) {
  let sum = 0
  for (const item of items) {
    sum += item.price;
  }
  return sum
}
```

**Markers stack.** More than one distinct tag can sit in the same trailing comment, each matched independently against `tags`/`gutters`:

```js notation=false
sum += item.price // [!code blame:alice] [!code coverage:miss]
```

That line carries a `blame` marker (payload `alice`) and a `coverage` marker (payload `miss`) at once — a `tags.blame` style, a `tags.coverage` style, and a gutter column for each all apply to it together. Within a *single* content column that maps both tags, the first marker in written order wins.





### A diff-style view [#a-diff-style-view]

`toDiffNotation(oldCode, newCode)` (from `@42-central/ui-react/code-block/diff`, a separate subpath — see below) runs a real line-diff and returns ONE merged code string with `// [!code removed]`/`// [!code added]` already appended to every affected line, so nothing here is hand-typed. Tag `removed`/`added` each with their own `background`; a `gutters` content column renders a fixed `−`/`+` marker for whichever tag matched. The marker's own `color` (text) is set per tag directly in `tags`, independent of the line's own (translucent, background-only) tint, so the marker itself reads unambiguously red/green.

<Preview align="block" code="&#x22;import { toDiffNotation } from '@42-central/ui-react/code-block/diff';\n\nconst oldCode = `function total(items) {\n  let sum = 0\n  for (const item of items) {\n    sum += item.price\n  }\n  return sum\n}`;\n\nconst newCode = `function total(items) {\n  return items.reduce((sum, item) => sum + item.price, 0)\n}`;\n\n<CodeBlock.Static\n  language=\&#x22;js\&#x22;\n  code={toDiffNotation(oldCode, newCode)}\n  tags={{\n    removed: { background: \&#x22;red\&#x22; },\n    added: { background: \&#x22;green\&#x22; },\n  }}\n  gutters={[\n    {\n      kind: \&#x22;content\&#x22;,\n      tags: {\n        removed: { text: \&#x22;−\&#x22;, color: \&#x22;red\&#x22; },\n        added: { text: \&#x22;+\&#x22;, color: \&#x22;green\&#x22; },\n      },\n      align: \&#x22;center\&#x22;,\n    },\n  ]}\n/>&#x22;">
  <CodeBlock.Static
    language="js"
    code="toDiffNotation(diffOldCode, diffNewCode)"
    tags="{
    removed: { background: &#x22;red&#x22; },
    added: { background: &#x22;green&#x22; },
  }"
    gutters="[
    {
      kind: &#x22;content&#x22;,
      tags: {
        removed: { text: &#x22;−&#x22;, color: &#x22;red&#x22; },
        added: { text: &#x22;+&#x22;, color: &#x22;green&#x22; },
      },
      align: &#x22;center&#x22;,
    },
  ]"
  />
</Preview>

### Diverging old/new line numbers [#diverging-oldnew-line-numbers]

Stack two counter columns, each `skip`-ping the tag that shouldn't advance it — the old-number column skips `added` lines, the new-number column skips `removed` ones. The numbers repeat and diverge automatically; nobody types a number, and — since `toDiffNotation` produced the markers — nobody counted a line either.

<Preview align="block" code="&#x22;import { toDiffNotation } from '@42-central/ui-react/code-block/diff';\n\nconst oldCode = `function total(items) {\n  let sum = 0\n  for (const item of items) {\n    sum += item.price\n  }\n  return sum\n}`;\n\nconst newCode = `function total(items) {\n  return items.reduce((sum, item) => sum + item.price, 0)\n}`;\n\n<CodeBlock.Static\n  language=\&#x22;js\&#x22;\n  code={toDiffNotation(oldCode, newCode)}\n  tags={{\n    removed: { background: \&#x22;red\&#x22; },\n    added: { background: \&#x22;green\&#x22; },\n  }}\n  gutters={[\n    { kind: \&#x22;counter\&#x22;, skip: [\&#x22;added\&#x22;] },\n    { kind: \&#x22;counter\&#x22;, skip: [\&#x22;removed\&#x22;] },\n    {\n      kind: \&#x22;content\&#x22;,\n      tags: {\n        removed: { text: \&#x22;−\&#x22;, color: \&#x22;red\&#x22; },\n        added: { text: \&#x22;+\&#x22;, color: \&#x22;green\&#x22; },\n      },\n      align: \&#x22;center\&#x22;,\n    },\n  ]}\n/>&#x22;">
  <CodeBlock.Static
    language="js"
    code="toDiffNotation(diffOldCode, diffNewCode)"
    tags="{
    removed: { background: &#x22;red&#x22; },
    added: { background: &#x22;green&#x22; },
  }"
    gutters="[
    { kind: &#x22;counter&#x22;, skip: [&#x22;added&#x22;] },
    { kind: &#x22;counter&#x22;, skip: [&#x22;removed&#x22;] },
    {
      kind: &#x22;content&#x22;,
      tags: {
        removed: { text: &#x22;−&#x22;, color: &#x22;red&#x22; },
        added: { text: &#x22;+&#x22;, color: &#x22;green&#x22; },
      },
      align: &#x22;center&#x22;,
    },
  ]"
  />
</Preview>

### A highlighted zone [#a-highlighted-zone]

Tag every line in a range with the same marker to tint a contiguous run — e.g. spotlighting a whole function body. `highlight()` is a ready-made preset for the common case of a single `highlight` tag.

<Preview align="block" code="&#x22;<CodeBlock.Static\n  language=\&#x22;ts\&#x22;\n  code={`function greet(name) {\n  const message = 'Hello, ' + name // [!code highlight]\n  console.log(message) // [!code highlight]\n  return message\n}`}\n  tags={highlight()}\n/>&#x22;">
  <CodeBlock.Static language="ts" code="&#x22;function greet(name) {\n  const message = 'Hello, ' + name // [!code highlight]\n  console.log(message) // [!code highlight]\n  return message\n}&#x22;" tags="highlight()" />
</Preview>

### Showing markers verbatim [#showing-markers-verbatim]

`notation={false}` skips the parse entirely, so `[!code …]` comments render as plain source — for a snippet that documents the notation itself, like the code panels on this page.

<Preview align="block" code="&#x22;<CodeBlock.Static\n  notation={false}\n  language=\&#x22;ts\&#x22;\n  code=\&#x22;console.log(message) // [!code highlight]\&#x22;\n/>&#x22;">
  <CodeBlock.Static notation="false" language="ts" code="console.log(message) // [!code highlight]" />
</Preview>

### Extending the notation [#extending-the-notation]

`CodeBlock` ships no "blame" or "coverage" concept — `tags`/`gutters` are the entire mechanism, and a new tag works exactly the way `removed`/`added`/`highlight` already do above. Adding one is always the same recipe:

1. Annotate source lines with `// [!code yourTag]` or `// [!code yourTag:payload]` (or whichever recognized comment style matches the language you're highlighting).
2. Optionally style every matching line via `tags.yourTag` — `background`/`color` tint the line itself, `className`/`style` are the escape hatch for anything else.
3. Optionally surface the tag in the gutter via a `gutters` content column: map `yourTag` to fixed text (a literal string, e.g. `"→"`), to `true` to render the marker's own payload verbatim, or to `{ text, color?, background? }` when the gutter marker needs its own color.

Three worked examples, rendered live, and one paste-ready recipe.

**Blame** — the marker's payload is a free-form author name, echoed verbatim in its own gutter column via the `true` shorthand:

<Preview align="block" code="&#x22;<CodeBlock.Static\n  language=\&#x22;js\&#x22;\n  code={`function total(items) {\n  let sum = 0 // [!code blame:alice]\n  for (const item of items) { // [!code blame:bob]\n    sum += item.price // [!code blame:bob]\n  }\n  return sum // [!code blame:alice]\n}`}\n  tags={{ blame: { background: \&#x22;neutral\&#x22; } }}\n  gutters={[\n    { kind: \&#x22;content\&#x22;, tags: { blame: true }, align: \&#x22;left\&#x22;, width: \&#x22;5ch\&#x22; },\n  ]}\n/>&#x22;">
  <CodeBlock.Static
    language="js"
    code="&#x22;function total(items) {\n  let sum = 0 // [!code blame:alice]\n  for (const item of items) { // [!code blame:bob]\n    sum += item.price // [!code blame:bob]\n  }\n  return sum // [!code blame:alice]\n}&#x22;"
    tags="{ blame: { background: &#x22;neutral&#x22; } }"
    gutters="[
    { kind: &#x22;content&#x22;, tags: { blame: true }, align: &#x22;left&#x22;, width: &#x22;5ch&#x22; },
  ]"
  />
</Preview>

**Tutorial steps** — the payload is a step number; `tags.step` tints the line and the same number is echoed, styled to match, in a dedicated gutter column via the full `{ text: true, color }` form:

<Preview align="block" code="&#x22;<CodeBlock.Static\n  language=\&#x22;js\&#x22;\n  code={`function greet(name) {\n  const message = 'Hello, ' + name // [!code step:1]\n  console.log(message) // [!code step:2]\n  return message // [!code step:3]\n}`}\n  tags={{ step: { background: \&#x22;blue\&#x22; } }}\n  gutters={[\n    { kind: \&#x22;content\&#x22;, tags: { step: { text: true, color: \&#x22;blue\&#x22; } }, align: \&#x22;center\&#x22; },\n  ]}\n/>&#x22;">
  <CodeBlock.Static
    language="js"
    code="&#x22;function greet(name) {\n  const message = 'Hello, ' + name // [!code step:1]\n  console.log(message) // [!code step:2]\n  return message // [!code step:3]\n}&#x22;"
    tags="{ step: { background: &#x22;blue&#x22; } }"
    gutters="[
    { kind: &#x22;content&#x22;, tags: { step: { text: true, color: &#x22;blue&#x22; } }, align: &#x22;center&#x22; },
  ]"
  />
</Preview>

**Coverage** — two distinct tags (`hit`/`miss`) instead of one tag with two payload values, since a line's coverage status is a discrete choice rather than free text worth capturing in a payload:

<Preview align="block" code="&#x22;<CodeBlock.Static\n  language=\&#x22;js\&#x22;\n  code={`function divide(a, b) {\n  if (b === 0) { // [!code miss]\n    throw new Error('divide by zero') // [!code miss]\n  }\n  return a / b // [!code hit]\n}`}\n  tags={{\n    hit: { background: \&#x22;green\&#x22; },\n    miss: { background: \&#x22;red\&#x22; },\n  }}\n  gutters={[\n    { kind: \&#x22;content\&#x22;, tags: { hit: \&#x22;✓\&#x22;, miss: \&#x22;✗\&#x22; }, align: \&#x22;center\&#x22; },\n  ]}\n/>&#x22;">
  <CodeBlock.Static
    language="js"
    code="&#x22;function divide(a, b) {\n  if (b === 0) { // [!code miss]\n    throw new Error('divide by zero') // [!code miss]\n  }\n  return a / b // [!code hit]\n}&#x22;"
    tags="{
    hit: { background: &#x22;green&#x22; },
    miss: { background: &#x22;red&#x22; },
  }"
    gutters="[
    { kind: &#x22;content&#x22;, tags: { hit: &#x22;✓&#x22;, miss: &#x22;✗&#x22; }, align: &#x22;center&#x22; },
  ]"
  />
</Preview>

**Severity** — `warn`/`error` tags color the line and put a fixed icon in the gutter. The payload still carries the actual message: nothing here renders it, but a downstream tool (or a second content column mapping the tag to `true`) can read it straight off the marker:

```tsx notation=false
<CodeBlock.Static
  language="js"
  code={`function riskyDivide(a, b) {
  console.log('dividing') // [!code warn:unused variable above]
  return a / b // [!code error:missing zero-check]
}`}
  tags={{
    warn: { background: "orange" },
    error: { background: "red" },
  }}
  gutters={[
    { kind: "content", tags: { warn: "!", error: "×" }, align: "center" },
  ]}
/>
```

## Extending Shiki [#extending-shiki]

`CodeBlock` hardcodes Shiki, but doesn't box in what Shiki itself can do:

* **`transformers`** — pass any [`ShikiTransformer`](https://shiki.style/guide/transformers), including community ones from [`@shikijs/transformers`](https://shiki.style/packages/transformers). They run after `CodeBlock`'s own internal notation transformer, so they can build on top of it.

  ```tsx
  import { transformerNotationDiff } from '@shikijs/transformers';

  <CodeBlock code="..." transformers={[transformerNotationDiff()]} />
  ```

* **`registerLanguage`/`registerTheme`** (exported from `@42-central/ui-react/code-block`) — register a custom TextMate grammar or VS Code-style theme once, so `language`/`themes` can reference it by name afterward, the same way a bundled one works. Await it before the first render that uses it (e.g. at app startup).

  ```tsx
  import { registerLanguage } from '@42-central/ui-react/code-block';

  await registerLanguage(myCustomGrammarJson);
  // later:
  <CodeBlock language="my-custom-lang" code="..." />
  ```

## Custom icons [#custom-icons]

`copyIcon`/`copiedIcon`/`expandIcon`/`collapseIcon` follow the same convention as [`Alert`](/docs/alert)'s `icon`: `undefined` uses the default, `null` hides it, any `ReactNode` overrides it.

## Custom toolbar controls [#custom-toolbar-controls]

`controls` renders before the built-in buttons in the same floating toolbar. Build a custom one with `CodeBlock.Button` — the same compact icon-button primitive copy/expand use internally, so it matches their size/style exactly instead of you hand-rolling one. It composes the same way the built-ins do too: wrap it in a `Tooltip` for a hover/focus label.

<Preview align="block" code="&#x22;<CodeBlock.Static\n  code={\&#x22;console.log('hi')\&#x22;}\n  controls={\n    <Tooltip label=\&#x22;Run\&#x22; asChild>\n      <CodeBlock.Button aria-label=\&#x22;Run\&#x22;>\n        <Play />\n      </CodeBlock.Button>\n    </Tooltip>\n  }\n/>&#x22;">
  <CodeBlock.Static
    code="&#x22;console.log('hi')&#x22;"
    controls="
    <CodeBlockButton aria-label=&#x22;Run&#x22;>
      <Play />
    </CodeBlockButton>
  "
  />
</Preview>

## Translations [#translations]

Button/label copy is factored into one `translations` object — the same convention as [`Calendar`](/docs/calendar)'s `translations` prop — rather than separate `copyLabel`/`expandLabel`/etc. props:

```tsx
<CodeBlock
  code="..."
  translations={{ copy: "Duplicate", copied: "Duplicated!" }}
/>
```

## API [#api]

<TypeTable
  type="{
  code: {
    type: &#x22;string&#x22;,
    description: &#x22;Source text to highlight. No children slot — content is Shiki-derived.&#x22;,
  },
  language: {
    type: &#x22;string&#x22;,
    description: 'Shiki-bundled language id (e.g. &#x22;tsx&#x22;, &#x22;bash&#x22;, &#x22;json&#x22;). Omit for a plain, unhighlighted &#x22;text&#x22; grammar.',
  },
  themes: {
    type: &#x22;{ light: string; dark: string }&#x22;,
    default: '{ light: &#x22;github-light&#x22;, dark: &#x22;github-dark-default&#x22; }',
    description: &#x22;Override the light/dark Shiki theme pair.&#x22;,
  },
  tags: {
    type: &#x22;Record<string, { background?: Color; color?: Color; className?: string; style?: string }>&#x22;,
    description: 'Notation-tag → styling lookup — a line carrying &#x22;// [!code x]&#x22; looks up tags.x. See the Notation section above.',
  },
  gutters: {
    type: '({ kind: &#x22;counter&#x22;; startAt?: number; skip?: string[] } | { kind: &#x22;content&#x22;; tags: Record<string, string | true | { text: string | true; color?: Color; background?: Color }> }) & { width?: string; align?: &#x22;left&#x22; | &#x22;center&#x22; | &#x22;right&#x22;; className?: string; ariaHidden?: boolean }[]',
    description: &#x22;Stacked, ordered gutter columns — a counter auto-increments (optionally skipping tagged lines, sized to fit its largest value); a content column renders a matched tag's payload/fixed text, optionally with its own color/background independent of the line's. lineNumbers() is the one-liner preset for a plain counter.&#x22;,
  },
  notation: {
    type: &#x22;boolean&#x22;,
    default: &#x22;true&#x22;,
    description: &#x22;Parse [!code tag] markers. false renders them verbatim instead.&#x22;,
  },
  title: {
    type: &#x22;ReactNode&#x22;,
    description: &#x22;Header row above the code (e.g. a filename). When set, the toolbar sits in the header instead of floating over the code.&#x22;,
  },
  transformers: {
    type: &#x22;ShikiTransformer[]&#x22;,
    description: &#x22;Extra Shiki transformers, run after the internal notation transformer — see Extending Shiki above.&#x22;,
  },
  withCopyButton: {
    type: &#x22;boolean&#x22;,
    default: &#x22;true&#x22;,
    description: &#x22;Render the copy-to-clipboard control.&#x22;,
  },
  copyIcon: {
    type: &#x22;ReactNode | null&#x22;,
    description: &#x22;Override the idle-state copy icon; null hides it.&#x22;,
  },
  copiedIcon: {
    type: &#x22;ReactNode | null&#x22;,
    description: &#x22;Override the copied-state icon; null hides it.&#x22;,
  },
  onCopy: {
    type: &#x22;() => void&#x22;,
    description: &#x22;Fires once per successful copy.&#x22;,
  },
  collapsedLines: {
    type: &#x22;number&#x22;,
    description: &#x22;Lines always visible before the fade + expand seam. Omit to never collapse.&#x22;,
  },
  defaultExpanded: {
    type: &#x22;boolean&#x22;,
    default: &#x22;false&#x22;,
    description: &#x22;Uncontrolled initial expanded state.&#x22;,
  },
  expanded: {
    type: &#x22;boolean&#x22;,
    description: &#x22;Controlled expanded state.&#x22;,
  },
  onExpandedChange: {
    type: &#x22;(expanded: boolean) => void&#x22;,
    description: &#x22;Fires on toggle (controlled or uncontrolled).&#x22;,
  },
  expandIcon: {
    type: &#x22;ReactNode | null&#x22;,
    description: &#x22;Override the collapsed-state affordance icon; null hides it.&#x22;,
  },
  collapseIcon: {
    type: &#x22;ReactNode | null&#x22;,
    description: &#x22;Override the expanded-state affordance icon; null hides it.&#x22;,
  },
  controls: {
    type: &#x22;ReactNode&#x22;,
    description: &#x22;Extra control(s) in the same floating controls row, before the copy button.&#x22;,
  },
  translations: {
    type: &#x22;Partial<{ copy: string; copied: string; expand: string; collapse: string }>&#x22;,
    description: &#x22;Button/label copy, merged over English defaults.&#x22;,
  },
}"
/>

## Accessibility [#accessibility]

The copy trigger's accessible name comes from `translations.copy`/`copied` (default "Copy"/"Copied"). The expand/collapse control is a real `<button aria-expanded>`, and its label text (`translations.expand`/`collapse`) doubles as its accessible name. A `gutters` counter column is `aria-hidden` by default (redundant, noisy if read aloud per line); a content column is left in the accessible tree by default, since its text is meaningful — both are overridable per column via `ariaHidden`.
