Skip to content
Primitiv home
Framework
Consumption mode

Code Block

stableSource Figma

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.

Playground

Density

Preview

import { Button } from "@primitiv-ui/react";
export function Save() {  return <Button variant="primary" onClick={() => persistEverythingRightNow()}>Save changes</Button>;}
Size
import { CodeBlock } from "@/components/ui/code-block";
<CodeBlock language="tsx" size="md" wrap code={source} />

Density is set by a data-density ancestor — the Context system, not a CodeBlock prop.

Installation

npx primitiv add code-block

Import

import { CodeBlock } from "@/components/ui/code-block";

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

Block

<CodeBlock code={} language="tsx" />

Tabbed

<CodeBlock.Tabs>  <CodeBlock.Header>    <CodeBlock.List>      <CodeBlock.Trigger />    </CodeBlock.List>    <CodeBlock.Copy />  </CodeBlock.Header>  <CodeBlock.Content /></CodeBlock.Tabs>

Props

CodeBlock

Extends HTMLDivElement — every native attribute of that element is accepted and forwarded.

PropTypeDefaultDescription
code (required)stringThe source to render.
filenamestringFilename shown in the header; its presence shows the header + copy control.
languagestringPrism language id (e.g. tsx, css, bash). Defaults to tsx.
showHeaderbooleanForce the header (with the copy control) even without a filename.
showLineNumbersbooleanShow a line-number gutter. Ignored under variant="inline".
variant"block" | "inline"blockPresentation. 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"mdType size; data-density scales the padding further.
wrap"true" | "false"trueWhether 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.

PropTypeDefaultDescription
activationMode"manual" | "automatic""automatic"When a focused trigger becomes active; see TabsActivationMode.
defaultValuestringValue 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".
lazyMountbooleanWhen 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) => voidFired on every user-driven activation with the activated tab's metadata.
onValueChange(value: string) => voidCalled 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.
valuestringValue of the currently active tab.
wrapbooleantrueWhether 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.

PropTypeDefaultDescription
ariaLabelledBystringId of an existing element to use as the tablist's accessible name, set as aria-labelledby. Mutually exclusive with label.
labelstringAccessible 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.

PropTypeDefaultDescription
value (required)stringIdentifies this trigger, linking it to the Tabs.Content of the same value. Not the visible label — use children for that.
asChildbooleanRender 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.
disabledbooleanfalseRemoves the trigger from the roving tab order and marks it aria-disabled/data-disabled; arrow-key navigation skips it.
refRef<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.

PropTypeDefaultDescription
code (required)stringThe source for this panel.
value (required)stringValue of the trigger this panel belongs to.
languagestringPrism language id (e.g. tsx, css, bash). Defaults to tsx.
showLineNumbersbooleanShow 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.

PropTypeDefaultDescription
asChildbooleanfalseRenders 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.
childrenReactNodeButton content. Defaults to the copy/check icon (swapping on success); pass text (e.g. Copy) for the text form.
refRef<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

--primitiv-code-block-background--primitiv-code-block-color--primitiv-code-block-border-color--primitiv-code-block-border-width--primitiv-code-block-radius--primitiv-code-block-padding--primitiv-code-block-header-padding-block--primitiv-code-block-font-family--primitiv-code-block-font-size--primitiv-code-block-line-height--primitiv-code-block-gutter-color--primitiv-code-block-filename-color--primitiv-code-block-tabs-header-padding--primitiv-code-syntax-keyword--primitiv-code-syntax-function--primitiv-code-syntax-string--primitiv-code-syntax-property--primitiv-code-syntax-number--primitiv-code-syntax-comment--primitiv-code-syntax-punctuation

Data attributes

CodeBlock

className: .primitiv-code-block

AttributeValueWhen
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

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.

Density
import { Button } from "@primitiv-ui/react";
export function Save() {  return <Button variant="primary" onClick={() => persistEverythingRightNow()}>Save changes</Button>;}
import { CodeBlock } from "@/components/ui/code-block";
<CodeBlock language="tsx" code={source} />

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.

Density
Save.tsx
import { Button } from "@primitiv-ui/react";
export function Save() {  return <Button variant="primary" onClick={() => persistEverythingRightNow()}>Save changes</Button>;}
import { CodeBlock } from "@/components/ui/code-block";
<CodeBlock filename="Save.tsx" language="tsx" code={source} />

Line numbers

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.

Density
import { Button } from "@primitiv-ui/react";
export function Save() {  return <Button variant="primary" onClick={() => persistEverythingRightNow()}>Save changes</Button>;}
import { CodeBlock } from "@/components/ui/code-block";
<CodeBlock showLineNumbers language="tsx" code={source} />

Tabbed (one snippet, several tools)

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.

Density
npx primitiv add button
import { CodeBlock } from "@/components/ui/code-block";
<CodeBlock.Tabs defaultValue="npm">  <CodeBlock.Header>    <CodeBlock.List label="Install with">      <CodeBlock.Trigger value="npm">npm</CodeBlock.Trigger>      <CodeBlock.Trigger value="pnpm">pnpm</CodeBlock.Trigger>    </CodeBlock.List>    <CodeBlock.Copy />  </CodeBlock.Header>  <CodeBlock.Content value="npm" language="bash" code="npx primitiv add button" />  <CodeBlock.Content value="pnpm" language="bash" code="pnpm dlx primitiv add button" /></CodeBlock.Tabs>

Inline

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.

Density
Import it with
import { Button } from "@primitiv-ui/react"
.
import { CodeBlock } from "@/components/ui/code-block";
<CodeBlock variant="inline" size="sm" language="tsx" code='import { Button } from "@primitiv-ui/react"' />