---
title: Code Block
description: A highlighted code slab in the CLI's colours, with a copy button that confirms in place.
source: https://adamjurek.com/components/code-block
---

## Home


Code on this site is a terminal, whichever theme the page is in: a warm
charcoal slab that does not invert, with a lit top edge and a contact shadow so
it sits *on* the paper rather than tinting a region of it.

The syntax palette is the CLI's own xterm-256 hues, so a command pasted from
the terminal into a fence keeps its colours.

<!-- ::start:showcase demo="code-block" height="380" -->
<!-- ::end:showcase -->

## Why it is built this way

Highlighting is synchronous, so a page full of these renders during SSR and
nothing re-highlights on hydration - only the copy button is live. Before this
component existed the same highlighted markup was built in two places
(`MarkdownView` for fences, the showcase block for demo source), and the copy
button would have made it three. One component, one slab, one palette.

The colours are semantic tokens - `--syn-keyword`, `--syn-string`,
`--syn-command` and friends - defined once in the stylesheet's `:root`. A shell
fence gets the deeper ground and its command in the CLI's brand orange, because
"type this" and "read this" are different asks and should read differently
before the first word does.

## What it does not do

It does not scroll its button. The copy chip sits on a `code-shell` wrapper
outside the scrolling content, so a long line scrolls under it. It does not
load grammars dynamically - the languages are the ones this site's content
uses, registered by hand in `highlighter.ts`, and adding one is a one-line,
noticeable change.

> [!NOTE] Install commands are not written here
> Anything in `packages/ui/registry.ts` gets its TanStack and shadcn commands
> attached automatically, so there is nothing to keep in sync.


## Install

<!-- ::start:tabs -->

### TanStack

```shell
tanstack add https://adamjurek.com/r/tanstack/code-block.json
```

### shadcn

```shell
pnpm dlx shadcn@latest add https://adamjurek.com/r/shadcn/code-block.json
```

### pnpm

```shell
pnpm add @sushindustries/ui @sushindustries/atoms
```

<!-- ::end:tabs -->

## What you get

| | |
| --- | --- |
| Version | 0.1.0 |
| Category | content · Rendering |
| Files | `code-block.tsx`, `highlighter.ts` |
| Dependencies | `@tanstack/highlight@0.0.10` |
| Also installs | `icon`, `copy-button` |
| Tags | code, highlight, copy, ssr |


## Get Started


Install commands are on Home, attached from the registry - they are not written
here, because a second copy is a copy that goes stale. This tab starts after the
install worked.

## Use it

```tsx
import { CodeBlock } from "@sushindustries/ui";

export function Example() {
	return (
		<CodeBlock
			code={`export function greet(name: string): string {\n\treturn \`hello, \${name}\`;\n}`}
			language="ts"
		/>
	);
}
```

## What you should see

A warm charcoal slab with a lit top edge, the code coloured in the CLI's own
syntax palette, and a language chip and copy button in the bottom right. The
trailing newline in `code` is dropped before rendering, so what the block
shows and what the copy button puts on the clipboard are exactly the same
text.

## If nothing happens

An unrecognised `language` does not error - it falls back to `plaintext` and
renders unstyled but otherwise correct. If the copy button or the language
chip is missing entirely, check that `icon` and `copy-button` are installed
alongside this component; `registry.ts` lists both as dependencies rather than
bundling them in.


## Guides


## Showing a filename

```tsx
<CodeBlock code={source} language="ts" file="package.json" />
```

The filename renders in a strip above the slab, with a small file glyph.
Passing `file` also switches the block into its wrapped form (`code-shell`),
the same one the copy button and language chip use, so a fence with a name on
it and a fence with a copy button look consistent even when only one of the
two is present.

## Turning the copy button off

```tsx
<CodeBlock code={source} language="json" copy={false} />
```

Reach for `copy={false}` when a fence is a short fragment being read rather
than pasted - a one-line diff inline in a sentence, say. With both `copy` and
`file` unset, `CodeBlock` renders the bare highlighted slab and skips the
wrapper entirely.

## Registering a language

`language` only resolves to something styled if it is listed by hand in
`highlighter.ts`, which is the grammars this site's own content actually
uses, plus a handful of aliases so a fence can be labelled the way people
actually write it:

```ts
languages: [ts, tsx, shell, json, css, plaintext];
```

A language outside that list is not a bug to fix in this component - it is a
one-line addition to the registered set, made on purpose rather than by
pulling in every grammar a highlighter ships.

## When not to use it

A block of code that is never shown, only copied - an install command with no
educational value in reading it - is closer to `CopyButton` wrapped around
plain text than to this.


## API


<!-- generated:api -->

## Props

| Prop | Type | Default | Does |
| --- | --- | --- | --- |
| `code` | `string` | - | The source. One trailing newline is dropped, so the copy is exactly what is shown. |
| `language?` | `string` | - | Fence language. Aliases like `bash` and `js` resolve; unknown falls back to plaintext. |
| `copy?` | `boolean` | `true` | The copy button is the default; a caller showing a fragment can decline it. |
| `file?` | `string` | - | Filename from the fence's `file="..."` metadata, shown above the code. |

<!-- /generated:api -->

## Notes

`copy` and `file` both being unset is the only case that skips the
`code-shell` wrapper and its tools row entirely - set either one and you get
the wrapper, the language chip, and whichever of the two you asked for.

`language` never throws. An alias resolves through `resolveLanguage`
(`bash`, `js`, `jsx` and a few others); anything else that is not a
registered grammar falls back to `plaintext`, rendered but unstyled, rather
than failing the build or the render.


## Examples


Examples are the tab where the component is shown doing a job, not
demonstrating a prop. The API tab already lists the props.

<!-- ::start:showcase demo="code-block" height="420" -->
<!-- ::end:showcase -->

Press Compare. The frames are real viewports, so a layout that breaks at 320
breaks here too rather than in somebody's hands.

## In a page

```tsx
import { CodeBlock } from "@sushindustries/ui";

export function InstallStep({ command }: { command: string }) {
	return (
		<section className="flex col gap-2">
			<p className="fg-dim m-0 text-sm">Then install it:</p>
			<CodeBlock code={command} language="bash" />
		</section>
	);
}
```

## What this example is not

Not proof that highlighting is free. It is synchronous and runs during SSR,
which is why a whole page of these costs nothing on hydration - but a page
that renders hundreds of blocks at once is still doing hundreds of synchronous
highlight passes on the server, on every request.
