Code Block
A syntax-highlighted code surface with stable streaming updates, line numbers, focused lines, smooth following, and copy feedback.
Preview
summarize.tstypescriptWriting
TSXcomponents/previews/agents/code-block.preview.tsx
"use client";
import { RotateCcw } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { CodeBlock } from "@/components/agents/code-block";
const LINES = [
'import { generateText } from "ai";',
"",
"export async function summarize(input: string) {",
" const { text } = await generateText({",
' model: "openai/gpt-5",',
` prompt: \`Summarize this clearly: \${input}\`,`,
" });",
"",
" return {",
" text,",
" generatedAt: new Date().toISOString(),",
" };",
"}",
];
function StreamingCodeBlock() {
const [visibleLines, setVisibleLines] = useState(1);
const timer = useRef<number | undefined>(undefined);
const complete = visibleLines === LINES.length;
useEffect(() => {
if (visibleLines >= LINES.length) return;
timer.current = window.setTimeout(
() => setVisibleLines((value) => value + 1),
260,
);
return () => {
if (timer.current) window.clearTimeout(timer.current);
};
}, [visibleLines]);
return (
<CodeBlock
filename="summarize.ts"
language="typescript"
code={LINES.slice(0, visibleLines).join("\n")}
status={complete ? "complete" : "streaming"}
highlightLines={[4, 5, 6, 7]}
maxHeight={224}
/>
);
}
export function CodeBlockPreview() {
const [run, setRun] = useState(0);
return (
<div className="relative h-[340px] w-full max-w-xl">
<StreamingCodeBlock key={run} />
<button
type="button"
onClick={() => setRun((value) => value + 1)}
className="absolute bottom-0 left-0 inline-flex items-center gap-1.5 rounded-full px-2 py-1 text-xs font-medium text-muted-foreground outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
<RotateCcw className="size-3" />
Replay
</button>
</div>
);
}
TSXcomponents/agents/code-block.tsx
"use client";
// beui.dev/components/agents/code-block
import { Check, Copy, FileCode2, LoaderCircle } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import {
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import {
type AgentCodeLanguage,
AgentCodeLine,
useAgentCodeTokens,
} from "@/components/agents/agent-code";
import { SPRING_PRESS } from "@/lib/ease";
import { cn } from "@/lib/utils";
export type CodeBlockStatus = "streaming" | "complete";
export interface CodeBlockProps {
code: string;
language?: AgentCodeLanguage;
filename?: ReactNode;
status?: CodeBlockStatus;
showLineNumbers?: boolean;
highlightLines?: number[];
maxHeight?: number;
wrap?: boolean;
copyable?: boolean;
onCopy?: () => void | Promise<void>;
className?: string;
}
export function CodeBlock({
code,
language = "typescript",
filename,
status = "complete",
showLineNumbers = true,
highlightLines = [],
maxHeight = 280,
wrap = false,
copyable = true,
onCopy,
className,
}: CodeBlockProps) {
const reduce = useReducedMotion() ?? false;
const viewportRef = useRef<HTMLDivElement>(null);
const copyTimer = useRef<number | undefined>(undefined);
const [copied, setCopied] = useState(false);
const streaming = status === "streaming";
const tokens = useAgentCodeTokens(code, language);
const highlighted = useMemo(
() => new Set(highlightLines),
[highlightLines],
);
let offset = 0;
const lines = code.split("\n").map((content) => {
const line = { content, offset };
offset += content.length + 1;
return line;
});
useEffect(
() => () => {
if (copyTimer.current) window.clearTimeout(copyTimer.current);
},
[],
);
useLayoutEffect(() => {
const viewport = viewportRef.current;
if (!viewport || !streaming) return;
const frame = requestAnimationFrame(() => {
if (viewport.scrollHeight <= viewport.clientHeight) return;
if (typeof viewport.scrollTo === "function") {
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: reduce ? "auto" : "smooth",
});
} else {
viewport.scrollTop = viewport.scrollHeight;
}
});
return () => cancelAnimationFrame(frame);
});
const handleCopy = useCallback(async () => {
if (onCopy) await onCopy();
else await navigator.clipboard?.writeText(code);
setCopied(true);
if (copyTimer.current) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1600);
}, [code, onCopy]);
return (
<div
data-state={status}
aria-busy={streaming}
className={cn(
"w-full overflow-hidden rounded-2xl bg-muted/80 text-sm",
className,
)}
>
<div className="flex h-10 items-center gap-2.5 px-3">
<FileCode2
aria-hidden="true"
className="size-3.5 shrink-0 text-muted-foreground/70"
/>
{filename ? (
<span className="min-w-0 truncate font-mono text-xs text-foreground/80">
{filename}
</span>
) : null}
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/55">
{language}
</span>
<span
className={cn(
"ml-auto inline-flex shrink-0 items-center gap-1 text-[10px] font-medium",
streaming
? "text-blue-600 dark:text-blue-400"
: "text-emerald-600 dark:text-emerald-400",
)}
>
{streaming ? (
<LoaderCircle className={cn("size-3", !reduce && "animate-spin")} />
) : (
<Check className="size-3" />
)}
{streaming ? "Writing" : "Ready"}
</span>
{copyable || onCopy ? (
<motion.button
type="button"
aria-label={copied ? "Copied" : "Copy code"}
title={copied ? "Copied" : "Copy code"}
onClick={handleCopy}
whileTap={reduce ? undefined : { scale: 0.9 }}
transition={SPRING_PRESS}
className="grid size-7 shrink-0 place-items-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-background/70 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
{copied ? (
<Check className="size-3.5" />
) : (
<Copy className="size-3.5" />
)}
</motion.button>
) : null}
</div>
<div
ref={viewportRef}
role={streaming ? "log" : undefined}
aria-live={streaming ? "polite" : undefined}
className="scrollbar-hide overflow-auto border-t border-foreground/[0.06] py-2"
style={{ maxHeight }}
>
<pre className="m-0 min-w-max font-mono text-xs leading-5 text-foreground/85">
<code>
{lines.map((line, index) => {
const lineNumber = index + 1;
return (
<span
key={line.offset}
className={cn(
"grid min-h-5",
showLineNumbers
? "grid-cols-[2.75rem_minmax(0,1fr)]"
: "grid-cols-1",
highlighted.has(lineNumber) && "bg-blue-500/[0.07]",
)}
>
{showLineNumbers ? (
<span className="select-none pr-3 text-right tabular-nums text-muted-foreground/35">
{lineNumber}
</span>
) : null}
<AgentCodeLine
code={line.content}
tokens={tokens?.[index]}
className={cn(
"pr-4",
showLineNumbers ? "pl-1" : "pl-4",
wrap ? "whitespace-pre-wrap break-words" : "whitespace-pre",
)}
/>
</span>
);
})}
</code>
</pre>
</div>
</div>
);
}
Install
Add it with the shadcn CLI, or copy the source manually.
$ bunx --bun shadcn add @beui/code-block
Needs the theme tokens once. Already ran
shadcn init? You are set. Theme setupInstall dependencies
npm i ai clsx lucide-react motion shiki tailwind-mergeAdd util files
TSXlib/ease.ts
// Shared motion tokens. Easing curves mirror the CSS custom properties in
// globals.css; springs are the canonical physics used across components.
// Strong custom variants — defaults like `ease-in`/`ease-out` feel weak.
export const EASE_OUT = [0.16, 1, 0.3, 1] as const;
export const EASE_IN_OUT = [0.77, 0, 0.175, 1] as const;
export const EASE_DRAWER = [0.32, 0.72, 0, 1] as const;
/** CSS string form of EASE_OUT for inline style transitions. */
export const EASE_OUT_CSS = "cubic-bezier(0.16, 1, 0.3, 1)";
/** Press feedback on buttons and other tappable surfaces. */
export const SPRING_PRESS = {
type: "spring",
stiffness: 500,
damping: 30,
mass: 0.6,
} as const;
/** Content swaps — label/icon slots trading places inside a control. */
export const SPRING_SWAP = {
type: "spring",
stiffness: 460,
damping: 30,
mass: 0.55,
} as const;
/** Overlay panel entrances — modals and sheets summoned by pointer. */
export const SPRING_PANEL = {
type: "spring",
stiffness: 420,
damping: 40,
mass: 0.5,
} as const;
/** Shared-layout glides — pills, indicators and panels morphing between positions. */
export const SPRING_LAYOUT = {
type: "spring",
stiffness: 360,
damping: 32,
mass: 0.6,
} as const;
/** Cursor-follow physics for decorative mouse tracking (magnetic, tilt, dock). */
export const SPRING_MOUSE = {
stiffness: 200,
damping: 15,
mass: 0.3,
} as const;
/** Dragged handles and fills (sliders) — critically damped `useSpring` config,
* so the value follows the pointer butterily and never rebounds off an end. */
export const SPRING_GLIDE = {
stiffness: 700,
damping: 50,
mass: 0.5,
} as const;
TSXlib/utils.ts
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy the source code
TSXcomponents/agents/code-block.tsx
"use client";
// beui.dev/components/agents/code-block
import { Check, Copy, FileCode2, LoaderCircle } from "lucide-react";
import { motion, useReducedMotion } from "motion/react";
import {
type ReactNode,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import {
type AgentCodeLanguage,
AgentCodeLine,
useAgentCodeTokens,
} from "@/components/agents/agent-code";
import { SPRING_PRESS } from "@/lib/ease";
import { cn } from "@/lib/utils";
export type CodeBlockStatus = "streaming" | "complete";
export interface CodeBlockProps {
code: string;
language?: AgentCodeLanguage;
filename?: ReactNode;
status?: CodeBlockStatus;
showLineNumbers?: boolean;
highlightLines?: number[];
maxHeight?: number;
wrap?: boolean;
copyable?: boolean;
onCopy?: () => void | Promise<void>;
className?: string;
}
export function CodeBlock({
code,
language = "typescript",
filename,
status = "complete",
showLineNumbers = true,
highlightLines = [],
maxHeight = 280,
wrap = false,
copyable = true,
onCopy,
className,
}: CodeBlockProps) {
const reduce = useReducedMotion() ?? false;
const viewportRef = useRef<HTMLDivElement>(null);
const copyTimer = useRef<number | undefined>(undefined);
const [copied, setCopied] = useState(false);
const streaming = status === "streaming";
const tokens = useAgentCodeTokens(code, language);
const highlighted = useMemo(
() => new Set(highlightLines),
[highlightLines],
);
let offset = 0;
const lines = code.split("\n").map((content) => {
const line = { content, offset };
offset += content.length + 1;
return line;
});
useEffect(
() => () => {
if (copyTimer.current) window.clearTimeout(copyTimer.current);
},
[],
);
useLayoutEffect(() => {
const viewport = viewportRef.current;
if (!viewport || !streaming) return;
const frame = requestAnimationFrame(() => {
if (viewport.scrollHeight <= viewport.clientHeight) return;
if (typeof viewport.scrollTo === "function") {
viewport.scrollTo({
top: viewport.scrollHeight,
behavior: reduce ? "auto" : "smooth",
});
} else {
viewport.scrollTop = viewport.scrollHeight;
}
});
return () => cancelAnimationFrame(frame);
});
const handleCopy = useCallback(async () => {
if (onCopy) await onCopy();
else await navigator.clipboard?.writeText(code);
setCopied(true);
if (copyTimer.current) window.clearTimeout(copyTimer.current);
copyTimer.current = window.setTimeout(() => setCopied(false), 1600);
}, [code, onCopy]);
return (
<div
data-state={status}
aria-busy={streaming}
className={cn(
"w-full overflow-hidden rounded-2xl bg-muted/80 text-sm",
className,
)}
>
<div className="flex h-10 items-center gap-2.5 px-3">
<FileCode2
aria-hidden="true"
className="size-3.5 shrink-0 text-muted-foreground/70"
/>
{filename ? (
<span className="min-w-0 truncate font-mono text-xs text-foreground/80">
{filename}
</span>
) : null}
<span className="text-[10px] font-medium uppercase tracking-wide text-muted-foreground/55">
{language}
</span>
<span
className={cn(
"ml-auto inline-flex shrink-0 items-center gap-1 text-[10px] font-medium",
streaming
? "text-blue-600 dark:text-blue-400"
: "text-emerald-600 dark:text-emerald-400",
)}
>
{streaming ? (
<LoaderCircle className={cn("size-3", !reduce && "animate-spin")} />
) : (
<Check className="size-3" />
)}
{streaming ? "Writing" : "Ready"}
</span>
{copyable || onCopy ? (
<motion.button
type="button"
aria-label={copied ? "Copied" : "Copy code"}
title={copied ? "Copied" : "Copy code"}
onClick={handleCopy}
whileTap={reduce ? undefined : { scale: 0.9 }}
transition={SPRING_PRESS}
className="grid size-7 shrink-0 place-items-center rounded-full text-muted-foreground outline-none transition-colors hover:bg-background/70 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
>
{copied ? (
<Check className="size-3.5" />
) : (
<Copy className="size-3.5" />
)}
</motion.button>
) : null}
</div>
<div
ref={viewportRef}
role={streaming ? "log" : undefined}
aria-live={streaming ? "polite" : undefined}
className="scrollbar-hide overflow-auto border-t border-foreground/[0.06] py-2"
style={{ maxHeight }}
>
<pre className="m-0 min-w-max font-mono text-xs leading-5 text-foreground/85">
<code>
{lines.map((line, index) => {
const lineNumber = index + 1;
return (
<span
key={line.offset}
className={cn(
"grid min-h-5",
showLineNumbers
? "grid-cols-[2.75rem_minmax(0,1fr)]"
: "grid-cols-1",
highlighted.has(lineNumber) && "bg-blue-500/[0.07]",
)}
>
{showLineNumbers ? (
<span className="select-none pr-3 text-right tabular-nums text-muted-foreground/35">
{lineNumber}
</span>
) : null}
<AgentCodeLine
code={line.content}
tokens={tokens?.[index]}
className={cn(
"pr-4",
showLineNumbers ? "pl-1" : "pl-4",
wrap ? "whitespace-pre-wrap break-words" : "whitespace-pre",
)}
/>
</span>
);
})}
</code>
</pre>
</div>
</div>
);
}
TSXcomponents/agents/agent-code.tsx
"use client";
import {
type CSSProperties,
Fragment,
useEffect,
useState,
} from "react";
import { createHighlighter, type Highlighter } from "shiki";
import { cn } from "@/lib/utils";
export type AgentCodeLanguage =
| "bash"
| "diff"
| "json"
| "text"
| "tsx"
| "typescript";
export interface AgentCodeToken {
content: string;
offset: number;
light?: string;
dark?: string;
}
export type AgentCodeTokenLines = AgentCodeToken[][];
export interface AgentCodeProps {
code: string;
language?: AgentCodeLanguage;
className?: string;
}
export interface AgentCodeLineProps {
code: string;
tokens?: AgentCodeToken[];
className?: string;
}
const LIGHT_THEME = "github-light-high-contrast";
const DARK_THEME = "github-dark-high-contrast";
let agentCodeHighlighter: Promise<Highlighter> | null = null;
const tokenCache = new Map<string, AgentCodeTokenLines>();
function getAgentCodeHighlighter() {
if (!agentCodeHighlighter) {
agentCodeHighlighter = createHighlighter({
themes: [LIGHT_THEME, DARK_THEME],
langs: ["bash", "diff", "json", "tsx", "typescript"],
});
}
return agentCodeHighlighter;
}
function tokenCacheKey(code: string, language: AgentCodeLanguage) {
return `${language}\u0000${code}`;
}
export function useAgentCodeTokens(
code: string,
language: AgentCodeLanguage,
) {
const key = tokenCacheKey(code, language);
const cached = tokenCache.get(key);
const [result, setResult] = useState<{
key: string;
code: string;
language: AgentCodeLanguage;
lines: AgentCodeTokenLines;
} | null>(cached ? { key, code, language, lines: cached } : null);
useEffect(() => {
const current = tokenCache.get(key);
if (current) {
setResult({ key, code, language, lines: current });
return;
}
let cancelled = false;
getAgentCodeHighlighter().then((highlighter) => {
if (cancelled) return;
const lines = highlighter
.codeToTokensWithThemes(code, {
lang: language,
themes: {
light: LIGHT_THEME,
dark: DARK_THEME,
},
})
.map((line) =>
line.map((token) => ({
content: token.content,
offset: token.offset,
light: token.variants.light?.color,
dark: token.variants.dark?.color,
})),
);
tokenCache.set(key, lines);
setResult({ key, code, language, lines });
});
return () => {
cancelled = true;
};
}, [code, key, language]);
if (result?.key === key) return result.lines;
if (result?.language === language && code.startsWith(result.code)) {
return result.lines;
}
return null;
}
export function AgentCodeLine({
code,
tokens,
className,
}: AgentCodeLineProps) {
return (
<span className={className}>
{tokens
? tokens.map((token) => (
<span
key={`${token.offset}-${token.content}`}
style={
{
"--agent-code-light": token.light ?? "currentColor",
"--agent-code-dark": token.dark ?? token.light ?? "currentColor",
} as CSSProperties
}
className="text-[var(--agent-code-light)] dark:text-[var(--agent-code-dark)]"
>
{token.content}
</span>
))
: code}
</span>
);
}
export function AgentCode({
code,
language = "bash",
className,
}: AgentCodeProps) {
const tokens = useAgentCodeTokens(code, language);
let offset = 0;
const lines = code.split("\n").map((content) => {
const line = { content, offset };
offset += content.length + 1;
return line;
});
return (
<pre
className={cn(
"m-0 overflow-x-auto whitespace-pre font-mono text-xs leading-5 text-foreground/85",
className,
)}
>
<code>
{lines.map((line, index) => (
<Fragment key={line.offset}>
<AgentCodeLine code={line.content} tokens={tokens?.[index]} />
{index < lines.length - 1 ? "\n" : null}
</Fragment>
))}
</code>
</pre>
);
}
API Reference
codestring—language?"text" | "bash" | "diff" | "json" | "tsx" | "typescript"typescriptfilename?ReactNode—status?"complete" | "streaming"completeshowLineNumbers?booleantruehighlightLines?number[][]maxHeight?number280wrap?booleanfalsecopyable?booleantrueonCopy?(() => void | Promise<void>)—className?string—Composition
Nest generated source inside the response that owns its streaming lifecycle.
StreamingResponse
└── CodeBlockNote: File Diff explains additions and removals around highlighted code. Tool Result wraps generated code in an execution outcome. Streaming Response embeds highlighted code inside a rich answer.
How it works
Generated code changes faster than a conventional static snippet. The surface must remain readable while lines arrive, preserve syntax structure, and make copying the final result predictable.
Updated