Code Block
Displays code, terminal output, logs, and diffs with a header, line numbers, and a copy action.
Preview
Loading...
import { useId, useState } from "react";
import { Button } from "@/components/ui/Button";
import {
CodeBlock,
CodeBlockActions,
CodeBlockContent,
CodeBlockCopyButton,
CodeBlockFooter,
CodeBlockHeader,
CodeBlockLine,
CodeBlockTitle,
} from "@/components/ui/CodeBlock";
const routeSource = `export async function POST(request: Request) {
const body = await request.json()
return Response.json(body)
}`;
export function CodeBlockDemo() {
return (
<div className="w-full max-w-lg">
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>app/api/chat/route.ts</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={routeSource} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent lineNumbers>{routeSource}</CodeBlockContent>
</CodeBlock>
</div>
)Installation
Make sure that namespace is set in your component.json file. Namespace docs: Learn more about namespaces
pnpm dlx shadcn@latest add @aura/code-blockManual
Install the following dependencies:
pnpm install @radix-ui/react-icons class-variance-authorityCopy and paste the button component into your components/ui/Button.tsx file.
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/class-names";
import { Spinner } from "@/components/ui/Spinner";
const buttonVariants = cva(
"button focus-visible:[outline:var(--aura-outline)] [&_svg]:size-[1em]",
{
variants: {
variant: {
default: "button-fill",
fill: "button-fill",
pill: "button-pill border border-gray-6 text-gray-11 bg-gray-2 hover:bg-gray-3",
link: "button-link",
menu: "button-menu",
split: "button-fill button-split-action",
splitMenu: "button-fill button-split-menu",
},
size: {
default: "h-4",
xs: "h-2.5",
sm: "h-3",
md: "h-4",
lg: "h-5",
xl: "h-6",
icon: "w-3 h-3 p-0",
"icon-sm": "w-3 h-3 p-0",
"icon-md": "w-4 h-4 p-0",
},
},
defaultVariants: {
variant: "fill",
size: "default",
},
},
);
interface ButtonProps
extends React.ComponentProps<"button">,
VariantProps<typeof buttonVariants> {
asChild?: boolean;
isDisabled?: boolean;
isLoading?: boolean;
isLoadingText?: string | React.ReactNode;
/** @deprecated Use `variant`. Still applied, and warns in development. */
mode?: VariantProps<typeof buttonVariants>["variant"];
label?: string | React.ReactNode;
}
const deprecationWarnings = new Set<string>();
function warnOnce(message: string) {
if (process.env.NODE_ENV === "production") return;
if (deprecationWarnings.has(message)) return;
deprecationWarnings.add(message);
console.warn(message);
}
function resolveButtonContent(
label: React.ReactNode,
children: React.ReactNode,
): React.ReactNode {
const hasLabel = label != null && label !== false;
const hasChildren = children != null && children !== false;
if (hasLabel && hasChildren) {
warnOnce(
"Button: `label` and `children` both set the visible text. Pass one. `children` is shown.",
);
return children;
}
return hasChildren ? children : label;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
(props: ButtonProps, ref) => {
const {
className,
variant,
mode,
size,
asChild = false,
isDisabled,
isLoading,
isLoadingText,
children,
label,
disabled: disabledProp,
...rest
} = props;
const Comp = asChild ? Slot : "button";
const explicitlyDisabled = Boolean(isDisabled || disabledProp);
const loading = Boolean(isLoading);
const nonInteractive = explicitlyDisabled || loading;
const effectiveVariant = variant ?? mode;
if (mode !== undefined) {
warnOnce(
"Button: `mode` is deprecated and duplicates `variant`. Pass `variant` instead. `mode` still works.",
);
}
if (variant === "default" || mode === "default") {
warnOnce(
'Button: variant "default" is deprecated and matches "fill". Pass `variant="fill"`. "default" still works.',
);
}
const content = resolveButtonContent(label, children);
return (
<Comp
data-slot="button"
className={cn(
buttonVariants({ variant: effectiveVariant, size, className }),
)}
ref={ref}
{...rest}
data-loading={loading && !explicitlyDisabled ? true : undefined}
aria-busy={loading ? true : undefined}
disabled={nonInteractive}
>
{asChild ? (
children
) : (
<>
{isLoading && isLoadingText ? (
isLoadingText
) : (
<>
{isLoading ? <Spinner /> : null}
{content}
</>
)}
</>
)}
</Comp>
);
},
);
Button.displayName = "Button";
export { Button, buttonVariants };
export type { ButtonProps };
export default Button;
Copy and paste the class names utility into your utils/class-names.ts file.
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
Copy and paste the CodeBlock component into your components/ui/CodeBlock.tsx file.
"use client";
import * as React from "react";
import { CheckIcon, CopyIcon } from "@radix-ui/react-icons";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/utils/class-names";
import { Button } from "@/components/ui/Button";
const codeBlockVariants = cva(
"group/code-block relative flex w-full min-w-0 flex-col overflow-hidden rounded-md border text-sm text-gray-12",
{
variants: {
variant: {
default: "border-gray-6 bg-gray-2",
danger: "border-danger bg-danger text-danger-contrast",
},
},
defaultVariants: {
variant: "default",
},
}
);
const codeBlockLineVariants = cva("min-w-0", {
variants: {
variant: {
default: "",
added: "bg-accent-3",
removed: "bg-gray-4",
highlighted: "bg-accent-4",
},
},
defaultVariants: {
variant: "default",
},
});
type CodeBlockLineVariant = NonNullable<
VariantProps<typeof codeBlockLineVariants>["variant"]
>;
function CodeBlock({
className,
variant = "default",
size = "default",
...props
}: React.ComponentProps<"div"> &
VariantProps<typeof codeBlockVariants> & {
size?: "default" | "sm";
}) {
return (
<div
data-slot="code-block"
data-variant={variant}
data-size={size}
className={cn(codeBlockVariants({ variant }), className)}
{...props}
/>
);
}
function CodeBlockHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="code-block-header"
className={cn(
"flex min-w-0 items-center gap-0.5 px-1 py-0.5 text-gray-11 group-data-[size=sm]/code-block:px-0.5",
className
)}
{...props}
/>
);
}
function CodeBlockTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="code-block-title"
className={cn(
"min-w-0 flex-1 truncate text-sm font-medium text-gray-12 group-data-[size=sm]/code-block:text-xs group-data-[variant=danger]/code-block:text-danger-contrast",
className
)}
{...props}
/>
);
}
function CodeBlockActions({
className,
...props
}: React.ComponentProps<"div">) {
return (
<div
data-slot="code-block-actions"
className={cn("ms-auto flex shrink-0 items-center gap-0.5", className)}
{...props}
/>
);
}
function CodeBlockCopyButton({
value,
variant = "pill",
size = "icon",
className,
onClick,
...props
}: Omit<React.ComponentProps<typeof Button>, "children"> & {
value: string;
}) {
const [copied, setCopied] = React.useState(false);
React.useEffect(() => {
if (!copied) return;
const timeout = window.setTimeout(() => setCopied(false), 2000);
return () => window.clearTimeout(timeout);
}, [copied]);
return (
<Button
type="button"
variant={variant}
size={size}
className={className}
{...props}
data-slot="code-block-copy-button"
data-copied={copied || undefined}
aria-label={copied ? "Copied" : "Copy"}
onClick={(event) => {
onClick?.(event);
if (event.defaultPrevented) return;
const writeText = navigator.clipboard?.writeText;
if (!writeText) return;
void writeText
.call(navigator.clipboard, value)
.then(() => setCopied(true))
.catch(() => setCopied(false));
}}
>
{copied ? <CheckIcon className="icon" /> : <CopyIcon className="icon" />}
</Button>
);
}
function CodeBlockContent({
className,
children,
style,
wrap = false,
lineNumbers = false,
startLine = 1,
...props
}: React.ComponentProps<"pre"> & {
wrap?: boolean;
lineNumbers?: boolean;
startLine?: number;
}) {
const lineStart = Number.isFinite(startLine) ? startLine : 1;
const numberedLines =
lineNumbers && typeof children === "string"
? children.replace(/\n$/, "").split("\n")
: null;
return (
<pre
data-slot="code-block-content"
data-wrap={wrap || undefined}
data-numbered={lineNumbers || undefined}
dir="ltr"
tabIndex={0}
style={{ counterReset: `line ${lineStart - 1}`, ...style }}
className={cn(
"min-h-0 min-w-0 overflow-auto px-1 py-1 text-start font-mono text-sm leading-relaxed outline-none",
"group-has-data-[slot=code-block-header]/code-block:pt-0.5",
"group-has-data-[slot=code-block-footer]/code-block:pb-0.5",
"group-data-[size=sm]/code-block:px-0.5 group-data-[size=sm]/code-block:py-0.5 group-data-[size=sm]/code-block:text-xs",
"focus-visible:[outline:var(--aura-outline)]",
className
)}
{...props}
>
<code data-slot="code-block-code">
{numberedLines
? numberedLines.map((line, index) => (
<CodeBlockLine key={index}>{line}</CodeBlockLine>
))
: children}
</code>
</pre>
);
}
function isBlankLine(children: React.ReactNode) {
return (
children === undefined ||
children === null ||
children === "" ||
(Array.isArray(children) && children.length === 0)
);
}
function CodeBlockLine({
className,
variant = "default",
children,
...props
}: React.ComponentProps<"span"> & {
variant?: CodeBlockLineVariant;
}) {
return (
<span
data-slot="code-block-line"
data-variant={variant}
className={cn(codeBlockLineVariants({ variant }), className)}
{...props}
>
<span data-slot="code-block-line-text">
{isBlankLine(children) ? "\n" : children}
</span>
</span>
);
}
function CodeBlockFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="code-block-footer"
className={cn(
"flex min-w-0 items-center gap-0.5 px-1 py-0.5 text-gray-11 group-data-[size=sm]/code-block:px-0.5",
className
)}
{...props}
/>
);
}
export {
CodeBlock,
CodeBlockHeader,
CodeBlockTitle,
CodeBlockActions,
CodeBlockCopyButton,
CodeBlockContent,
CodeBlockLine,
CodeBlockFooter,
codeBlockVariants,
codeBlockLineVariants,
};
export type { CodeBlockLineVariant };
Usage
Terminal
export const Terminal = () => (
<div className="w-full max-w-lg">
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>Terminal</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={terminalOutput} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>{terminalOutput}</CodeBlockContent>
</CodeBlock>
</div>
)Diff
export const Diff = () => (
<div className="w-full max-w-lg">
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>package.json</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockContent lineNumbers>
<CodeBlockLine>{"{"}</CodeBlockLine>
<CodeBlockLine variant="removed"> "version": "1.2.0"</CodeBlockLine>
<CodeBlockLine variant="added"> "version": "1.3.0"</CodeBlockLine>
<CodeBlockLine>{"}"}</CodeBlockLine>
</CodeBlockContent>
</CodeBlock>
</div>
)Highlighted
export const Highlighted = () => (
<div className="w-full max-w-lg">
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>next.config.ts</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockContent lineNumbers startLine={4}>
<CodeBlockLine>experimental: {"{"}</CodeBlockLine>
<CodeBlockLine variant="highlighted"> reactCompiler: true,</CodeBlockLine>
<CodeBlockLine>{"}"},</CodeBlockLine>
</CodeBlockContent>
</CodeBlock>
</div>
)Wrap
export const Wrap = () => (
<div className="w-full max-w-lg">
<CodeBlock variant="danger">
<CodeBlockHeader>
<CodeBlockTitle>Logs</CodeBlockTitle>
</CodeBlockHeader>
<CodeBlockContent wrap>
Error: The model returned an error. The prompt is 212431 tokens and the
context window is 200000 tokens. Remove some messages or summarize the
conversation and try again.
</CodeBlockContent>
</CodeBlock>
</div>
)Small
export const Small = () => (
<div className="w-full max-w-lg">
<CodeBlock size="sm">
<CodeBlockHeader>
<CodeBlockTitle>node --version</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value="v24.18.0" />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent>v24.18.0</CodeBlockContent>
</CodeBlock>
</div>
)FloatingActions
export const FloatingActions = () => (
<div className="w-full max-w-lg">
<CodeBlock>
<CodeBlockActions>
<CodeBlockCopyButton value={terminalOutput} />
</CodeBlockActions>
<CodeBlockContent>{terminalOutput}</CodeBlockContent>
</CodeBlock>
</div>
)Logs
export const Logs = () => {
const [expanded, setExpanded] = useState(false);
const outputId = useId();
return (
<div className="w-full max-w-lg">
<CodeBlock>
<CodeBlockHeader>
<CodeBlockTitle>Logs</CodeBlockTitle>
<CodeBlockActions>
<CodeBlockCopyButton value={logOutput} />
</CodeBlockActions>
</CodeBlockHeader>
<CodeBlockContent
id={outputId}
className={expanded ? undefined : "max-h-16 overflow-y-hidden"}
>
{logOutput}
</CodeBlockContent>
<CodeBlockFooter className="justify-center">
<Button
type="button"
variant="pill"
size="xs"
aria-expanded={expanded}
aria-controls={outputId}
onClick={() => setExpanded((current) => !current)}
>
{expanded ? "Show less" : "Show more"}
</Button>
</CodeBlockFooter>
</CodeBlock>
</div>
);
};