Aura Design System

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-block

Manual

Install the following dependencies:

pnpm install @radix-ui/react-icons class-variance-authority

Copy 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">  &quot;version&quot;: &quot;1.2.0&quot;</CodeBlockLine>
        <CodeBlockLine variant="added">  &quot;version&quot;: &quot;1.3.0&quot;</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>
  );
};