- Accordion
- Alert
- Alert Dialog
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Chart
- Checkbox
- Code Block
- Collapsible
- Combobox
- Command
- Context Menu
- Data Table
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- Form
- Hover Card
- Input
- Input Group
- Input OTP
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Native Select
- Navigation Menu
- Pagination
- Popover
- Progress
- Questionnaire
- Radio Group
- Resizable
- Scroll Area
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Sonner
- Spinner
- Switch
- Table
- Tabs
- Textarea
- Toast
- Toggle
- Toggle Group
- Tooltip
- Trace
Displays code, terminal output and diffs with an optional header, line numbers and a copy button.
import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages, })
return result.toUIMessageStreamResponse()}"use client"
import { FileCodeIcon } from "lucide-react"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/code-block"
const source = `import { streamText } from "ai"
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: "anthropic/claude-sonnet-4.5",
messages,
})
return result.toUIMessageStreamResponse()
}`
export function CodeBlockDemo() {
const lines = useHighlightedCode(source, "ts")
return (
<CodeBlock className="max-w-lg">
<CodeBlockHeader>
<CodeBlockTitle>
<FileCodeIcon />
app/api/chat/route.ts
</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={source} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent lineNumbers>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}CodeBlock displays code, terminal output, logs and diffs. It has a header with a title and actions, line numbers, wrapping, diff and highlighted lines, and a small size for tool output in a Trace.
Syntax highlighting is optional and lives in a separate hook, so a block that only shows plain output stays small.
The block is a well sunk into the page, the Kbd's recess at panel scale: a soft shadow under the top lip and a 1px catch-light under the bottom edge, on a fill one step below the background in dark mode (light keeps background, so every highlighted token stays at 4.5:1 or above). Focusing the code draws the Input's ring.
Installation#
pnpm dlx shadcn@latest add https://ui.ikramhasan.com/r/code-block.json
code-block also installs useHighlightedCode for syntax highlighting.
Usage#
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockFooter,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/code-block"<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>Terminal</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={output} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>{output}</CodeBlockContent>
</CodeBlock>Composition#
CodeBlock
├── CodeBlockHeader
│ ├── CodeBlockTitle
│ └── CodeBlockActions
│ └── CodeBlockCopyButton
├── CodeBlockActions (floating, when there is no header)
├── CodeBlockContent
│ └── CodeBlockLine
└── CodeBlockFooterCodeBlockContent takes a string or a list of CodeBlockLine. Use lines when you need line variants or highlighted tokens.
Features#
- It streams cheaply. A string child renders as one text node, and the highlighter tokenizes only the new lines as code arrives.
- It copies cleanly. Line numbers and diff markers are left out of a selection, and blank lines survive the copy.
- It reads left to right. The content is always
dir="ltr". The header follows the page direction. - It highlights without WASM.
useHighlightedCodeuses Shiki's JavaScript engine, loads each language on first use and renders ANSI colors in terminal output. On the server,highlightCodekeeps Shiki out of the client bundle. - It fits where code shows up. Use it in a message, a Trace line, Tabs, a Dialog or a Sheet.
- It scrolls by keyboard. The content is focusable, and floating actions show on focus as well as on hover.
Basic#
Pass a string to CodeBlockContent. It renders as one text node, so it stays cheap while output streams.
$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
const install = `$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0`
export function CodeBlockBasic() {
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent>{install}</CodeBlockContent>
</CodeBlock>
)
}Header#
CodeBlockHeader holds a CodeBlockTitle and CodeBlockActions. CodeBlockCopyButton copies its value.
$ pnpm build
> next build
▲ Next.js 16.0.0
Creating an optimized production build ...
✓ Compiled successfully in 4.2s
✓ Generating static pages (12/12)
Route (app) Size First Load JS
┌ ○ / 1.9 kB 112 kB
├ ○ /_not-found 993 B 103 kB
└ ƒ /api/chat 141 B 102 kBimport { TerminalIcon } from "lucide-react"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockHeader,
CodeBlockTitle,
} from "@/components/ui/code-block"
const build = `$ pnpm build
> next build
▲ Next.js 16.0.0
Creating an optimized production build ...
✓ Compiled successfully in 4.2s
✓ Generating static pages (12/12)
Route (app) Size First Load JS
┌ ○ / 1.9 kB 112 kB
├ ○ /_not-found 993 B 103 kB
└ ƒ /api/chat 141 B 102 kB`
export function CodeBlockHeaderExample() {
return (
<CodeBlock className="max-w-lg">
<CodeBlockHeader>
<CodeBlockTitle>
<TerminalIcon />
Terminal
</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={build} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>{build}</CodeBlockContent>
</CodeBlock>
)
}A long title truncates. To link the title to a file, wrap CodeBlockTitle in an anchor.
<CodeBlockHeader>
<a href="/files/route.ts">
<CodeBlockTitle>app/api/chat/route.ts</CodeBlockTitle>
</a>
</CodeBlockHeader>Actions#
CodeBlockActions holds any number of buttons. For a wrap toggle, keep the state yourself and pass it to wrap.
TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)"use client"
import * as React from "react"
import { DownloadIcon, WrapTextIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockHeader,
CodeBlockTitle,
} from "@/components/ui/code-block"
const error = `TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)`
function download(code: string, filename: string) {
const url = URL.createObjectURL(new Blob([code], { type: "text/plain" }))
const link = document.createElement("a")
link.href = url
link.download = filename
link.click()
URL.revokeObjectURL(url)
}
export function CodeBlockActionsExample() {
const [wrap, setWrap] = React.useState(false)
return (
<CodeBlock className="max-w-lg">
<CodeBlockHeader>
<CodeBlockTitle>error.log</CodeBlockTitle>
<CodeBlockActions>
<Button
variant="ghost"
size="icon-xs"
aria-label="Wrap lines"
aria-pressed={wrap}
className="aria-pressed:bg-accent aria-pressed:[&_svg]:text-foreground"
onClick={() => setWrap(!wrap)}
>
<WrapTextIcon />
</Button>
<Button
variant="ghost"
size="icon-xs"
aria-label="Download"
onClick={() => download(error, "error.log")}
>
<DownloadIcon />
</Button>
<CodeBlockCopyButton value={error} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent wrap={wrap}>{error}</CodeBlockContent>
</CodeBlock>
)
}const [wrap, setWrap] = React.useState(false)
<CodeBlockActions>
<Button
variant="ghost"
size="icon-xs"
aria-label="Wrap lines"
aria-pressed={wrap}
onClick={() => setWrap(!wrap)}
>
<WrapTextIcon />
</Button>
<CodeBlockCopyButton value={output} />
</CodeBlockActions>
<CodeBlockContent wrap={wrap}>{output}</CodeBlockContent>Floating Actions#
Without a header, put CodeBlockActions directly inside CodeBlock. The actions float at the top end and stay hidden until the block is hovered or focused. They always show on touch screens.
$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0pnpm dlx shadcn@latest add code-blockimport {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
} from "@/components/ui/code-block"
const install = `$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0`
export function CodeBlockFloatingActions() {
return (
<div className="flex w-full max-w-lg flex-col gap-4">
<CodeBlock>
<CodeBlockActions>
<CodeBlockCopyButton value={install} />
</CodeBlockActions>
<CodeBlockContent>{install}</CodeBlockContent>
</CodeBlock>
<CodeBlock size="sm">
<CodeBlockActions>
<CodeBlockCopyButton value="pnpm dlx shadcn@latest add code-block" />
</CodeBlockActions>
<CodeBlockContent>
pnpm dlx shadcn@latest add code-block
</CodeBlockContent>
</CodeBlock>
</div>
)
}Line Numbers#
Add lineNumbers to CodeBlockContent. A string child is split into lines, and the numbers are not copied with a selection.
import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages, })
return result.toUIMessageStreamResponse()}import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
const source = `import { streamText } from "ai"
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: "anthropic/claude-sonnet-4.5",
messages,
})
return result.toUIMessageStreamResponse()
}`
export function CodeBlockLineNumbers() {
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent lineNumbers>{source}</CodeBlockContent>
</CodeBlock>
)
}Start Line#
Set startLine to number lines from a given line, for a snippet from the middle of a file. The gutter grows to fit the widest number.
if (!session) { return new Response("Unauthorized", { status: 401 }) }
const { messages } = await request.json() const result = streamText({ model, messages })
return result.toUIMessageStreamResponse()import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
const middle = ` if (!session) {
return new Response("Unauthorized", { status: 401 })
}
const { messages } = await request.json()
const result = streamText({ model, messages })
return result.toUIMessageStreamResponse()`
export function CodeBlockStartLine() {
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent lineNumbers startLine={96}>
{middle}
</CodeBlockContent>
</CodeBlock>
)
}Wrap#
Long lines scroll by default. Add wrap to wrap them, which suits tool output and error messages. To let the reader toggle it, see Actions.
TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
const error = `TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)`
export function CodeBlockWrap() {
return (
<CodeBlock className="max-w-md">
<CodeBlockContent wrap>{error}</CodeBlockContent>
</CodeBlock>
)
}Highlighted Lines#
Render each line as a CodeBlockLine and set variant="highlighted" on the lines to call out.
import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages, })
return result.toUIMessageStreamResponse()}import {
CodeBlock,
CodeBlockContent,
CodeBlockLine,
} from "@/components/ui/code-block"
const source = `import { streamText } from "ai"
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: "anthropic/claude-sonnet-4.5",
messages,
})
return result.toUIMessageStreamResponse()
}`
const highlighted = [6, 7, 8, 9]
export function CodeBlockHighlightedLines() {
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent lineNumbers>
{source.split("\n").map((line, index) => (
<CodeBlockLine
key={index}
variant={
highlighted.includes(index + 1) ? "highlighted" : "default"
}
>
{line}
</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}Diff#
Set variant="added" or variant="removed" on a CodeBlockLine. Each one adds a + or − marker.
export function Greeting({ name }: Props) { return <p>Hello, {name}</p> return <p className="text-sm">Hello, {name}!</p>}import {
CodeBlock,
CodeBlockContent,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/code-block"
const diff = [
{ variant: "default", text: "export function Greeting({ name }: Props) {" },
{ variant: "removed", text: " return <p>Hello, {name}</p>" },
{
variant: "added",
text: ' return <p className="text-sm">Hello, {name}!</p>',
},
{ variant: "default", text: "}" },
] as const
export function CodeBlockDiff() {
return (
<CodeBlock className="max-w-lg">
<CodeBlockHeader>
<CodeBlockTitle>components/greeting.tsx</CodeBlockTitle>
<span className="ms-auto text-xs tabular-nums">
<span className="text-success">+1</span>{" "}
<span className="text-destructive">−1</span>
</span>
</CodeBlockHeader>
<CodeBlockContent lineNumbers>
{diff.map((line, index) => (
<CodeBlockLine key={index} variant={line.variant}>
{line.text}
</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}Height#
Cap the height with a max-h-* class on CodeBlockContent. The content scrolls inside the block.
[01] GET /api/chat 200 in 12ms
[02] GET /api/chat 200 in 19ms
[03] GET /api/chat 200 in 26ms
[04] GET /api/chat 200 in 33ms
[05] GET /api/chat 200 in 40ms
[06] GET /api/chat 200 in 47ms
[07] GET /api/chat 200 in 14ms
[08] GET /api/chat 200 in 21ms
[09] GET /api/chat 200 in 28ms
[10] GET /api/chat 200 in 35ms
[11] GET /api/chat 200 in 42ms
[12] GET /api/chat 200 in 49ms
[13] GET /api/chat 200 in 16ms
[14] GET /api/chat 200 in 23ms
[15] GET /api/chat 200 in 30ms
[16] GET /api/chat 200 in 37ms
[17] GET /api/chat 200 in 44ms
[18] GET /api/chat 200 in 51ms
[19] GET /api/chat 200 in 18ms
[20] GET /api/chat 200 in 25ms
[21] GET /api/chat 200 in 32ms
[22] GET /api/chat 200 in 39ms
[23] GET /api/chat 200 in 46ms
[24] GET /api/chat 200 in 13ms
[25] GET /api/chat 200 in 20ms
[26] GET /api/chat 200 in 27ms
[27] GET /api/chat 200 in 34ms
[28] GET /api/chat 200 in 41ms
[29] GET /api/chat 200 in 48ms
[30] GET /api/chat 200 in 15ms
[31] GET /api/chat 200 in 22ms
[32] GET /api/chat 200 in 29ms
[33] GET /api/chat 200 in 36ms
[34] GET /api/chat 200 in 43ms
[35] GET /api/chat 200 in 50ms
[36] GET /api/chat 200 in 17ms
[37] GET /api/chat 200 in 24ms
[38] GET /api/chat 200 in 31ms
[39] GET /api/chat 200 in 38ms
[40] GET /api/chat 200 in 45msimport { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
const log = Array.from(
{ length: 40 },
(_, index) =>
`[${String(index + 1).padStart(2, "0")}] GET /api/chat 200 in ${12 + ((index * 7) % 40)}ms`
).join("\n")
export function CodeBlockHeight() {
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent className="max-h-60 scroll-fade-y">
{log}
</CodeBlockContent>
</CodeBlock>
)
}CodeBlockContent fades its edges with scroll-fade (from shadcn/tailwind.css) as it scrolls: left and right by default, top and bottom when it wraps. Add scroll-fade-y to a capped block that does not wrap.
Show More#
CodeBlockFooter is a slot below the content. For Show More, cap the content while it is collapsed and put a ghost xs Button in the footer.
[01] GET /api/chat 200 in 12ms
[02] GET /api/chat 200 in 19ms
[03] GET /api/chat 200 in 26ms
[04] GET /api/chat 200 in 33ms
[05] GET /api/chat 200 in 40ms
[06] GET /api/chat 200 in 47ms
[07] GET /api/chat 200 in 14ms
[08] GET /api/chat 200 in 21ms
[09] GET /api/chat 200 in 28ms
[10] GET /api/chat 200 in 35ms
[11] GET /api/chat 200 in 42ms
[12] GET /api/chat 200 in 49ms
[13] GET /api/chat 200 in 16ms
[14] GET /api/chat 200 in 23ms
[15] GET /api/chat 200 in 30ms
[16] GET /api/chat 200 in 37ms
[17] GET /api/chat 200 in 44ms
[18] GET /api/chat 200 in 51ms
[19] GET /api/chat 200 in 18ms
[20] GET /api/chat 200 in 25ms
[21] GET /api/chat 200 in 32ms
[22] GET /api/chat 200 in 39ms
[23] GET /api/chat 200 in 46ms
[24] GET /api/chat 200 in 13ms
[25] GET /api/chat 200 in 20ms
[26] GET /api/chat 200 in 27ms
[27] GET /api/chat 200 in 34ms
[28] GET /api/chat 200 in 41ms
[29] GET /api/chat 200 in 48ms
[30] GET /api/chat 200 in 15ms
[31] GET /api/chat 200 in 22ms
[32] GET /api/chat 200 in 29ms
[33] GET /api/chat 200 in 36ms
[34] GET /api/chat 200 in 43ms
[35] GET /api/chat 200 in 50ms
[36] GET /api/chat 200 in 17ms
[37] GET /api/chat 200 in 24ms
[38] GET /api/chat 200 in 31ms
[39] GET /api/chat 200 in 38ms
[40] GET /api/chat 200 in 45ms"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
CodeBlock,
CodeBlockContent,
CodeBlockFooter,
} from "@/components/ui/code-block"
const log = Array.from(
{ length: 40 },
(_, index) =>
`[${String(index + 1).padStart(2, "0")}] GET /api/chat 200 in ${12 + ((index * 7) % 40)}ms`
).join("\n")
export function CodeBlockShowMore() {
const [expanded, setExpanded] = React.useState(false)
const id = React.useId()
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent
id={id}
className={
expanded ? undefined : "max-h-48 overflow-y-hidden scroll-fade-y"
}
>
{log}
</CodeBlockContent>
<CodeBlockFooter className="justify-center">
<Button
variant="ghost"
size="xs"
aria-expanded={expanded}
aria-controls={id}
onClick={() => setExpanded(!expanded)}
>
{expanded ? "Show less" : "Show more"}
<ChevronDownIcon
data-icon="inline-end"
className="transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] group-aria-expanded/button:rotate-180 motion-reduce:transition-none"
/>
</Button>
</CodeBlockFooter>
</CodeBlock>
)
}const [expanded, setExpanded] = React.useState(false)
const id = React.useId()
<CodeBlockContent
id={id}
className={expanded ? undefined : "max-h-64 overflow-y-hidden scroll-fade-y"}
>
{code}
</CodeBlockContent>
<CodeBlockFooter className="justify-center">
<Button
variant="ghost"
size="xs"
aria-expanded={expanded}
aria-controls={id}
onClick={() => setExpanded(!expanded)}
>
{expanded ? "Show less" : "Show more"}
</Button>
</CodeBlockFooter>Max Height#
To keep a long block from taking over the page, raise the cap instead of removing it. The expanded content scrolls, and the edges fade as you scroll.
[01] GET /api/chat 200 in 12ms
[02] GET /api/chat 200 in 19ms
[03] GET /api/chat 200 in 26ms
[04] GET /api/chat 200 in 33ms
[05] GET /api/chat 200 in 40ms
[06] GET /api/chat 200 in 47ms
[07] GET /api/chat 200 in 14ms
[08] GET /api/chat 200 in 21ms
[09] GET /api/chat 200 in 28ms
[10] GET /api/chat 200 in 35ms
[11] GET /api/chat 200 in 42ms
[12] GET /api/chat 200 in 49ms
[13] GET /api/chat 200 in 16ms
[14] GET /api/chat 200 in 23ms
[15] GET /api/chat 200 in 30ms
[16] GET /api/chat 200 in 37ms
[17] GET /api/chat 200 in 44ms
[18] GET /api/chat 200 in 51ms
[19] GET /api/chat 200 in 18ms
[20] GET /api/chat 200 in 25ms
[21] GET /api/chat 200 in 32ms
[22] GET /api/chat 200 in 39ms
[23] GET /api/chat 200 in 46ms
[24] GET /api/chat 200 in 13ms
[25] GET /api/chat 200 in 20ms
[26] GET /api/chat 200 in 27ms
[27] GET /api/chat 200 in 34ms
[28] GET /api/chat 200 in 41ms
[29] GET /api/chat 200 in 48ms
[30] GET /api/chat 200 in 15ms
[31] GET /api/chat 200 in 22ms
[32] GET /api/chat 200 in 29ms
[33] GET /api/chat 200 in 36ms
[34] GET /api/chat 200 in 43ms
[35] GET /api/chat 200 in 50ms
[36] GET /api/chat 200 in 17ms
[37] GET /api/chat 200 in 24ms
[38] GET /api/chat 200 in 31ms
[39] GET /api/chat 200 in 38ms
[40] GET /api/chat 200 in 45ms"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "@/components/ui/button"
import {
CodeBlock,
CodeBlockContent,
CodeBlockFooter,
} from "@/components/ui/code-block"
const log = Array.from(
{ length: 40 },
(_, index) =>
`[${String(index + 1).padStart(2, "0")}] GET /api/chat 200 in ${12 + ((index * 7) % 40)}ms`
).join("\n")
export function CodeBlockShowMoreScroll() {
const [expanded, setExpanded] = React.useState(false)
const id = React.useId()
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent
id={id}
className={
expanded
? "max-h-96 scroll-fade-y"
: "max-h-48 overflow-y-hidden scroll-fade-y"
}
>
{log}
</CodeBlockContent>
<CodeBlockFooter className="justify-center">
<Button
variant="ghost"
size="xs"
aria-expanded={expanded}
aria-controls={id}
onClick={() => setExpanded(!expanded)}
>
{expanded ? "Show less" : "Show more"}
<ChevronDownIcon
data-icon="inline-end"
className="transition-transform duration-200 ease-[cubic-bezier(0.23,1,0.32,1)] group-aria-expanded/button:rotate-180 motion-reduce:transition-none"
/>
</Button>
</CodeBlockFooter>
</CodeBlock>
)
}Size#
Use size="sm" for tool and Trace output. It has tighter padding and text-xs text.
$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0import { TerminalIcon } from "lucide-react"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockHeader,
CodeBlockTitle,
} from "@/components/ui/code-block"
const install = `$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0`
export function CodeBlockSize() {
return (
<div className="flex w-full max-w-lg flex-col gap-4">
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>
<TerminalIcon />
Default
</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={install} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>{install}</CodeBlockContent>
</CodeBlock>
<CodeBlock size="sm">
<CodeBlockHeader>
<CodeBlockTitle>
<TerminalIcon />
Small
</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={install} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>{install}</CodeBlockContent>
</CodeBlock>
</div>
)
}Destructive#
Use variant="destructive" for errors and failed output.
TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)ENOENT: no such file or directory, open 'CHANGELOG.md'import { CircleAlertIcon } from "lucide-react"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockHeader,
CodeBlockTitle,
} from "@/components/ui/code-block"
const error = `TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)`
export function CodeBlockDestructive() {
return (
<div className="flex w-full max-w-lg flex-col gap-4">
<CodeBlock variant="destructive">
<CodeBlockHeader>
<CodeBlockTitle>
<CircleAlertIcon />
Unhandled runtime error
</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={error} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent wrap>{error}</CodeBlockContent>
</CodeBlock>
<CodeBlock variant="destructive" size="sm">
<CodeBlockContent wrap>
ENOENT: no such file or directory, open 'CHANGELOG.md'
</CodeBlockContent>
</CodeBlock>
</div>
)
}Trace#
Put a CodeBlock with size="sm" in TraceCollapsibleContent for tool and terminal output. Add wrap and a max-h-64 class to CodeBlockContent so long output wraps and scrolls inside the trace. The block sets its own text color, so TraceCollapsibleContent does not mute it.
$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)import { TerminalIcon } from "lucide-react"
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block"
import {
Trace,
TraceCollapsible,
TraceCollapsibleContent,
TraceCollapsibleTrigger,
TraceIcon,
TraceLine,
TraceTitle,
} from "@/components/ui/trace"
const install = `$ pnpm add ai @ai-sdk/react zod
Progress: resolved 412, reused 398, downloaded 14, added 14, done
dependencies:
+ @ai-sdk/react 3.0.12
+ ai 6.0.12
+ zod 4.1.12
Done in 2.8s using pnpm v10.18.0`
const error = `TypeError: Cannot read properties of undefined (reading 'map')
at MessageList (components/message-list.tsx:18:22)
at renderWithHooks (react-dom.development.js:16305:18)
at mountIndeterminateComponent (react-dom.development.js:20069:13)`
export function CodeBlockTrace() {
return (
<Trace className="max-w-lg">
<TraceCollapsible defaultOpen>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<TerminalIcon />
</TraceIcon>
<TraceTitle>Ran pnpm add ai @ai-sdk/react zod</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<CodeBlock size="sm">
<CodeBlockContent wrap className="max-h-64">
{install}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
<TraceCollapsible defaultOpen>
<TraceCollapsibleTrigger>
<TraceLine status="error">
<TraceIcon>
<TerminalIcon />
</TraceIcon>
<TraceTitle>Failed to run pnpm dev</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<CodeBlock size="sm" variant="destructive">
<CodeBlockContent wrap className="max-h-64">
{error}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
)
}Tabs#
Show several files with Tabs, one CodeBlock in each TabsContent.
import { streamText } from "ai"
export async function POST(request: Request) { const { messages } = await request.json() const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages })
return result.toUIMessageStreamResponse()}"use client"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockLine,
} from "@/components/ui/code-block"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
const files = [
{
name: "route.ts",
language: "ts",
code: `import { streamText } from "ai"
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({ model: "anthropic/claude-sonnet-4.5", messages })
return result.toUIMessageStreamResponse()
}`,
},
{
name: "chat.tsx",
language: "tsx",
code: `"use client"
import { useChat } from "@ai-sdk/react"
export function Chat() {
const { messages } = useChat()
return <MessageList messages={messages} />
}`,
},
{
name: ".env.local",
language: "bash",
code: `AI_GATEWAY_API_KEY=your-key`,
},
]
function File({ code, language }: { code: string; language: string }) {
const lines = useHighlightedCode(code, language)
return (
<CodeBlock>
<CodeBlockActions>
<CodeBlockCopyButton value={code} />
</CodeBlockActions>
<CodeBlockContent>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}
export function CodeBlockTabs() {
return (
<Tabs defaultValue={files[0].name} className="w-full max-w-lg">
<TabsList>
{files.map((file) => (
<TabsTrigger key={file.name} value={file.name} className="font-mono">
{file.name}
</TabsTrigger>
))}
</TabsList>
{files.map((file) => (
<TabsContent key={file.name} value={file.name}>
<File code={file.code} language={file.language} />
</TabsContent>
))}
</Tabs>
)
}Message#
Put a CodeBlock between paragraphs inside a Message.
Wrap the value in a useDebounce hook:
export function useDebounce<T>(value: T, delay = 300) { const [debounced, setDebounced] = React.useState(value)
React.useEffect(() => { const timeout = setTimeout(() => setDebounced(value), delay) return () => clearTimeout(timeout) }, [value, delay])
return debounced}Then query with the debounced value instead of the raw input.
"use client"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import { Bubble, BubbleContent } from "@/components/ui/bubble"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/code-block"
import { Message, MessageContent } from "@/components/ui/message"
const code = `export function useDebounce<T>(value: T, delay = 300) {
const [debounced, setDebounced] = React.useState(value)
React.useEffect(() => {
const timeout = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(timeout)
}, [value, delay])
return debounced
}`
export function CodeBlockMessage() {
const lines = useHighlightedCode(code, "ts")
return (
<div className="flex w-full max-w-lg flex-col gap-6 text-sm">
<Message align="end">
<MessageContent>
<Bubble>
<BubbleContent>How do I debounce the search input?</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageContent className="gap-3 leading-relaxed">
<p>
Wrap the value in a <code className="font-mono">useDebounce</code>{" "}
hook:
</p>
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>hooks/use-debounce.ts</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={code} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
<p>Then query with the debounced value instead of the raw input.</p>
</MessageContent>
</Message>
</div>
)
}Dialog#
A CodeBlock in a Dialog sits inside the card's padding, recessed into the card by its top shadow and hairline.
"use client"
import { FileCodeIcon } from "lucide-react"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import { Button } from "@/components/ui/button"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockLine,
} from "@/components/ui/code-block"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
const code = `{
"name": "acme-chat",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"lint": "eslint ."
}
}`
export function CodeBlockDialog() {
const lines = useHighlightedCode(code, "json")
return (
<Dialog>
<DialogTrigger render={<Button variant="outline" />}>
<FileCodeIcon data-icon="inline-start" />
View package.json
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>package.json</DialogTitle>
<DialogDescription>
The scripts the agent will run in this project.
</DialogDescription>
</DialogHeader>
<CodeBlock>
<CodeBlockActions>
<CodeBlockCopyButton value={code} />
</CodeBlockActions>
<CodeBlockContent lineNumbers>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
</DialogContent>
</Dialog>
)
}Syntax Highlighting#
useHighlightedCode highlights with Shiki and returns one React node per line for CodeBlockLine.
{ "$schema": "https://ui.shadcn.com/schema.json", "style": "base-nova", "rsc": true, "tsx": true, "aliases": { "components": "@/components", "ui": "@/components/ui" }}"use client"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import {
CodeBlock,
CodeBlockContent,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/code-block"
const code = `{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-nova",
"rsc": true,
"tsx": true,
"aliases": {
"components": "@/components",
"ui": "@/components/ui"
}
}`
export function CodeBlockHighlighting() {
const lines = useHighlightedCode(code, "json")
return (
<CodeBlock className="max-w-lg">
<CodeBlockHeader>
<CodeBlockTitle>components.json</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockContent>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}const lines = useHighlightedCode(code, "json")
<CodeBlock>
<CodeBlockContent>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>Shiki uses its JavaScript regex engine, so there is no WASM to load.
Languages#
Each language loads the first time it is used. Edit the list in lib/highlighter.tsx. A language that is not in the list renders as plain text.
SELECT id, title, created_atFROM messagesWHERE chat_id = $1ORDER BY created_at DESCLIMIT 50;def chunk(items: list[str], size: int = 8): for start in range(0, len(items), size): yield items[start : start + size]on: push: branches: [main]jobs: deploy: runs-on: ubuntu-latest"use client"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import {
CodeBlock,
CodeBlockContent,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/code-block"
const samples = [
{
title: "query.sql",
language: "sql",
code: `SELECT id, title, created_at
FROM messages
WHERE chat_id = $1
ORDER BY created_at DESC
LIMIT 50;`,
},
{
title: "train.py",
language: "python",
code: `def chunk(items: list[str], size: int = 8):
for start in range(0, len(items), size):
yield items[start : start + size]`,
},
{
title: "deploy.yml",
language: "yaml",
code: `on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest`,
},
]
function Sample({
title,
language,
code,
}: {
title: string
language: string
code: string
}) {
const lines = useHighlightedCode(code, language)
return (
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>{title}</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockContent>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}
export function CodeBlockLanguages() {
return (
<div className="flex w-full max-w-lg flex-col gap-4">
{samples.map((sample) => (
<Sample key={sample.title} {...sample} />
))}
</div>
)
}The list ships with bash, css, diff, html, javascript, json, jsx, markdown, python, sql, tsx, typescript and yaml, plus aliases such as ts, js, sh and yml.
Terminal Output#
Pass "ansi" as the language to render the colors in terminal output. Without it, escape codes show as text.
$ pnpm test[32m ✓[39m src/lib/format.test.ts [2m(4 tests)[22m[32m ✓[39m src/lib/stream.test.ts [2m(7 tests)[22m[31m ✗[39m src/lib/parse.test.ts [2m(1 failed)[22m
[1mTests[22m [31m1 failed[39m | [32m11 passed[39m (12)"use client"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import {
CodeBlock,
CodeBlockContent,
CodeBlockLine,
} from "@/components/ui/code-block"
const output = [
"$ pnpm test",
"\u001b[32m ✓\u001b[39m src/lib/format.test.ts \u001b[2m(4 tests)\u001b[22m",
"\u001b[32m ✓\u001b[39m src/lib/stream.test.ts \u001b[2m(7 tests)\u001b[22m",
"\u001b[31m ✗\u001b[39m src/lib/parse.test.ts \u001b[2m(1 failed)\u001b[22m",
"",
"\u001b[1mTests\u001b[22m \u001b[31m1 failed\u001b[39m | \u001b[32m11 passed\u001b[39m (12)",
].join("\n")
export function CodeBlockAnsi() {
const lines = useHighlightedCode(output, "ansi")
return (
<CodeBlock className="max-w-lg">
<CodeBlockContent>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}ANSI colors do not carry from one line to the next.
Server Rendering#
highlightCode is async and works in a Server Component, so Shiki stays out of the client bundle.
import { highlightCode } from "@/lib/highlighter"
export async function ServerCode({ code }: { code: string }) {
const lines = (await highlightCode(code, "tsx")) ?? code.split("\n")
return (
<CodeBlock>
<CodeBlockContent lineNumbers>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
)
}Streaming#
While the code streams, the hook tokenizes only the new lines, starting where the last line left off. Lines that did not change keep the same nodes, so React skips them.
"use client"
import * as React from "react"
import { useHighlightedCode } from "@/hooks/use-highlighted-code"
import { Button } from "@/components/ui/button"
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/code-block"
const code = `import { streamText } from "ai"
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: "anthropic/claude-sonnet-4.5",
messages,
})
return result.toUIMessageStreamResponse()
}`
// Reveals the code a few characters at a time, like a model writing it.
function useStream(text: string) {
const [length, setLength] = React.useState(0)
const [running, setRunning] = React.useState(false)
React.useEffect(() => {
if (!running) {
return
}
const interval = setInterval(() => {
setLength((current) => {
const next = Math.min(current + 3, text.length)
if (next === text.length) {
setRunning(false)
}
return next
})
}, 24)
return () => clearInterval(interval)
}, [running, text])
return {
value: text.slice(0, length),
running,
start: () => {
setLength(0)
setRunning(true)
},
}
}
export function CodeBlockStreaming() {
const stream = useStream(code)
const lines = useHighlightedCode(stream.value, "ts")
return (
<div className="flex w-full max-w-lg flex-col items-start gap-4">
<CodeBlock className="min-h-20">
<CodeBlockHeader>
<CodeBlockTitle>app/api/chat/route.ts</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={code} disabled={stream.running} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>
{lines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))}
</CodeBlockContent>
</CodeBlock>
<Button
variant="outline"
size="sm"
disabled={stream.running}
onClick={stream.start}
>
{stream.value ? "Stream again" : "Stream"}
</Button>
</div>
)
}Accessibility#
CodeBlockContentis aprewith acodeelement and always setsdir="ltr", so code reads left to right on an RTL page.CodeBlockContenthastabIndex={0}, so keyboard users can focus it and scroll it. PasstabIndex={-1}to remove the tab stop.- Floating actions show when focus is inside the block, not only on hover, so keyboard users can reach them.
- Line numbers are hidden from screen readers.
- Diff lines carry a
+or−marker as well as a tint, so the change does not rely on color. CodeBlockCopyButtonis labelled "Copy", and a polite live region announces "Copied" after a successful copy.- Give a wrap toggle
aria-pressedso its state is announced. - Give a Show More button
aria-expandedand pointaria-controlsat theidofCodeBlockContent.
API Reference#
CodeBlock#
The surface that holds the header, the content and the footer.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "default" | The tone of the block. |
size | "default" | "sm" | "default" | "sm" has tighter padding and text-xs code, for tool output. |
className | string | - | Additional classes to apply to the block. |
CodeBlockHeader#
A row above the content for a title and actions.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes to apply to the header. |
CodeBlockTitle#
A filename or a label, with an optional icon. It truncates.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes to apply to the title. |
CodeBlockActions#
Holds buttons at the end of the header. As a direct child of CodeBlock, it floats.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes to apply to the actions. |
CodeBlockCopyButton#
A ghost icon button that copies value to the clipboard. It accepts the props of Button.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Required. The text to copy. |
className | string | - | Additional classes to apply to the button. |
CodeBlockContent#
The pre that scrolls or wraps.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | A string, or a list of CodeBlockLine. |
wrap | boolean | false | Wrap long lines instead of scrolling. |
lineNumbers | boolean | false | Number each line. A string child is split into lines. |
startLine | number | 1 | The number of the first line, when lineNumbers is set. |
tabIndex | number | 0 | Makes the content focusable so it can be scrolled by keyboard. |
className | string | - | Additional classes to apply to the content, such as max-h-72. |
CodeBlockLine#
One line of code.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "added" | "removed" | "highlighted" | "default" | The state of the line. |
className | string | - | Additional classes to apply to the line. |
CodeBlockFooter#
A row below the content, such as for a Show More button.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | - | Additional classes to apply to the footer. |
useHighlightedCode#
const lines = useHighlightedCode(code, language)Returns one React node per line. It returns plain lines until the first pass finishes, and for a language that is not in the list.
| Argument | Type | Description |
|---|---|---|
code | string | The code to highlight. A trailing newline is ignored. |
language | string | A language or alias from lib/highlighter.tsx, or "ansi". |
On This Page
InstallationUsageCompositionFeaturesBasicHeaderActionsFloating ActionsLine NumbersStart LineWrapHighlighted LinesDiffHeightShow MoreMax HeightSizeDestructiveTraceTabsMessageDialogSyntax HighlightingLanguagesTerminal OutputServer RenderingStreamingAccessibilityAPI ReferenceCodeBlockCodeBlockHeaderCodeBlockTitleCodeBlockActionsCodeBlockCopyButtonCodeBlockContentCodeBlockLineCodeBlockFooteruseHighlightedCode