Chatbot

Code Block

Provides syntax highlighting, line numbers, and copy to clipboard functionality for code blocks.

The CodeBlock component provides syntax highlighting, line numbers, and copy to clipboard functionality for code blocks.

"use client";import { CodeBlock, CodeBlockCopyButton } from "@/components/ai-elements/elements/code-block";const code = `function MyComponent(props) {  return (    <div>      <h1>Hello, {props.name}!</h1>      <p>This is an example React component.</p>    </div>  );}`;const Example = () => (  <CodeBlock code={code} language="jsx">    <CodeBlockCopyButton      onCopy={() => console.log("Copied code to clipboard")}      onError={() => console.error("Failed to copy code to clipboard")}    />  </CodeBlock>);export default Example;

Installation

npx ai-elements@latest add code-block
npx shadcn@latest add @ai-elements/code-block
"use client";import { Button } from "@repo/shadcn-ui/components/ui/button";import { cn } from "@repo/shadcn-ui/lib/utils";import type { Element } from "hast";import { CheckIcon, CopyIcon } from "lucide-react";import {  type ComponentProps,  createContext,  type HTMLAttributes,  useContext,  useEffect,  useRef,  useState,} from "react";import { type BundledLanguage, codeToHtml, type ShikiTransformer } from "shiki";type CodeBlockProps = HTMLAttributes<HTMLDivElement> & {  code: string;  language: BundledLanguage;  showLineNumbers?: boolean;};type CodeBlockContextType = {  code: string;};const CodeBlockContext = createContext<CodeBlockContextType>({  code: "",});const lineNumberTransformer: ShikiTransformer = {  name: "line-numbers",  line(node: Element, line: number) {    node.children.unshift({      type: "element",      tagName: "span",      properties: {        className: [          "inline-block",          "min-w-10",          "mr-4",          "text-right",          "select-none",          "text-muted-foreground",        ],      },      children: [{ type: "text", value: String(line) }],    });  },};export async function highlightCode(  code: string,  language: BundledLanguage,  showLineNumbers = false) {  const transformers: ShikiTransformer[] = showLineNumbers    ? [lineNumberTransformer]    : [];  return await Promise.all([    codeToHtml(code, {      lang: language,      theme: "one-light",      transformers,    }),    codeToHtml(code, {      lang: language,      theme: "one-dark-pro",      transformers,    }),  ]);}export const CodeBlock = ({  code,  language,  showLineNumbers = false,  className,  children,  ...props}: CodeBlockProps) => {  const [html, setHtml] = useState<string>("");  const [darkHtml, setDarkHtml] = useState<string>("");  const mounted = useRef(false);  useEffect(() => {    highlightCode(code, language, showLineNumbers).then(([light, dark]) => {      if (!mounted.current) {        setHtml(light);        setDarkHtml(dark);        mounted.current = true;      }    });    return () => {      mounted.current = false;    };  }, [code, language, showLineNumbers]);  return (    <CodeBlockContext.Provider value={{ code }}>      <div        className={cn(          "group relative w-full overflow-hidden rounded-md border bg-background text-foreground",          className        )}        {...props}      >        <div className="relative">          <div            className="overflow-hidden dark:hidden [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"            // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."            dangerouslySetInnerHTML={{ __html: html }}          />          <div            className="hidden overflow-hidden dark:block [&>pre]:m-0 [&>pre]:bg-background! [&>pre]:p-4 [&>pre]:text-foreground! [&>pre]:text-sm [&_code]:font-mono [&_code]:text-sm"            // biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."            dangerouslySetInnerHTML={{ __html: darkHtml }}          />          {children && (            <div className="absolute top-2 right-2 flex items-center gap-2">              {children}            </div>          )}        </div>      </div>    </CodeBlockContext.Provider>  );};export type CodeBlockCopyButtonProps = ComponentProps<typeof Button> & {  onCopy?: () => void;  onError?: (error: Error) => void;  timeout?: number;};export const CodeBlockCopyButton = ({  onCopy,  onError,  timeout = 2000,  children,  className,  ...props}: CodeBlockCopyButtonProps) => {  const [isCopied, setIsCopied] = useState(false);  const { code } = useContext(CodeBlockContext);  const copyToClipboard = async () => {    if (typeof window === "undefined" || !navigator?.clipboard?.writeText) {      onError?.(new Error("Clipboard API not available"));      return;    }    try {      await navigator.clipboard.writeText(code);      setIsCopied(true);      onCopy?.();      setTimeout(() => setIsCopied(false), timeout);    } catch (error) {      onError?.(error as Error);    }  };  const Icon = isCopied ? CheckIcon : CopyIcon;  return (    <Button      className={cn("shrink-0", className)}      onClick={copyToClipboard}      size="icon"      variant="ghost"      {...props}    >      {children ?? <Icon size={14} />}    </Button>  );};

Usage

import { CodeBlock, CodeBlockCopyButton } from '@/components/ai-elements/code-block';
<CodeBlock data={"console.log('hello world')"} language="jsx">
  <CodeBlockCopyButton
    onCopy={() => console.log('Copied code to clipboard')}
    onError={() => console.error('Failed to copy code to clipboard')}
  />
</CodeBlock>

Usage with AI SDK

Build a simple code generation tool using the experimental_useObject hook.

Add the following component to your frontend:

app/page.tsx
'use client';

import { experimental_useObject as useObject } from '@ai-sdk/react';
import { codeBlockSchema } from '@/app/api/codegen/route';
import {
  Input,
  PromptInputTextarea,
  PromptInputSubmit,
} from '@/components/ai-elements/prompt-input';
import {
  CodeBlock,
  CodeBlockCopyButton,
} from '@/components/ai-elements/code-block';
import { useState } from 'react';

export default function Page() {
  const [input, setInput] = useState('');
  const { object, submit, isLoading } = useObject({
    api: '/api/codegen',
    schema: codeBlockSchema,
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (input.trim()) {
      submit(input);
    }
  };

  return (
    <div className="max-w-4xl mx-auto p-6 relative size-full rounded-lg border h-[600px]">
      <div className="flex flex-col h-full">
        <div className="flex-1 overflow-auto mb-4">
          {object?.code && object?.language && (
            <CodeBlock
              code={object.code}
              language={object.language}
              showLineNumbers={true}
            >
              <CodeBlockCopyButton />
            </CodeBlock>
          )}
        </div>

        <Input
          onSubmit={handleSubmit}
          className="mt-4 w-full max-w-2xl mx-auto relative"
        >
          <PromptInputTextarea
            value={input}
            placeholder="Generate a React todolist component"
            onChange={(e) => setInput(e.currentTarget.value)}
            className="pr-12"
          />
          <PromptInputSubmit
            status={isLoading ? 'streaming' : 'ready'}
            disabled={!input.trim()}
            className="absolute bottom-1 right-1"
          />
        </Input>
      </div>
    </div>
  );
}

Add the following route to your backend:

api/codegen/route.ts
import { streamObject } from 'ai';
import { z } from 'zod';

export const codeBlockSchema = z.object({
  language: z.string(),
  filename: z.string(),
  code: z.string(),
});
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;

export async function POST(req: Request) {
  const context = await req.json();

  const result = streamObject({
    model: 'openai/gpt-4o',
    schema: codeBlockSchema,
    prompt:
      `You are a helpful coding assitant. Only generate code, no markdown formatting or backticks, or text.` +
      context,
  });

  return result.toTextStreamResponse();
}

Features

  • Syntax highlighting with react-syntax-highlighter
  • Line numbers (optional)
  • Copy to clipboard functionality
  • Automatic light/dark theme switching
  • Customizable styles
  • Accessible design

Examples

Dark Mode

To use the CodeBlock component in dark mode, you can wrap it in a div with the dark class.

"use client";import { CodeBlock, CodeBlockCopyButton } from "@/components/ai-elements/elements/code-block";const code = `function MyComponent(props) {  return (    <div>      <h1>Hello, {props.name}!</h1>      <p>This is an example React component.</p>    </div>  );}`;const Example = () => (  <div className="dark">    <CodeBlock code={code} language="jsx">      <CodeBlockCopyButton        onCopy={() => console.log("Copied code to clipboard")}        onError={() => console.error("Failed to copy code to clipboard")}      />    </CodeBlock>  </div>);export default Example;

Props

<CodeBlock />

Prop

Type

<CodeBlockCopyButton />

Prop

Type