A block of source code — a bordered, tinted surface with mono type, an optional filename/copy header, an optional line-number gutter, and Prism syntax highlighting themed from tokens. Sized xs–xl; data-density scales the padding. Compound subcomponents (CodeBlock.Tabs/Header/List/Trigger/Content/Copy) add a tabbed variant that composes the headless Tabs primitive and reuses the tabs component's classes.
Registry-only
No headless primitive — this component ships only as a copied styled file, so there is no Headless mode. primitiv add code-block installs it whichever mode you are reading.
No headless primitive — this one ships only as a copied styled surface, so primitiv add is the only way in, whichever mode you are reading.
Anatomy
Two forms. The block is a single CodeBlock element driven by code/language. The tabbed form composes CodeBlock.Tabs with a header (a CodeBlock.List of CodeBlock.Triggers and a CodeBlock.Copy) and one CodeBlock.Content per tab.
Generated from the copied file’s props type and contract.json. Never hand-maintained.
CodeBlock
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
code* (required)
string
—
The source to render.
filename
string
—
Filename shown in the header; its presence shows the header + copy control.
language
string
—
Prism language id (e.g. tsx, css, bash). Defaults to tsx.
showHeader
boolean
—
Force the header (with the copy control) even without a filename.
showLineNumbers
boolean
—
Show a line-number gutter. Ignored under variant="inline".
variant
"block" | "inline"
block
Presentation. block is the standalone code surface; inline is a chip that sits in a line of prose or beside a label — same highlighting, same syntax palette, no header or gutter.
size
"xs" | "sm" | "md" | "lg" | "xl"
md
Type size; data-density scales the padding further.
wrap
"true" | "false"
true
Whether long lines reflow to the container. Wrapping is the default because a horizontal scrollbar reads worse on a narrow viewport. Turn it off for content whose column alignment IS the content — a tree with aligned trailing // annotations, a table of values — where reflowing a line drops its annotation onto the next row and makes it read as belonging to the row below. The block then scrolls inside its own box.
CodeBlock.Tabs
Headless only — the copied file renders this part for you and exports no separate component, so there is nothing to import under Styled.
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
activationMode
"manual" | "automatic"
"automatic"
When a focused trigger becomes active; see TabsActivationMode.
defaultValue
string
—
Value of the tab active on first render.
dir
"ltr" | "rtl"
—
Reading direction; see TabsReadingDirection. Inherited from the
nearest DirectionProvider when omitted, falling back to "ltr".
lazyMount
boolean
—
When true, a panel's children are not rendered until that tab is first
activated. Once mounted they remain in the DOM across subsequent tab
switches (lazy mount, not unmount-on-hide). Useful for panels that own
expensive initialisation — e.g. a scroll-snap carousel whose initial
scroll position must be set while the panel is visible.
onChange
({ index, name }: TabMetadata) => void
—
Fired on every user-driven activation with the activated tab's metadata.
onValueChange
(value: string) => void
—
Called with the requested value when the user activates a tab.
orientation
"horizontal" | "vertical"
"horizontal"
Layout axis; see TabsOrientation.
size
"xs" | "sm" | "md" | "lg" | "xl"
"md"
Type size for the block; data-density scales the padding.
value
string
—
Value of the currently active tab.
wrap
boolean
true
Whether long lines reflow to the panel. Turn it off for content whose column
alignment is the content; the panel then scrolls horizontally.
CodeBlock.Header
Headless only — the copied file renders this part for you and exports no separate component, so there is nothing to import under Styled.
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
No props of its own.
CodeBlock.List
Headless only — the copied file renders this part for you and exports no separate component, so there is nothing to import under Styled.
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
ariaLabelledBy
string
—
Id of an existing element to use as the tablist's accessible name,
set as aria-labelledby. Mutually exclusive with label.
label
string
—
Accessible name for the tablist, announced as aria-label. Pick a
short, human-readable description of the set (e.g. "Account
sections", not "Tabs"). Mutually exclusive with ariaLabelledBy.
CodeBlock.Trigger
Headless only — the copied file renders this part for you and exports no separate component, so there is nothing to import under Styled.
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
value* (required)
string
—
Identifies this trigger, linking it to the Tabs.Content of the same
value. Not the visible label — use children for that.
asChild
boolean
—
Render the child element instead of the default <button>. All tab
ARIA attributes and event handlers are merged onto the child. The child
must accept a ref. Useful for routing links that need tab semantics.
disabled
boolean
false
Removes the trigger from the roving tab order and marks it
aria-disabled/data-disabled; arrow-key navigation skips it.
ref
Ref<HTMLElement>
—
Ref to the rendered element. Defaults to HTMLButtonElement; when using
asChild, specify the child's element type (e.g. HTMLAnchorElement).
CodeBlock.Content
Headless only — the copied file renders this part for you and exports no separate component, so there is nothing to import under Styled.
Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
code* (required)
string
—
The source for this panel.
value* (required)
string
—
Value of the trigger this panel belongs to.
language
string
—
Prism language id (e.g. tsx, css, bash). Defaults to tsx.
showLineNumbers
boolean
—
Show a line-number gutter.
CodeBlock.Copy
Headless only — the copied file renders this part for you and exports no separate component, so there is nothing to import under Styled.
Extends HTMLButtonElement — every native attribute of that element is accepted and forwarded.
Prop
Type
Default
Description
asChild
boolean
false
Renders the child element instead of a native <button>, merging all
props — aria-, data-, event handlers, ref — onto it via
Slot. type is not forwarded in this mode; the child owns its
own type semantics. See the asChild example on Button.
children
ReactNode
—
Button content. Defaults to the copy/check icon (swapping on success); pass
text (e.g. Copy) for the text form.
ref
Ref<HTMLButtonElement>
—
Allows getting a ref to the component instance.
Once the component unmounts, React will set ref.current to null
(or call the ref with null if you passed a callback ref).
Forwarded to the underlying HTMLButtonElement, or — under
asChild — merged onto the rendered child via Slot.
type
"button" | "submit" | "reset"
"button"
The button's native type attribute, restricted to the three valid
values. Defaults to "button" (not the DOM's own default of
"submit"), so a Button placed inside a <form> never triggers an
accidental submit unless set explicitly.
Styling contract
20 CSS custom properties on .primitiv-code-block — mode-agnostic. These names are the stable surface; the values are not.
Emitted automatically by the headless primitive — style against these rather than adding your own state classes. Grouped by the part that emits them, since most are emitted by more than one.
CodeBlock
className:.primitiv-code-block
Attribute
Value
When
data-line-numbers
""
the line-number gutter is shown
Accessibility
The copy control is a real <button> with an aria-label, and it announces success after a copy — so a keyboard or screen-reader user gets the same one-click affordance and the same confirmation as a mouse user.
The tabbed variant is the Tabs pattern: the triggers share one tab stop and the arrow keys move between them, with each panel wired to its trigger — so a reader tabs into the group once, not through every tool.
Syntax highlighting is colour on top of the code, never the only signal — the code reads correctly with colour off, and the mono face plus the bordered surface carry the “this is code” distinction.
A horizontal scroll (the default over wrap) is keyboard-scrollable, and the region is focusable when it overflows, so a keyboard user can reach code that runs past the edge.
Examples
Every example below reacts to the density control. Currently showing Styled mode.
A code block
The default: a bordered, tinted surface with mono type and Prism syntax highlighting, driven by code and language. wrap decides whether long lines wrap or the block scrolls horizontally — scroll is the default, since wrapping code changes where the line breaks fall.
Pass filename for a header naming the file, with a copy button beside it — the reader can see where the snippet belongs and take it in one click. Use showHeader to force the header (and its copy control) without a filename.
showLineNumbers adds a gutter, for a longer listing you want to reference by line. It is ignored under variant="inline", where there is only one line to number.
The tabbed variant shows the same step across tools — an install command in npm / pnpm / yarn — without stacking four blocks. Compose CodeBlock.Tabs with a CodeBlock.Header (holding a CodeBlock.List of CodeBlock.Triggers and a CodeBlock.Copy) and one CodeBlock.Content per tab. It is the Tabs keyboard model, so the arrow keys move between triggers.
variant="inline" is a single-line highlighted chip for code named mid-sentence when it needs syntax colour — an import path, a typed value. It is InlineCode with highlighting; reach for plain InlineCode when the fragment does not need a language.
Import it with
import{Button}from"@primitiv-ui/react"
.
import{CodeBlock}from"@/components/ui/code-block";<CodeBlockvariant="inline"size="sm"language="tsx"code='import { Button } from "@primitiv-ui/react"'/>