CodeBlock

A syntax-highlighted code block component with copy functionality and language detection

Installation

Terminal
pnpm add @choice-ui/code-block

Import

Component.tsx
import { CodeBlock, CodeBlockHeader, CodeBlockFooter, CodeBlockContent, CodeBlockCode, type CodeBlockProps, type CodeBlockHeaderProps, type CodeBlockFooterProps, type CodeBlockContentProps, type CodeBlockCodeProps, getDefaultFilenameForLanguage } from "@choice-ui/code-block"

Basic

Demonstrates different programming languages with automatic theme switching. The theme automatically adapts to the system/app theme (light/dark).
export function add(a: number, b: number): number {
  return a + b
}
{"name":"prompt-kit","version":"1.0.0","private":true}
diff --git a/index.ts b/index.ts
--- a/index.ts
+++ b/index.ts
@@
- console.log('hello')
+ console.info('hello world')

Long Code With Scroll

Shows how long code content is handled with vertical scrolling. Useful for displaying large code files without taking up too much space.
console.log('line 1')
console.log('line 2')
console.log('line 3')
console.log('line 4')
console.log('line 5')
console.log('line 6')
console.log('line 7')
console.log('line 8')
console.log('line 9')
console.log('line 10')
console.log('line 11')
console.log('line 12')
console.log('line 13')
console.log('line 14')
console.log('line 15')
console.log('line 16')
console.log('line 17')
console.log('line 18')
console.log('line 19')
console.log('line 20')
console.log('line 21')
console.log('line 22')
console.log('line 23')
console.log('line 24')
console.log('line 25')
console.log('line 26')
console.log('line 27')
console.log('line 28')
console.log('line 29')
console.log('line 30')
console.log('line 31')
console.log('line 32')
console.log('line 33')
console.log('line 34')
console.log('line 35')
console.log('line 36')
console.log('line 37')
console.log('line 38')
console.log('line 39')
console.log('line 40')
console.log('line 41')
console.log('line 42')
console.log('line 43')
console.log('line 44')
console.log('line 45')
console.log('line 46')
console.log('line 47')
console.log('line 48')
console.log('line 49')
console.log('line 50')
console.log('line 51')
console.log('line 52')
console.log('line 53')
console.log('line 54')
console.log('line 55')
console.log('line 56')
console.log('line 57')
console.log('line 58')
console.log('line 59')
console.log('line 60')
console.log('line 61')
console.log('line 62')
console.log('line 63')
console.log('line 64')
console.log('line 65')
console.log('line 66')
console.log('line 67')
console.log('line 68')
console.log('line 69')
console.log('line 70')
console.log('line 71')
console.log('line 72')
console.log('line 73')
console.log('line 74')
console.log('line 75')
console.log('line 76')
console.log('line 77')
console.log('line 78')
console.log('line 79')
console.log('line 80')

Long Code Without Scroll

Demonstrates code block without scroll area container. Use this when you want the code to naturally extend the container height.
console.log('line 1')
console.log('line 2')
console.log('line 3')
console.log('line 4')
console.log('line 5')
console.log('line 6')
console.log('line 7')
console.log('line 8')
console.log('line 9')
console.log('line 10')
console.log('line 11')
console.log('line 12')
console.log('line 13')
console.log('line 14')
console.log('line 15')
console.log('line 16')
console.log('line 17')
console.log('line 18')
console.log('line 19')
console.log('line 20')
console.log('line 21')
console.log('line 22')
console.log('line 23')
console.log('line 24')
console.log('line 25')
console.log('line 26')
console.log('line 27')
console.log('line 28')
console.log('line 29')
console.log('line 30')
console.log('line 31')
console.log('line 32')
console.log('line 33')
console.log('line 34')
console.log('line 35')
console.log('line 36')
console.log('line 37')
console.log('line 38')
console.log('line 39')
console.log('line 40')
console.log('line 41')
console.log('line 42')
console.log('line 43')
console.log('line 44')
console.log('line 45')
console.log('line 46')
console.log('line 47')
console.log('line 48')
console.log('line 49')
console.log('line 50')
console.log('line 51')
console.log('line 52')
console.log('line 53')
console.log('line 54')
console.log('line 55')
console.log('line 56')
console.log('line 57')
console.log('line 58')
console.log('line 59')
console.log('line 60')
console.log('line 61')
console.log('line 62')
console.log('line 63')
console.log('line 64')
console.log('line 65')
console.log('line 66')
console.log('line 67')
console.log('line 68')
console.log('line 69')
console.log('line 70')
console.log('line 71')
console.log('line 72')
console.log('line 73')
console.log('line 74')
console.log('line 75')
console.log('line 76')
console.log('line 77')
console.log('line 78')
console.log('line 79')
console.log('line 80')

Variants

Code block with different variants.
file.txt
console.log('Hello, world!')
file.txt
console.log('Hello, world!')
file.txt
console.log('Hello, world!')

Long Tsx Demo

Real-world example with a complex React component. Demonstrates how readable long code remains with syntax highlighting.
import React, { useState, useEffect, useCallback, useMemo } from "react"
import { 
  ChevronDown, 
  ChevronRight, 
  Code2, 
  Copy, 
  Check,
  Terminal,
  FileCode,
  Sparkles
} from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { tcx } from "~/utils"

interface CodeEditorProps {
  initialCode?: string
  language?: "typescript" | "javascript" | "python" | "rust" | "go"
  theme?: "dark" | "light"
  onCodeChange?: (code: string) => void
  readOnly?: boolean
  showLineNumbers?: boolean
  highlightLines?: number[]
  className?: string
}

export const CodeEditor: React.FC<CodeEditorProps> = ({
  initialCode = "",
  language = "typescript",
  theme = "dark",
  onCodeChange,
  readOnly = false,
  showLineNumbers = true,
  highlightLines = [],
  className
}) => {
  const [code, setCode] = useState(initialCode)
  const [copied, setCopied] = useState(false)
  const [isExpanded, setIsExpanded] = useState(true)
  const [hoveredLine, setHoveredLine] = useState<number | null>(null)

  // Memoize syntax highlighting
  const highlightedCode = useMemo(() => {
    return highlightSyntax(code, language)
  }, [code, language])

  // Handle code changes
  const handleCodeChange = useCallback((newCode: string) => {
    if (!readOnly) {
      setCode(newCode)
      onCodeChange?.(newCode)
    }
  }, [readOnly, onCodeChange])

  // Copy to clipboard functionality
  const copyToClipboard = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(code)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch (err) {
      console.error("Failed to copy:", err)
    }
  }, [code])

  // Auto-save functionality
  useEffect(() => {
    const autoSaveTimer = setTimeout(() => {
      if (code !== initialCode) {
        console.log("Auto-saving code...")
        // Implement auto-save logic here
      }
    }, 2000)

    return () => clearTimeout(autoSaveTimer)
  }, [code, initialCode])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyPress = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "s") {
        e.preventDefault()
        console.log("Saving code...")
      }
      if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "f") {
        e.preventDefault()
        console.log("Formatting code...")
      }
    }

    window.addEventListener("keydown", handleKeyPress)
    return () => window.removeEventListener("keydown", handleKeyPress)
  }, [])

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
      className={tcx(
        "relative rounded-lg overflow-hidden shadow-2xl",
        theme === "dark" ? "bg-gray-900" : "bg-white",
        className
      )}
    >
      {/* Header */}
      <div className={tcx(
        "flex items-center justify-between px-4 py-3 border-b",
        theme === "dark" ? "bg-gray-800 border-gray-700" : "bg-gray-50 border-gray-200"
      )}>
        <div className="flex items-center gap-3">
          <button
            onClick={() => setIsExpanded(!isExpanded)}
            className="p-1 rounded hover:bg-gray-700 transition-colors"
          >
            {isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
          </button>
          <FileCode size={18} className="text-blue-500" />
          <span className="text-body-small font-mono">{language}</span>
        </div>

        <div className="flex items-center gap-2">
          <button
            onClick={copyToClipboard}
            className={tcx(
              "p-2 rounded transition-all",
              theme === "dark" ? "hover:bg-gray-700" : "hover:bg-gray-200"
            )}
          >
            <AnimatePresence mode="wait">
              {copied ? (
                <motion.div
                  initial={{ scale: 0 }}
                  animate={{ scale: 1 }}
                  exit={{ scale: 0 }}
                >
                  <Check size={16} className="text-green-500" />
                </motion.div>
              ) : (
                <Copy size={16} />
              )}
            </AnimatePresence>
          </button>
        </div>
      </div>

      {/* Code Content */}
      <AnimatePresence>
        {isExpanded && (
          <motion.div
            initial={{ height: 0 }}
            animate={{ height: "auto" }}
            exit={{ height: 0 }}
            transition={{ duration: 0.2 }}
            className="overflow-hidden"
          >
            <div className="relative">
              {showLineNumbers && (
                <div className={tcx(
                  "absolute left-0 top-0 bottom-0 w-12 text-right pr-2",
                  theme === "dark" ? "bg-gray-850 text-gray-500" : "bg-gray-50 text-gray-400"
                )}>
                  {code.split("\n").map((_, index) => (
                    <div
                      key={index}
                      className={tcx(
                        "text-xs leading-6 font-mono",
                        highlightLines.includes(index + 1) && "text-yellow-500 font-bold"
                      )}
                    >
                      {index + 1}
                    </div>
                  ))}
                </div>
              )}

              <div
                className={tcx(
                  "p-4 overflow-x-auto",
                  showLineNumbers && "pl-16",
                  theme === "dark" ? "bg-gray-900" : "bg-white"
                )}
                contentEditable={!readOnly}
                suppressContentEditableWarning
                onInput={(e) => handleCodeChange(e.currentTarget.textContent || "")}
                style={{ minHeight: "200px" }}
              >
                <pre className="font-mono text-body-small leading-6">
                  <code dangerouslySetInnerHTML={{ __html: highlightedCode }} />
                </pre>
              </div>
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Footer Status Bar */}
      <div className={tcx(
        "flex items-center justify-between px-4 py-2 text-xs",
        theme === "dark" ? "bg-gray-800 text-gray-400" : "bg-gray-50 text-gray-600"
      )}>
        <div className="flex items-center gap-4">
          <span>Lines: {code.split("\n").length}</span>
          <span>Characters: {code.length}</span>
        </div>
        <div className="flex items-center gap-2">
          <Sparkles size={12} />
          <span>AI-powered suggestions available</span>
        </div>
      </div>
    </motion.div>
  )
}

// Utility function for syntax highlighting (simplified)
function highlightSyntax(code: string, language: string): string {
  // This is a simplified version - in production, use a proper syntax highlighter
  const keywords = {
    typescript: ["const", "let", "var", "function", "return", "if", "else", "for", "while", "class", "interface", "type", "import", "export", "from"],
    javascript: ["const", "let", "var", "function", "return", "if", "else", "for", "while", "class"],
    python: ["def", "class", "import", "from", "return", "if", "else", "for", "while", "in"],
    rust: ["fn", "let", "mut", "const", "struct", "impl", "trait", "use", "mod", "pub"],
    go: ["func", "var", "const", "type", "struct", "interface", "import", "package", "return"]
  }

  let highlighted = code
  keywords[language]?.forEach(keyword => {
    const regex = new RegExp(`\\b${keyword}\\b`, "g")
    highlighted = highlighted.replace(regex, `<span class="text-blue-500">${keyword}</span>`)
  })

  return highlighted
}

export default CodeEditor

With Interactive Header

Code block with interactive header showing filename and controls. Header includes language icon, filename, copy button, and expand/collapse toggle.
HelloWorld.tsx
import React from 'react'

export function Button({ label }: { label: string }) {
  return (
    <button className="px-3 py-1 rounded border">{label}</button>
  )
}

Collapsed By Default

Code block collapsed by default to save space. Users can click the expand button to reveal the code content.
example.py

Expanded Code

Long code that starts fully expanded, showing all lines. Useful when you want to display complete code without initial truncation.
LongFile.tsx
import React, { useState, useEffect, useCallback, useMemo } from "react"
import { 
  ChevronDown, 
  ChevronRight, 
  Code2, 
  Copy, 
  Check,
  Terminal,
  FileCode,
  Sparkles
} from "lucide-react"
import { motion, AnimatePresence } from "framer-motion"
import { tcx } from "~/utils"

interface CodeEditorProps {
  initialCode?: string
  language?: "typescript" | "javascript" | "python" | "rust" | "go"
  theme?: "dark" | "light"
  onCodeChange?: (code: string) => void
  readOnly?: boolean
  showLineNumbers?: boolean
  highlightLines?: number[]
  className?: string
}

export const CodeEditor: React.FC<CodeEditorProps> = ({
  initialCode = "",
  language = "typescript",
  theme = "dark",
  onCodeChange,
  readOnly = false,
  showLineNumbers = true,
  highlightLines = [],
  className
}) => {
  const [code, setCode] = useState(initialCode)
  const [copied, setCopied] = useState(false)
  const [isExpanded, setIsExpanded] = useState(true)
  const [hoveredLine, setHoveredLine] = useState<number | null>(null)

  // Memoize syntax highlighting
  const highlightedCode = useMemo(() => {
    return highlightSyntax(code, language)
  }, [code, language])

  // Handle code changes
  const handleCodeChange = useCallback((newCode: string) => {
    if (!readOnly) {
      setCode(newCode)
      onCodeChange?.(newCode)
    }
  }, [readOnly, onCodeChange])

  // Copy to clipboard functionality
  const copyToClipboard = useCallback(async () => {
    try {
      await navigator.clipboard.writeText(code)
      setCopied(true)
      setTimeout(() => setCopied(false), 2000)
    } catch (err) {
      console.error("Failed to copy:", err)
    }
  }, [code])

  // Auto-save functionality
  useEffect(() => {
    const autoSaveTimer = setTimeout(() => {
      if (code !== initialCode) {
        console.log("Auto-saving code...")
        // Implement auto-save logic here
      }
    }, 2000)

    return () => clearTimeout(autoSaveTimer)
  }, [code, initialCode])

  // Keyboard shortcuts
  useEffect(() => {
    const handleKeyPress = (e: KeyboardEvent) => {
      if ((e.metaKey || e.ctrlKey) && e.key === "s") {
        e.preventDefault()
        console.log("Saving code...")
      }
      if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === "f") {
        e.preventDefault()
        console.log("Formatting code...")
      }
    }

    window.addEventListener("keydown", handleKeyPress)
    return () => window.removeEventListener("keydown", handleKeyPress)
  }, [])

  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.3 }}
      className={tcx(
        "relative rounded-lg overflow-hidden shadow-2xl",
        theme === "dark" ? "bg-gray-900" : "bg-white",
        className
      )}
    >
      {/* Header */}
      <div className={tcx(
        "flex items-center justify-between px-4 py-3 border-b",
        theme === "dark" ? "bg-gray-800 border-gray-700" : "bg-gray-50 border-gray-200"
      )}>
        <div className="flex items-center gap-3">
          <button
            onClick={() => setIsExpanded(!isExpanded)}
            className="p-1 rounded hover:bg-gray-700 transition-colors"
          >
            {isExpanded ? <ChevronDown size={16} /> : <ChevronRight size={16} />}
          </button>
          <FileCode size={18} className="text-blue-500" />
          <span className="text-body-small font-mono">{language}</span>
        </div>

        <div className="flex items-center gap-2">
          <button
            onClick={copyToClipboard}
            className={tcx(
              "p-2 rounded transition-all",
              theme === "dark" ? "hover:bg-gray-700" : "hover:bg-gray-200"
            )}
          >
            <AnimatePresence mode="wait">
              {copied ? (
                <motion.div
                  initial={{ scale: 0 }}
                  animate={{ scale: 1 }}
                  exit={{ scale: 0 }}
                >
                  <Check size={16} className="text-green-500" />
                </motion.div>
              ) : (
                <Copy size={16} />
              )}
            </AnimatePresence>
          </button>
        </div>
      </div>

      {/* Code Content */}
      <AnimatePresence>
        {isExpanded && (
          <motion.div
            initial={{ height: 0 }}
            animate={{ height: "auto" }}
            exit={{ height: 0 }}
            transition={{ duration: 0.2 }}
            className="overflow-hidden"
          >
            <div className="relative">
              {showLineNumbers && (
                <div className={tcx(
                  "absolute left-0 top-0 bottom-0 w-12 text-right pr-2",
                  theme === "dark" ? "bg-gray-850 text-gray-500" : "bg-gray-50 text-gray-400"
                )}>
                  {code.split("\n").map((_, index) => (
                    <div
                      key={index}
                      className={tcx(
                        "text-xs leading-6 font-mono",
                        highlightLines.includes(index + 1) && "text-yellow-500 font-bold"
                      )}
                    >
                      {index + 1}
                    </div>
                  ))}
                </div>
              )}

              <div
                className={tcx(
                  "p-4 overflow-x-auto",
                  showLineNumbers && "pl-16",
                  theme === "dark" ? "bg-gray-900" : "bg-white"
                )}
                contentEditable={!readOnly}
                suppressContentEditableWarning
                onInput={(e) => handleCodeChange(e.currentTarget.textContent || "")}
                style={{ minHeight: "200px" }}
              >
                <pre className="font-mono text-body-small leading-6">
                  <code dangerouslySetInnerHTML={{ __html: highlightedCode }} />
                </pre>
              </div>
            </div>
          </motion.div>
        )}
      </AnimatePresence>

      {/* Footer Status Bar */}
      <div className={tcx(
        "flex items-center justify-between px-4 py-2 text-xs",
        theme === "dark" ? "bg-gray-800 text-gray-400" : "bg-gray-50 text-gray-600"
      )}>
        <div className="flex items-center gap-4">
          <span>Lines: {code.split("\n").length}</span>
          <span>Characters: {code.length}</span>
        </div>
        <div className="flex items-center gap-2">
          <Sparkles size={12} />
          <span>AI-powered suggestions available</span>
        </div>
      </div>
    </motion.div>
  )
}

// Utility function for syntax highlighting (simplified)
function highlightSyntax(code: string, language: string): string {
  // This is a simplified version - in production, use a proper syntax highlighter
  const keywords = {
    typescript: ["const", "let", "var", "function", "return", "if", "else", "for", "while", "class", "interface", "type", "import", "export", "from"],
    javascript: ["const", "let", "var", "function", "return", "if", "else", "for", "while", "class"],
    python: ["def", "class", "import", "from", "return", "if", "else", "for", "while", "in"],
    rust: ["fn", "let", "mut", "const", "struct", "impl", "trait", "use", "mod", "pub"],
    go: ["func", "var", "const", "type", "struct", "interface", "import", "package", "return"]
  }

  let highlighted = code
  keywords[language]?.forEach(keyword => {
    const regex = new RegExp(`\\b${keyword}\\b`, "g")
    highlighted = highlighted.replace(regex, `<span class="text-blue-500">${keyword}</span>`)
  })

  return highlighted
}

export default CodeEditor

Non Expandable

Code block with expand/collapse functionality disabled. Use this for short code snippets that don't need truncation.
config.json
{"name":"prompt-kit","version":"1.0.0","private":true}

Internationalization

Demonstrates internationalization support with custom labels. You can provide custom translations for buttons and tooltips.
国际化示例.tsx
import React from 'react'

export function Button({ label }: { label: string }) {
  return (
    <button className="px-3 py-1 rounded border">{label}</button>
  )
}

Language Icons

Comprehensive showcase of language-specific icons in the header. Icons are automatically selected based on the language or filename. Demonstrates JavaScript, TypeScript, markup, config files, and more.
script.js
console.log('Hello, World!');
Component.jsx
export const Button = () => <button>Click</button>
example.ts
export function add(a: number, b: number): number {
  return a + b
}
App.tsx
import React from 'react'

export function Button({ label }: { label: string }) {
  return (
    <button className="px-3 py-1 rounded border">{label}</button>
  )
}
types.d.ts
declare module 'my-lib' {
  export function hello(): string
}
styles.css
:root {
  --primary: #0ea5e9;
}

.button {
  background: var(--primary);
  color: white;
}
theme.scss
$primary: #0ea5e9;
.button {
  color: $primary;
}
index.html
<!doctype html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Example</title>
  </head>
  <body>
    <div id="app">Hello</div>
  </body>
</html>
config.xml
<?xml version="1.0"?>
<root>
  <item>value</item>
</root>
README.md
# Hello World

This is a markdown file.
docs.mdx
# Component Docs

import { Button } from './Button'

<Button />
package.json
{"name":"prompt-kit","version":"1.0.0","private":true}
config.yml
name: My App
version: 1.0.0
scripts:
  - build
  - test
deploy.sh
#!/bin/bash
echo "Deploying..."
npm run build
setup.sh
#!/bin/sh
export NODE_ENV=production
main.py
def add(a: int, b: int) -> int:
    return a + b

print(add(2, 3))
server.js
const express = require('express')
const app = express()
app.listen(3000)
notes.txt
This is a plain text file.
No syntax highlighting.
main.go
package main

func main() {
  println("Hello")
}
main.rs
fn main() {
    println!("Hello");
}
Main.java
public class Main {
  public static void main(String[] args) {
    System.out.println("Hello");
  }
}

Default Filename

component.tsx
import React from 'react'

export function Button({ label }: { label: string }) {
  return (
    <button className="px-3 py-1 rounded border">{label}</button>
  )
}

Line Count Threshold

Shows how the lineThreshold property controls when footer appears. Footer with expand controls only shows when code exceeds the threshold. Useful for managing space with varying code lengths.

10 lines (no footer, below threshold)

short.js
// Short code (5 lines)
function greet(name) {
  return `Hello, ${name}!`;
}
console.log(greet('World'));

12 lines (footer shown, above threshold)

medium.js
// Medium code (12 lines)
import React from 'react';

function TodoItem({ todo, onToggle }) {
  return (
    <li className="todo-item">
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
    </li>
  );
}

Custom threshold (3 lines)

custom.js
// Short code (5 lines)
function greet(name) {
  return `Hello, ${name}!`;
}
console.log(greet('World'));

Streaming With Auto Scroll

Streaming code blocks simulation with auto-scroll to bottom. Uses useStickToBottom hook to keep scroll position at bottom as new code blocks appear. Click "Add Code Block" to simulate streaming new content.
Blocks: 0 | At bottom: Yes
Click "Add Code Block" or "Auto Stream" to start

Character Streaming

Simulates character-by-character streaming of code content. Content appears gradually like AI-generated code output.
useUser.ts
// Code will appear here...

API reference

CodeBlockRootTypeDefault
className
string
|
undefined
-
defaultCodeExpanded
boolean
|
undefined
-
defaultExpanded
boolean
|
undefined
-
expandable
boolean
|
undefined
-
filename
string
|
undefined
-
language
string
|
undefined
-
lineThreshold
number
|
undefined
-
onCodeExpandChange
((expanded: boolean) => void)
|
undefined
-
onExpandChange
((expanded: boolean) => void)
|
undefined
-
variant
undefined
|
"default"
|
"light"
|
"dark"
-
CodeBlockPropsTypeDefault
className
string
|
undefined
-
defaultCodeExpanded
boolean
|
undefined
-
defaultExpanded
boolean
|
undefined
-
expandable
boolean
|
undefined
-
filename
string
|
undefined
-
language
string
|
undefined
-
lineThreshold
number
|
undefined
-
onCodeExpandChange
((expanded: boolean) => void)
|
undefined
-
onExpandChange
((expanded: boolean) => void)
|
undefined
-
variant
undefined
|
"default"
|
"light"
|
"dark"
-