Code snippets

Text a reader copies exactly: one shiki-highlighted block with copy, expand, line numbers, and collapse, plus a tabbed form. Three sheet types, no size axis.

Demo

The sheet’s three types on one select — horizontal, vertical, vertical with tabs. There is no type prop: each is the prop combination the snippet shows.

import { Button } from "@/components/base/buttons/button"; export const SaveBar = ({ onSave }: { onSave: () => void }) => ( <div className="flex justify-end gap-3"> <Button color="secondary" size="sm"> Cancel </Button> <Button size="sm" onClick={onSave}> Save changes </Button> </div> );
type
language
import { CodeSnippet } from "@/components/application/code-snippet/code-snippet"; const code = `import { Button } from "@/components/base/buttons/button"; export const SaveBar = ({ onSave }: { onSave: () => void }) => ( <div className="flex justify-end gap-3"> <Button color="secondary" size="sm"> Cancel </Button> <Button size="sm" onClick={onSave}> Save changes </Button> </div> );`; <CodeSnippet code={code} language="tsx" />

The modern toggle starts on each type’s own default

modern defaults to !showLineNumbers, so picking a type seats the toggle on what that type already derives — false for horizontal, true for vertical — and the snippet emits the prop only when you flip it against the grain, which is the off-sheet combination the guidelines warn about. The copied code carries the sample as a template literal, because code is a string and a one-element snippet has nowhere else to put it.

Contextual demo

A snippet is read next to what it documents. The preview puts the three types in an API reference and a settings card.

This page is also a consumer

The code box under every playground on this site, including the one above, is this component: language="tsx" with modern={false} and line numbers, rendered by _components/playground.tsx at line 268. The gray blocks on the Code tab are not: CodeBlock in _components/prose.tsx is a plain pre and code with no highlighting and no copy button, read from that file. One live consumer in this repo, then, rendered on every component page that has a playground.

The family

One block, four tabs parts reachable off it, and the highlight function it calls. All six come from one module.

The components in this family, with their descriptions and import paths
ComponentDescriptionImport from
CodeSnippetThe block: highlights a string on the client or renders pre-highlighted nodes, with a copy button, an optional expand button, an optional line-number gutter, and an optional collapse.@/​components/​application/​code-snippet/​code-snippet
CodeSnippet.TabsThe tabbed root. Owns the selection and the outer tray, forces every block inside it into the modern style, and shares a ref so the header’s copy button can find the active panel.@/​components/​application/​code-snippet/​code-snippet
CodeSnippet.TabListThe tab bar with the header copy button at its end. The button copies the text of whichever panel is showing.@/​components/​application/​code-snippet/​code-snippet
CodeSnippet.TabOne label. Quaternary at rest, primary when selected or hovered, with a 2px focus ring 2px outside.@/​components/​application/​code-snippet/​code-snippet
CodeSnippet.TabPanelThe content for one tab, matched to it by id, with the 8px inset the tray needs around a block.@/​components/​application/​code-snippet/​code-snippet
highlightThe async function the block calls itself: shiki’s codeToHast with the kit’s two themes and color replacements, returned as React nodes. Exported from a “use client” module, so a server component gets a client reference it cannot call; the server path the children prop describes needs the same codeToHast call in a server module of your own.@/​components/​application/​code-snippet/​code-snippet

Built from

Nine pieces make up the block; four have their own docs page.

Component
Where it’s used here
ButtonUtilityThe action buttons, xs tertiary with a tooltip that doubles as the accessible name: Copy on every block and in the tabs header, Expand when onExpand is given.
TooltipReached through ButtonUtility’s tooltip prop, which sets the button’s aria-label to the same string, so the name is there whether or not the tooltip opens.
ButtonThe Show more and Show less button, sm secondary.
@untitledui/iconsCopy01 at rest, Check for 2 seconds after a copy, and Maximize01 on the Expand button since 2026-08-23.

The variant space

The sheet’s three types at its one size, all live: horizontal, vertical, and vertical with tabs.

import { Button } from "@/components/base/buttons/button"; export const SaveBar = ({ onSave }: { onSave: () => void }) => ( <div className="flex justify-end gap-3"> <Button color="secondary" size="sm"> Cancel </Button> <Button size="sm" onClick={onSave}> Save changes </Button> </div> );
horizontal: the default. showLineNumbers true, modern unset
whsec_4f8a2c91e7b34d6f0a1c5e2d9b7f3a6e
vertical: showLineNumbers false, so the tray follows
npm i @untitledui/icons
vertical with tabs: CodeSnippet.Tabs, one panel per package manager

Line numbers pick the container

Read from the source: isModern is isInsideTabs || (modern ?? !showLineNumbers). The horizontal type is a ring-1 ring-secondary ring-inset block on bg-primary with no shadow; the vertical type is a shadow-lg block on bg-primary_alt with a ring-secondary_alt ring, inside a rounded-[20px] p-2 tray on bg-secondary_alt with shadow-xs. Inside tabs the block skips its own tray and is forced modern, but showLineNumbers still defaults to true there, which is why the install panels above pass it false. Without the gutter the content gets p-5; with it, the gutter’s own 20px above the first line and below the last do the same job.

Collapsing and expanding

Two affordances, two triggers: maxHeight collapses the block in place; onExpand adds a button that hands off to you.

import { headers } from "next/headers"; import { NextResponse } from "next/server"; import { verifySignature } from "@/lib/webhooks"; export async function POST(request: Request) { const signature = (await headers()).get("x-signature") ?? ""; const body = await request.text(); if (!verifySignature(body, signature, process.env.WEBHOOK_SECRET)) { return NextResponse.json({ error: "Bad signature" }, { status: 401 }); } const event = JSON.parse(body) as { type: string; data: unknown }; switch (event.type) { case "invoice.paid": await markInvoicePaid(event.data); break; case "customer.deleted": await removeCustomer(event.data); break; } return NextResponse.json({ received: true }); }
maxHeight 160: the handler is 25 lines, so it clips, fades, and shows the Show more button
.line-numbers .shiki code .line::before { content: counter(step); counter-increment: step; width: 49px; margin-inline-end: 20px; text-align: end; }
onExpand given: the Expand button renders before Copy. Press it

What the collapse is made of

Read from the source. The content wrapper gets max-height from the prop and, while collapsed, overflow-y-hidden; an effect compares its scrollHeight to maxHeight − 10 and nothing collapses under that. Over it, an absolutely positioned 160px overlay paints the fade and holds a Button sm secondary 24px up from the bottom, which reads as 32px from the tray’s outer edge on the vertical type. Pressing it sets max-height: none and turns the overlay into a 68px sticky strip with Show less: the sheet draws no Show less, so that state is the kit’s own. The fade mixes the block’s background token through color-mix over 16 stops since 2026-08-23; before that it was hard-coded white and a gray-950 this theme never uses, so it never blended in dark mode.

Languages

Four of the kit’s union. Shiki’s web bundle grammar; github themes with five values swapped for DrumKit tokens.

import { Button } from "@/components/base/buttons/button"; export const SaveBar = ({ onSave }: { onSave: () => void }) => ( <div className="flex justify-end gap-3"> <Button color="secondary" size="sm"> Cancel </Button> <Button size="sm" onClick={onSave}> Save changes </Button> </div> );
language="tsx"
npm i @untitledui/icons npx untitledui add code-snippet npm run dev
language="bash"
{ "name": "drumkit", "private": true, "scripts": { "dev": "next dev", "build": "next build", "test": "vitest run" } }
language="json"
.line-numbers .shiki code .line::before { content: counter(step); counter-increment: step; width: 49px; margin-inline-end: 20px; text-align: end; }
language="css"

Five replacements per theme, and what they leave alone

Read from highlight: github’s default text becomes text-primary, constants and support utility-blue-600, keywords and storage utility-pink-600, entities utility-brand-600, and strings text-primary, in both themes. Everything else keeps github’s own value: comments stay #6a737d, tag names stay green, and variables stay orange, which is why the tsx sample above has two colors no DrumKit token names. The sheet is inconsistent on comments itself, teal on one symbol and slate on the other, so that row stays recorded on the Code tab rather than fixed.

Client-side, with a server path

The block calls highlight in a useLayoutEffect, so the first paint is nodes ?? code ?? <p>Loading...</p>: the raw string in the block’s monospace, without the gutter, which appears when the async pass lands. “Loading...” itself only shows when neither prop is given. To skip the client pass, build the nodes on the server and pass them as children; they are the initial state, and if code is also given the client pass replaces them anyway. Not through highlight itself, though: the module is "use client", so a server component gets a client reference it cannot call, and the server pass needs the same codeToHast call in a server module of your own. The Code tab has the example.

States

Copied, hovered, and focused on the buttons; selected, hovered, and focused on the tabs.

npm i @untitledui/icons npx untitledui add code-snippet npm run dev
Copied: press Copy and the glyph is Check for 2 seconds while a hidden live region says “Copied!”. The buttons are visible at rest since 2026-08-23
npm i @untitledui/icons
Tab states: tab into the bar and use the arrow keys. Selected and hovered are both text-primary; focus adds a 2px ring 2px outside

Selection is color-only, on the sheet as in the kit

Read from CodeSnippetTab: the label is text-quaternary and turns text-primary when isSelected || isHovered, with no fill and no rule, which is also what the sheet’s six tab symbols draw. The focus ring is outline-2 in outline-focus-ring at outline-offset-2, and focus alone does not recolor an unselected tab. aria-selected carries the state for assistive tech; a sighted keyboard user reads it from color. The buttons’ own states are ButtonUtility’s: a bg-primary_hover fill on hover and the same 2px ring on focus.