Components

Code Block

A powerful, animated code block component with syntax highlighting, copy/download functionality, and multiple variants.

Last updated on

Edit on GitHub
import { CodeBlock } from "@/components/ui/code-block";
 
const code = `import React from "react";
 
function HelloWorld() {
  return (
    <div className="p-4 rounded-lg bg-primary text-primary-foreground">
      <h1>Hello, World!</h1>
      <p>This is a simple React component.</p>
    </div>
  );
}
 
export HelloWorld;`;
 
export function CodeBlockDemo() {
  return (
    <div className="w-full max-w-3xl">
      <CodeBlock
        code={code}
        language="tsx"
        title="HelloWorld.tsx"
        highlightLines={[6]}
      />
    </div>
  );
}

Installation

CLI

npx shadcn@latest add "https://jolyui.dev/r/code-block"

Manual

Install the following dependencies:

npm install lucide-react motion prism-react-renderer

Copy and paste the following code into your project.

import {
  Check,
  Copy,
  Download,
  FileCode,
  Maximize2,
  Minimize2,
  Terminal,
  WrapText,
} from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { Highlight, type Language, themes } from "prism-react-renderer";
import * as React from "react";
import { cn } from "@/lib/utils";
 
type CodeBlockVariant =
  | "default"
  | "terminal"
  | "minimal"
  | "gradient"
  | "glass";
type AnimationType = "none" | "fadeIn" | "slideIn" | "typewriter" | "highlight";
type ThemeType =
  | "oneDark"
  | "dracula"
  | "github"
  | "nightOwl"
  | "oceanicNext"
  | "palenight"
  | "shadesOfPurple"
  | "synthwave84"
  | "vsDark"
  | "vsLight";
 
// Theme mapping
const themeMap: Record<ThemeType, typeof themes.oneDark> = {
  oneDark: themes.oneDark,
  dracula: themes.dracula,
  github: themes.github,
  nightOwl: themes.nightOwl,
  oceanicNext: themes.oceanicNext,
  palenight: themes.palenight,
  shadesOfPurple: themes.shadesOfPurple,
  synthwave84: themes.synthwave84,
  vsDark: themes.vsDark,
  vsLight: themes.vsLight,
};
 
// Supported languages list
const supportedLanguages = [
  "javascript",
  "typescript",
  "jsx",
  "tsx",
  "python",
  "bash",
  "shell",
  "css",
  "scss",
  "html",
  "json",
  "yaml",
  "markdown",
  "sql",
  "graphql",
  "rust",
  "go",
  "java",
  "c",
  "cpp",
  "csharp",
  "php",
  "ruby",
  "swift",
  "kotlin",
  "scala",
  "r",
  "lua",
  "perl",
  "haskell",
  "elixir",
  "clojure",
  "dockerfile",
  "toml",
  "ini",
  "xml",
  "diff",
  "makefile",
  "regex",
] as const;
 
interface CodeBlockProps {
  code: string;
  language?: Language | string;
  title?: string;
  showLineNumbers?: boolean;
  highlightLines?: number[];
  addedLines?: number[];
  removedLines?: number[];
  variant?: CodeBlockVariant;
  animation?: AnimationType;
  animationDelay?: number;
  className?: string;
  copyable?: boolean;
  downloadable?: boolean;
  downloadFileName?: string;
  maxHeight?: string;
  theme?: ThemeType;
  wrapLongLines?: boolean;
  showLanguage?: boolean;
  collapsible?: boolean;
  defaultCollapsed?: boolean;
  startingLineNumber?: number;
  caption?: string;
}
 
// Copy button component
const CopyButton = ({
  code,
  className,
}: {
  code: string;
  className?: string;
}) => {
  const [copied, setCopied] = React.useState(false);
 
  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(code);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch (err) {
      console.error("Failed to copy:", err);
    }
  };
 
  return (
    <motion.button
      onClick={handleCopy}
      className={cn(
        "rounded-md p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground",
        className,
      )}
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      aria-label={copied ? "Copied!" : "Copy code"}
      title={copied ? "Copied!" : "Copy code"}
    >
      <AnimatePresence mode="wait">
        {copied ? (
          <motion.div
            key="check"
            initial={{ scale: 0, rotate: -180 }}
            animate={{ scale: 1, rotate: 0 }}
            exit={{ scale: 0, rotate: 180 }}
            transition={{ duration: 0.2 }}
          >
            <Check className="h-4 w-4 text-green-500" />
          </motion.div>
        ) : (
          <motion.div
            key="copy"
            initial={{ scale: 0 }}
            animate={{ scale: 1 }}
            exit={{ scale: 0 }}
            transition={{ duration: 0.2 }}
          >
            <Copy className="h-4 w-4" />
          </motion.div>
        )}
      </AnimatePresence>
    </motion.button>
  );
};
 
// Download button component
const DownloadButton = ({
  code,
  fileName,
  language,
}: {
  code: string;
  fileName?: string;
  language: string;
}) => {
  const handleDownload = () => {
    const extension = getFileExtension(language);
    const name = fileName || `code.${extension}`;
    const blob = new Blob([code], { type: "text/plain" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url;
    a.download = name;
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };
 
  return (
    <motion.button
      onClick={handleDownload}
      className="rounded-md p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
      whileHover={{ scale: 1.05 }}
      whileTap={{ scale: 0.95 }}
      aria-label="Download code"
      title="Download code"
    >
      <Download className="h-4 w-4" />
    </motion.button>
  );
};
 
// Get file extension from language
const getFileExtension = (language: string): string => {
  const extensions: Record<string, string> = {
    javascript: "js",
    typescript: "ts",
    jsx: "jsx",
    tsx: "tsx",
    python: "py",
    bash: "sh",
    shell: "sh",
    css: "css",
    scss: "scss",
    html: "html",
    json: "json",
    yaml: "yml",
    markdown: "md",
    sql: "sql",
    graphql: "graphql",
    rust: "rs",
    go: "go",
    java: "java",
    c: "c",
    cpp: "cpp",
    csharp: "cs",
    php: "php",
    ruby: "rb",
    swift: "swift",
    kotlin: "kt",
    dockerfile: "dockerfile",
    toml: "toml",
    xml: "xml",
  };
  return extensions[language] || "txt";
};
 
// Language display names
const getLanguageDisplayName = (language: string): string => {
  const names: Record<string, string> = {
    javascript: "JavaScript",
    typescript: "TypeScript",
    jsx: "JSX",
    tsx: "TSX",
    python: "Python",
    bash: "Bash",
    shell: "Shell",
    css: "CSS",
    scss: "SCSS",
    html: "HTML",
    json: "JSON",
    yaml: "YAML",
    markdown: "Markdown",
    sql: "SQL",
    graphql: "GraphQL",
    rust: "Rust",
    go: "Go",
    java: "Java",
    c: "C",
    cpp: "C++",
    csharp: "C#",
    php: "PHP",
    ruby: "Ruby",
    swift: "Swift",
    kotlin: "Kotlin",
    dockerfile: "Dockerfile",
    toml: "TOML",
    xml: "XML",
    diff: "Diff",
  };
  return (
    names[language] || language.charAt(0).toUpperCase() + language.slice(1)
  );
};
 
// Typewriter code animation component
const TypewriterCode = ({
  code,
  language,
  speed = 20,
  showLineNumbers,
  highlightLines = [],
  startingLineNumber = 1,
  theme,
}: {
  code: string;
  language: Language;
  speed?: number;
  showLineNumbers: boolean;
  highlightLines: number[];
  startingLineNumber: number;
  theme: typeof themes.oneDark;
}) => {
  const [displayedCode, setDisplayedCode] = React.useState("");
  const [currentIndex, setCurrentIndex] = React.useState(0);
 
  React.useEffect(() => {
    if (currentIndex >= code.length) return;
 
    const timeout = setTimeout(() => {
      setDisplayedCode(code.slice(0, currentIndex + 1));
      setCurrentIndex((prev) => prev + 1);
    }, speed);
 
    return () => clearTimeout(timeout);
  }, [currentIndex, code, speed]);
 
  return (
    <div className="relative">
      <Highlight theme={theme} code={displayedCode || " "} language={language}>
        {({ className, style, tokens, getLineProps, getTokenProps }) => (
          <pre
            className={cn(
              className,
              "!bg-transparent font-mono text-sm leading-relaxed",
            )}
            style={{ ...style, background: "transparent" }}
          >
            {tokens.map((line, i) => {
              const lineNumber = i + startingLineNumber;
              const isHighlighted = highlightLines.includes(lineNumber);
              return (
                <div
                  key={i}
                  {...getLineProps({ line })}
                  className={cn(
                    "flex",
                    isHighlighted &&
                      "-mx-4 border-primary border-l-2 bg-primary/10 px-4",
                  )}
                >
                  {showLineNumbers && (
                    <span className="mr-4 inline-block w-8 shrink-0 select-none text-right text-muted-foreground/50">
                      {lineNumber}
                    </span>
                  )}
                  <span className="flex-1">
                    {line.map((token, key) => (
                      <span key={key} {...getTokenProps({ token })} />
                    ))}
                  </span>
                </div>
              );
            })}
          </pre>
        )}
      </Highlight>
      {currentIndex < code.length && (
        <motion.span
          className="absolute inline-block h-4 w-2 bg-primary"
          animate={{ opacity: [1, 0] }}
          transition={{ duration: 0.5, repeat: Infinity }}
        />
      )}
    </div>
  );
};
 
// Main CodeBlock component
const CodeBlock = React.forwardRef<HTMLDivElement, CodeBlockProps>(
  (
    {
      code,
      language = "typescript",
      title,
      showLineNumbers = true,
      highlightLines = [],
      addedLines = [],
      removedLines = [],
      variant = "default",
      animation = "fadeIn",
      animationDelay = 0,
      className,
      copyable = true,
      downloadable = false,
      downloadFileName,
      maxHeight,
      theme = "oneDark",
      wrapLongLines = false,
      showLanguage = true,
      collapsible = false,
      defaultCollapsed = false,
      startingLineNumber = 1,
      caption,
    },
    ref,
  ) => {
    const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed);
    const [isExpanded, setIsExpanded] = React.useState(false);
    const [wordWrap, setWordWrap] = React.useState(wrapLongLines);
    const trimmedCode = code.trim();
    const selectedTheme = themeMap[theme] || themes.oneDark;
 
    const variantStyles: Record<CodeBlockVariant, string> = {
      default: "bg-card border border-border shadow-sm",
      terminal: "bg-[#1a1b26] border border-border shadow-lg",
      minimal: "bg-muted/50",
      gradient:
        "bg-gradient-to-br from-card via-card to-primary/5 border border-border shadow-md",
      glass: "bg-card/80 backdrop-blur-xl border border-border/50 shadow-xl",
    };
 
    const headerStyles: Record<CodeBlockVariant, string> = {
      default: "border-b border-border bg-muted/50",
      terminal: "border-b border-border bg-[#16161e]",
      minimal: "border-b border-border/50",
      gradient: "border-b border-border bg-muted/30",
      glass: "border-b border-border/50 bg-muted/30 backdrop-blur-sm",
    };
 
    const containerAnimation = {
      fadeIn: {
        initial: { opacity: 0, y: 20 },
        animate: { opacity: 1, y: 0 },
        transition: { duration: 0.4, delay: animationDelay },
      },
      slideIn: {
        initial: { opacity: 0, x: -20 },
        animate: { opacity: 1, x: 0 },
        transition: { duration: 0.4, delay: animationDelay },
      },
      highlight: {
        initial: { opacity: 0, scale: 0.98 },
        animate: { opacity: 1, scale: 1 },
        transition: { duration: 0.3, delay: animationDelay },
      },
      none: {
        initial: {},
        animate: {},
        transition: {},
      },
      typewriter: {
        initial: { opacity: 0 },
        animate: { opacity: 1 },
        transition: { duration: 0.2 },
      },
    };
 
    const currentAnimation =
      containerAnimation[animation] || containerAnimation.fadeIn;
 
    return (
      <motion.div
        ref={ref}
        className={cn(
          "overflow-hidden rounded-lg",
          variantStyles[variant],
          isExpanded && "fixed inset-4 z-50",
          className,
        )}
        initial={currentAnimation.initial}
        animate={currentAnimation.animate}
        transition={currentAnimation.transition}
      >
        {/* Header */}
        <div
          className={cn(
            "flex items-center justify-between px-4 py-2",
            headerStyles[variant],
          )}
        >
          <div className="flex items-center gap-3">
            {/* Window controls */}
            <div className="flex gap-1.5">
              <div className="h-3 w-3 rounded-full bg-red-500/80 transition-colors hover:bg-red-500" />
              <div className="h-3 w-3 rounded-full bg-yellow-500/80 transition-colors hover:bg-yellow-500" />
              <div className="h-3 w-3 rounded-full bg-green-500/80 transition-colors hover:bg-green-500" />
            </div>
 
            {/* Title or language */}
            <div className="flex items-center gap-2 text-muted-foreground text-sm">
              {variant === "terminal" ? (
                <Terminal className="h-4 w-4" />
              ) : (
                <FileCode className="h-4 w-4" />
              )}
              <span className="font-medium">
                {title || (showLanguage && getLanguageDisplayName(language))}
              </span>
            </div>
          </div>
 
          {/* Action buttons */}
          <div className="flex items-center gap-1">
            {/* Word wrap toggle */}
            <motion.button
              onClick={() => setWordWrap(!wordWrap)}
              className={cn(
                "rounded-md p-2 transition-colors hover:bg-muted",
                wordWrap
                  ? "text-primary"
                  : "text-muted-foreground hover:text-foreground",
              )}
              whileHover={{ scale: 1.05 }}
              whileTap={{ scale: 0.95 }}
              aria-label="Toggle word wrap"
              title="Toggle word wrap"
            >
              <WrapText className="h-4 w-4" />
            </motion.button>
 
            {/* Expand toggle */}
            <motion.button
              onClick={() => setIsExpanded(!isExpanded)}
              className="rounded-md p-2 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"
              whileHover={{ scale: 1.05 }}
              whileTap={{ scale: 0.95 }}
              aria-label={isExpanded ? "Minimize" : "Maximize"}
              title={isExpanded ? "Minimize" : "Maximize"}
            >
              {isExpanded ? (
                <Minimize2 className="h-4 w-4" />
              ) : (
                <Maximize2 className="h-4 w-4" />
              )}
            </motion.button>
 
            {/* Download button */}
            {downloadable && (
              <DownloadButton
                code={trimmedCode}
                fileName={downloadFileName}
                language={language}
              />
            )}
 
            {/* Copy button */}
            {copyable && <CopyButton code={trimmedCode} />}
 
            {/* Collapse toggle */}
            {collapsible && (
              <motion.button
                onClick={() => setIsCollapsed(!isCollapsed)}
                className="rounded-md px-2 py-1 text-muted-foreground text-xs transition-colors hover:bg-muted hover:text-foreground"
                whileHover={{ scale: 1.02 }}
                whileTap={{ scale: 0.98 }}
              >
                {isCollapsed ? "Expand" : "Collapse"}
              </motion.button>
            )}
          </div>
        </div>
 
        {/* Code content */}
        <AnimatePresence>
          {!isCollapsed && (
            <motion.div
              initial={{ height: 0, opacity: 0 }}
              animate={{ height: "auto", opacity: 1 }}
              exit={{ height: 0, opacity: 0 }}
              transition={{ duration: 0.2 }}
              className={cn(
                "overflow-auto p-4",
                wordWrap && "whitespace-pre-wrap break-words",
              )}
              style={maxHeight && !isExpanded ? { maxHeight } : undefined}
            >
              {animation === "typewriter" ? (
                <TypewriterCode
                  code={trimmedCode}
                  language={language as Language}
                  showLineNumbers={showLineNumbers}
                  highlightLines={highlightLines}
                  startingLineNumber={startingLineNumber}
                  theme={selectedTheme}
                />
              ) : (
                <Highlight
                  theme={selectedTheme}
                  code={trimmedCode}
                  language={language as Language}
                >
                  {({
                    className: preClassName,
                    style,
                    tokens,
                    getLineProps,
                    getTokenProps,
                  }) => (
                    <pre
                      className={cn(
                        preClassName,
                        "!bg-transparent font-mono text-sm leading-relaxed",
                      )}
                      style={{ ...style, background: "transparent" }}
                    >
                      {tokens.map((line, i) => {
                        const lineNumber = i + startingLineNumber;
                        const isHighlighted =
                          highlightLines.includes(lineNumber);
                        const isAdded = addedLines.includes(lineNumber);
                        const isRemoved = removedLines.includes(lineNumber);
 
                        return (
                          <motion.div
                            key={i}
                            {...getLineProps({ line })}
                            className={cn(
                              "flex",
                              isHighlighted &&
                                "-mx-4 border-primary border-l-2 bg-primary/10 px-4",
                              isAdded &&
                                "-mx-4 border-green-500 border-l-2 bg-green-500/10 px-4",
                              isRemoved &&
                                "-mx-4 border-red-500 border-l-2 bg-red-500/10 px-4 line-through opacity-60",
                            )}
                            initial={
                              animation === "slideIn"
                                ? { opacity: 0, x: -10 }
                                : animation === "highlight"
                                  ? {
                                      backgroundColor:
                                        "hsl(var(--primary) / 0.2)",
                                    }
                                  : {}
                            }
                            animate={
                              animation === "slideIn"
                                ? { opacity: 1, x: 0 }
                                : animation === "highlight"
                                  ? { backgroundColor: "transparent" }
                                  : {}
                            }
                            transition={{
                              duration: 0.3,
                              delay: animationDelay + i * 0.03,
                            }}
                          >
                            {showLineNumbers && (
                              <span className="mr-4 inline-block w-8 shrink-0 select-none text-right text-muted-foreground/50">
                                {isAdded && (
                                  <span className="mr-1 text-green-500">+</span>
                                )}
                                {isRemoved && (
                                  <span className="mr-1 text-red-500">-</span>
                                )}
                                {lineNumber}
                              </span>
                            )}
                            <span
                              className={cn("flex-1", wordWrap && "break-all")}
                            >
                              {line.map((token, key) => (
                                <span key={key} {...getTokenProps({ token })} />
                              ))}
                            </span>
                          </motion.div>
                        );
                      })}
                    </pre>
                  )}
                </Highlight>
              )}
            </motion.div>
          )}
        </AnimatePresence>
 
        {/* Caption */}
        {caption && (
          <div className="border-border border-t px-4 py-2 text-muted-foreground text-xs">
            {caption}
          </div>
        )}
      </motion.div>
    );
  },
);
 
CodeBlock.displayName = "CodeBlock";
 
// Inline code component
interface InlineCodeProps {
  children: string;
  className?: string;
  variant?: "default" | "primary" | "success" | "warning" | "error";
}
 
const InlineCode = React.forwardRef<HTMLSpanElement, InlineCodeProps>(
  ({ children, className, variant = "default" }, ref) => {
    const variantStyles: Record<string, string> = {
      default: "bg-muted text-foreground",
      primary: "bg-primary/10 text-primary",
      success: "bg-green-500/10 text-green-600 dark:text-green-400",
      warning: "bg-yellow-500/10 text-yellow-600 dark:text-yellow-400",
      error: "bg-red-500/10 text-red-600 dark:text-red-400",
    };
 
    return (
      <code
        ref={ref}
        className={cn(
          "rounded-md px-1.5 py-0.5 font-mono text-sm",
          variantStyles[variant],
          className,
        )}
      >
        {children}
      </code>
    );
  },
);
 
InlineCode.displayName = "InlineCode";
 
// Code comparison component
interface CodeCompareProps {
  before: string;
  after: string;
  language?: string;
  beforeTitle?: string;
  afterTitle?: string;
  className?: string;
  theme?: ThemeType;
  showDiff?: boolean;
}
 
const CodeCompare = React.forwardRef<HTMLDivElement, CodeCompareProps>(
  (
    {
      before,
      after,
      language = "typescript",
      beforeTitle = "Before",
      afterTitle = "After",
      className,
      theme = "oneDark",
      showDiff = false,
    },
    ref,
  ) => {
    // Simple diff calculation for line additions/removals
    const beforeLines = before.trim().split("\n");
    const afterLines = after.trim().split("\n");
 
    const removedLines = showDiff
      ? beforeLines
          .map((_, i) => i + 1)
          .filter(
            (_, i) => beforeLines[i] && !afterLines.includes(beforeLines[i]),
          )
      : [];
    const addedLines = showDiff
      ? afterLines
          .map((_, i) => i + 1)
          .filter(
            (_, i) => afterLines[i] && !beforeLines.includes(afterLines[i]),
          )
      : [];
 
    return (
      <div ref={ref} className={cn("grid gap-4 md:grid-cols-2", className)}>
        <CodeBlock
          code={before}
          language={language}
          title={beforeTitle}
          variant="default"
          animation="slideIn"
          theme={theme}
          removedLines={removedLines}
        />
        <CodeBlock
          code={after}
          language={language}
          title={afterTitle}
          variant="gradient"
          animation="slideIn"
          animationDelay={0.2}
          theme={theme}
          addedLines={addedLines}
        />
      </div>
    );
  },
);
 
CodeCompare.displayName = "CodeCompare";
 
// Animated code tabs
interface CodeTabsProps {
  tabs: Array<{
    label: string;
    code: string;
    language?: string;
    icon?: React.ReactNode;
  }>;
  className?: string;
  theme?: ThemeType;
  defaultTab?: number;
}
 
const CodeTabs = React.forwardRef<HTMLDivElement, CodeTabsProps>(
  ({ tabs, className, theme = "oneDark", defaultTab = 0 }, ref) => {
    const [activeTab, setActiveTab] = React.useState(defaultTab);
 
    return (
      <div
        ref={ref}
        className={cn(
          "overflow-hidden rounded-lg border border-border bg-card shadow-sm",
          className,
        )}
      >
        {/* Tab headers */}
        <div className="flex overflow-x-auto border-border border-b bg-muted/50">
          {tabs.map((tab, index) => (
            <motion.button
              key={index}
              onClick={() => setActiveTab(index)}
              className={cn(
                "flex items-center gap-2 whitespace-nowrap px-4 py-2.5 font-medium text-sm transition-colors",
                activeTab === index
                  ? "border-primary border-b-2 bg-background/50 text-primary"
                  : "text-muted-foreground hover:bg-muted/50 hover:text-foreground",
              )}
              whileHover={{
                backgroundColor:
                  activeTab === index ? undefined : "hsl(var(--muted) / 0.5)",
              }}
              whileTap={{ scale: 0.98 }}
            >
              {tab.icon}
              {tab.label}
            </motion.button>
          ))}
        </div>
 
        {/* Tab content */}
        <AnimatePresence mode="wait">
          <motion.div
            key={activeTab}
            initial={{ opacity: 0, y: 10 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, y: -10 }}
            transition={{ duration: 0.2 }}
          >
            <CodeBlock
              code={tabs[activeTab]?.code || ""}
              language={tabs[activeTab]?.language || "typescript"}
              showLineNumbers={true}
              variant="minimal"
              animation="none"
              copyable={true}
              className="rounded-none border-0"
              theme={theme}
            />
          </motion.div>
        </AnimatePresence>
      </div>
    );
  },
);
 
CodeTabs.displayName = "CodeTabs";
 
// Terminal/Command component
interface TerminalBlockProps {
  commands: Array<{
    command: string;
    output?: string;
  }>;
  title?: string;
  className?: string;
  animated?: boolean;
}
 
const TerminalBlock = React.forwardRef<HTMLDivElement, TerminalBlockProps>(
  ({ commands, title = "Terminal", className, animated = true }, ref) => {
    return (
      <motion.div
        ref={ref}
        className={cn(
          "overflow-hidden rounded-lg border border-border bg-[#1a1b26] shadow-lg",
          className,
        )}
        initial={animated ? { opacity: 0, y: 20 } : {}}
        animate={animated ? { opacity: 1, y: 0 } : {}}
        transition={{ duration: 0.4 }}
      >
        {/* Header */}
        <div className="flex items-center gap-3 border-border border-b bg-[#16161e] px-4 py-2">
          <div className="flex gap-1.5">
            <div className="h-3 w-3 rounded-full bg-red-500/80" />
            <div className="h-3 w-3 rounded-full bg-yellow-500/80" />
            <div className="h-3 w-3 rounded-full bg-green-500/80" />
          </div>
          <div className="flex items-center gap-2 text-muted-foreground text-sm">
            <Terminal className="h-4 w-4" />
            <span>{title}</span>
          </div>
        </div>
 
        {/* Commands */}
        <div className="p-4 font-mono text-sm">
          {commands.map((item, index) => (
            <motion.div
              key={index}
              initial={animated ? { opacity: 0, x: -10 } : {}}
              animate={animated ? { opacity: 1, x: 0 } : {}}
              transition={{ delay: index * 0.1 }}
              className="mb-2 last:mb-0"
            >
              <div className="flex items-center gap-2">
                <span className="text-green-400">$</span>
                <span className="text-foreground">{item.command}</span>
              </div>
              {item.output && (
                <div className="mt-1 whitespace-pre-wrap pl-4 text-muted-foreground">
                  {item.output}
                </div>
              )}
            </motion.div>
          ))}
        </div>
      </motion.div>
    );
  },
);
 
TerminalBlock.displayName = "TerminalBlock";
 
export {
  CodeBlock,
  CodeCompare,
  CodeTabs,
  InlineCode,
  supportedLanguages,
  TerminalBlock,
  themeMap,
};
export type {
  AnimationType,
  CodeBlockProps,
  CodeBlockVariant,
  CodeCompareProps,
  CodeTabsProps,
  InlineCodeProps,
  TerminalBlockProps,
  ThemeType,
};

Features

  • Syntax Highlighting: Supports over 30 languages using prism-react-renderer.
  • Multiple Variants: Default, Terminal, Minimal, Gradient, and Glass styles.
  • Animations: Fade in, Slide in, Typewriter, and Highlight effects.
  • Interactive: Copy to clipboard, download code, collapse/expand.
  • Line Handling: Line numbers, line highlighting, added/removed lines (diff).
  • Special Components: Includes CodeCompare, CodeTabs, and TerminalBlock.

Examples

Terminal

Terminal variant with fade in animation.

import { TerminalBlock } from "@/components/ui/code-block";
 
export function CodeBlockTerminalDemo() {
  return (
    <div className="w-full max-w-3xl">
      <TerminalBlock
        title="Terminal"
        commands={[
          { command: "npm install jolyui", output: "added 1 package in 2.3s" },
          {
            command: "npm run dev",
            output:
              "VITE v5.0.0 ready in 300 ms\n\n➜  Local: http://localhost:5173/",
          },
        ]}
      />
    </div>
  );
}

Glass + Dracula Theme

Glass variant with dracula theme and highlight animation.

import { CodeBlock } from "@/components/ui/code-block";
 
export function CodeBlockTerminalDemo() {
  return (
    <div className="w-full max-w-3xl">
      <CodeBlock
        code={`const greeting = "Hello, jolyui!";
console.log(greeting);
 
// Supports multiple themes!`}
        language="typescript"
        variant="glass"
        theme="dracula"
        animation="highlight"
      />
    </div>
  );
}

Code Comparison

Display side-by-side code comparisons with diff highlighting.

import { CodeCompare } from "@/components/ui/code-block";
 
const beforeCode = `function add(a, b) {
  return a + b;
}`;
 
const afterCode = `function add(a: number, b: number): number {
  return a + b;
}`;
 
export function CodeBlockCompareDemo() {
  return (
    <div className="w-full max-w-4xl">
      <CodeCompare
        before={beforeCode}
        after={afterCode}
        language="typescript"
        beforeTitle="JavaScript"
        afterTitle="TypeScript"
        showDiff={true}
      />
    </div>
  );
}

Code Tabs

Organize multiple code snippets into tabs.

import { FileJson, FileType } from "lucide-react";
import { CodeTabs } from "@/components/ui/code-block";
 
const tsCode = `interface User {
  id: number;
  name: string;
  email: string;
}`;
 
const jsonCode = `{
  "id": 1,
  "name": "John Doe",
  "email": "john@example.com"
}`;
 
export function CodeBlockTabsDemo() {
  return (
    <div className="w-full max-w-3xl">
      <CodeTabs
        tabs={[
          {
            label: "types.ts",
            code: tsCode,
            language: "typescript",
            icon: <FileType className="h-4 w-4" />,
          },
          {
            label: "data.json",
            code: jsonCode,
            language: "json",
            icon: <FileJson className="h-4 w-4" />,
          },
        ]}
      />
    </div>
  );
}

API Reference

CodeBlock

The main component for displaying code.

Prop

Type

CodeCompare

A component for comparing two blocks of code.

Prop

Type

CodeTabs

A component for displaying multiple code blocks in tabs.

Prop

Type

TerminalBlock

A component for displaying terminal commands and outputs.

Prop

Type

How is this guide?

On this page