Code Block

shadcn/ui's code block, animated. The code types itself out with a cursor at its end, the block follows the last line, and the copy button's icon swaps.

app/api/chat/route.ts

"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Copy01Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import * as CodeBlockPrimitive from "@animate-ui/react/primitives/code-block"
import {
  CopyButton,
  CopyButtonIcon,
} from "@animate-ui/react/primitives/buttons/copy"

import { Button } from "@/components/ui/button"

const codeBlockVariants = cva(
  "group/code-block relative flex w-full min-w-0 flex-col overflow-hidden rounded-2xl text-sm has-[>[data-slot=code-block-content]:focus-visible]:ring-3 has-[>[data-slot=code-block-content]:focus-visible]:ring-ring/30 data-[size=sm]:rounded-xl data-[size=sm]:text-xs",
  {
    variants: {
      variant: {
        default:
          "bg-muted text-foreground ring-1 ring-foreground/5 dark:ring-foreground/10",
        destructive:
          "bg-destructive/5 text-destructive ring-1 ring-destructive/15 dark:bg-destructive/10 dark:ring-destructive/20",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

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 shrink-0 items-center gap-2 px-4 pt-4 text-muted-foreground group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:pt-3 group-data-[variant=destructive]/code-block:text-destructive *:has-[>[data-slot=code-block-title]]:min-w-0",
        className
      )}
      {...props}
    />
  )
}

function CodeBlockTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="code-block-title"
      className={cn(
        "min-w-0 truncate font-sans text-sm font-medium text-foreground group-data-[size=sm]/code-block:text-xs group-data-[variant=destructive]/code-block:text-destructive [&_svg]:pointer-events-none [&_svg]:inline-block [&_svg]:align-middle [&_svg:not([class*='size-'])]:size-4 group-data-[size=sm]/code-block:[&_svg:not([class*='size-'])]:size-3.5 *:[svg]:me-1.5",
        className
      )}
      {...props}
    />
  )
}

function CodeBlockActions({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="code-block-actions"
      className={cn(
        "-my-1 ms-auto -me-1.5 flex shrink-0 items-center gap-1 *:data-[slot=button]:hover:bg-foreground/5 dark:*:data-[slot=button]:hover:bg-foreground/10 [[data-slot=code-block]>&]:absolute [[data-slot=code-block]>&]:end-3 [[data-slot=code-block]>&]:top-3 [[data-slot=code-block]>&]:z-10 [[data-slot=code-block]>&]:m-0 [[data-slot=code-block]>&]:opacity-0 [[data-slot=code-block]>&]:transition-opacity [[data-slot=code-block]>&]:group-focus-within/code-block:opacity-100 [[data-slot=code-block]>&]:group-hover/code-block:opacity-100 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:end-2 group-data-[size=sm]/code-block:[[data-slot=code-block]>&]:top-2 [[data-slot=code-block]>&]:pointer-coarse:opacity-100 [[data-slot=code-block]>&]:*:[button]:bg-background [[data-slot=code-block]>&]:*:[button]:ring-1 [[data-slot=code-block]>&]:*:[button]:ring-foreground/10",
        className
      )}
      {...props}
    />
  )
}

function CodeBlockCopyButton({
  value,
  variant = "ghost",
  size = "icon-xs",
  className,
  ...props
}: React.ComponentProps<typeof Button> & { value: string }) {
  const [copied, setCopied] = React.useState(false)

  return (
    <>
      <CopyButton
        asChild
        data-slot="code-block-copy-button"
        content={value}
        delay={2000}
        onCopiedChange={setCopied}
      >
        <Button
          data-copied={copied || undefined}
          variant={variant}
          size={size}
          aria-label={copied ? "Copied" : "Copy"}
          className={cn(
            "hover:bg-foreground/5 dark:hover:bg-foreground/10",
            className
          )}
          {...props}
        >
          <CopyButtonIcon
            icon={<HugeiconsIcon icon={Copy01Icon} strokeWidth={2} />}
            copiedIcon={<HugeiconsIcon icon={Tick02Icon} strokeWidth={2} />}
          />
        </Button>
      </CopyButton>
      <span aria-live="polite" className="sr-only">
        {copied ? "Copied" : ""}
      </span>
    </>
  )
}

function CodeBlockContent({
  className,
  children,
  style,
  wrap = false,
  lineNumbers = false,
  startLine = 1,
  ...props
}: React.ComponentProps<typeof CodeBlockPrimitive.Content> & {
  wrap?: boolean
  lineNumbers?: boolean
  startLine?: number
}) {
  return (
    <CodeBlockPrimitive.Content
      data-slot="code-block-content"
      data-wrap={wrap || undefined}
      data-numbered={lineNumbers || undefined}
      dir="ltr"
      tabIndex={0}
      style={{ counterReset: `line ${startLine - 1}`, ...style }}
      className={cn(
        "min-h-0 min-w-0 scroll-fade-x scrollbar-thin overflow-auto px-4 py-4 text-start font-mono leading-relaxed outline-none group-has-data-[slot=code-block-footer]/code-block:pb-2 group-has-data-[slot=code-block-header]/code-block:pt-2 group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:py-3 group-data-[size=sm]/code-block:group-has-data-[slot=code-block-header]/code-block:pt-1.5 data-wrap:scroll-fade-y data-wrap:wrap-break-word data-wrap:whitespace-pre-wrap",
        className
      )}
      {...props}
    >
      <code
        data-slot="code-block-code"
        className="grid min-h-lh min-w-max content-start in-data-numbered:grid-cols-[auto_1fr] in-data-wrap:min-w-0"
      >
        {lineNumbers && typeof children === "string"
          ? children
              .replace(/\n$/, "")
              .split("\n")
              .map((line, index) => (
                <CodeBlockLine key={index}>{line}</CodeBlockLine>
              ))
          : children}
      </code>
    </CodeBlockPrimitive.Content>
  )
}

const codeBlockLineVariants = cva(
  "relative -mx-4 block min-h-lh px-4 group-data-[size=sm]/code-block:-mx-3 group-data-[size=sm]/code-block:px-3 before:min-w-6 before:text-end before:text-muted-foreground after:absolute after:start-1 after:top-0 after:text-muted-foreground after:select-none in-data-numbered:col-span-2 in-data-numbered:grid in-data-numbered:grid-cols-subgrid in-data-numbered:gap-x-3 in-data-numbered:[counter-increment:line] in-data-numbered:before:content-[counter(line)] in-data-numbered:before:select-none in-data-numbered:supports-[content:counter(line)_/_'']:before:content-[counter(line)_/_'']",
  {
    variants: {
      variant: {
        default: "",
        added: "bg-foreground/5 after:content-['+']",
        removed: "bg-destructive/10 after:text-destructive after:content-['−']",
        highlighted: "bg-foreground/5",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function CodeBlockLine({
  className,
  variant = "default",
  children,
  ...props
}: React.ComponentProps<"span"> & VariantProps<typeof codeBlockLineVariants>) {
  // An empty line holds a newline so copying a selection keeps blank lines.
  const empty =
    children === undefined ||
    children === null ||
    children === "" ||
    (Array.isArray(children) && children.length === 0)

  return (
    <span
      data-slot="code-block-line"
      data-variant={variant}
      className={cn(codeBlockLineVariants({ variant }), className)}
      {...props}
    >
      <span data-slot="code-block-line-content" className="min-w-0">
        {empty ? "\n" : children}
      </span>
    </span>
  )
}

function CodeBlockFooter({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="code-block-footer"
      className={cn(
        "flex min-w-0 shrink-0 items-center gap-2 px-4 pb-4 text-muted-foreground group-data-[size=sm]/code-block:px-3 group-data-[size=sm]/code-block:pb-3",
        className
      )}
      {...props}
    />
  )
}

export {
  CodeBlock,
  CodeBlockHeader,
  CodeBlockTitle,
  CodeBlockActions,
  CodeBlockCopyButton,
  CodeBlockContent,
  CodeBlockLine,
  CodeBlockFooter,
}

import {
  Download01Icon,
  FileCodeIcon,
  TextWrapIcon,
} from "@hugeicons/core-free-icons"
import { useCodeWriting } from "@animate-ui/react/primitives/code-block"

import { useHighlightedCode } from "@/hooks/use-highlighted-code"

const code = `import { streamText } from "ai"

export async function POST(request: Request) {
  const { messages } = await request.json()

  const result = streamText({
    model: "anthropic/claude-sonnet-4.5",
    system: "You are a helpful assistant. Answer in one short paragraph.",
    messages,
  })

  return result.toUIMessageStreamResponse()
}`

// The block keeps its full height while the code is written.
const LINES = code.split("\n").length

function download(value: string, filename: string) {
  const url = URL.createObjectURL(new Blob([value], { type: "text/plain" }))
  const link = document.createElement("a")
  link.href = url
  link.download = filename
  link.click()
  URL.revokeObjectURL(url)
}

export default function CodeBlockDemo({
  delay = 0,
  duration = 6000,
  writing = true,
  cursor = true,
  cadence = "even",
}: {
  delay?: number | string
  duration?: number | string
  writing?: boolean | string
  cursor?: boolean | string
  cadence?: "natural" | "even"
}) {
  const [wrap, setWrap] = React.useState(false)
  const written = useCodeWriting<HTMLDivElement>(code, {
    writing: String(writing) === "true",
    delay: Number(delay),
    duration: Number(duration),
    cadence,
    inView: true,
  })
  const lines = useHighlightedCode(written.code, "ts")

  return (
    <div className="w-full max-w-lg">
      <CodeBlock ref={written.ref}>
        <CodeBlockHeader>
          <CodeBlockTitle>
            <HugeiconsIcon icon={FileCodeIcon} strokeWidth={2} />
            app/api/chat/route.ts
          </CodeBlockTitle>
          <CodeBlockActions>
            <Button
              variant="ghost"
              size="icon-xs"
              aria-label="Wrap lines"
              aria-pressed={wrap}
              className="aria-pressed:text-foreground"
              onClick={() => setWrap(!wrap)}
            >
              <HugeiconsIcon icon={TextWrapIcon} strokeWidth={2} />
            </Button>
            <Button
              variant="ghost"
              size="icon-xs"
              aria-label="Download"
              onClick={() => download(code, "route.ts")}
            >
              <HugeiconsIcon icon={Download01Icon} strokeWidth={2} />
            </Button>
            <CodeBlockCopyButton value={code} />
          </CodeBlockActions>
        </CodeBlockHeader>
        <CodeBlockContent
          wrap={wrap}
          lineNumbers
          writing={written.writing}
          cursor={String(cursor) === "true"}
          style={{ minHeight: `calc(${LINES}lh + 1.5rem)` }}
        >
          {lines.map((line, index) => (
            <CodeBlockLine
              key={index}
              variant={index >= 5 && index <= 9 ? "highlighted" : "default"}
            >
              {line}
            </CodeBlockLine>
          ))}
        </CodeBlockContent>
      </CodeBlock>
    </div>
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add code-block
pnpm dlx shadcn@latest add code-block
yarn dlx shadcn@latest add code-block
bun x shadcn@latest add code-block

Animate it

The CLI finds your shadcn/ui components through components.json and makes the edits the Manual tab lists for you.

npx @animate-ui/cli@latest animate code-block
pnpm dlx @animate-ui/cli@latest animate code-block
yarn dlx @animate-ui/cli@latest animate code-block
bun x @animate-ui/cli@latest animate code-block

shadcn/ui's Code Block isn't built on a primitive library: animate-ui animate code-block rewrites it in place instead. Its pre becomes the code-block primitive's Content, and its copy button is built on the Copy Button, keeping your icons. A part you changed is left as it is, and the CLI points here: the Manual tab lists the edits.

Usage

import { useCodeWriting } from '@animate-ui/react/primitives/code-block';

import { useHighlightedCode } from '@/hooks/use-highlighted-code';
import {
  CodeBlock,
  CodeBlockActions,
  CodeBlockContent,
  CodeBlockCopyButton,
  CodeBlockHeader,
  CodeBlockLine,
  CodeBlockTitle,
} from '@/components/ui/code-block';

export function Snippet({ code }: { code: string }) {
  const written = useCodeWriting<HTMLDivElement>(code, {
    duration: 4000,
    inView: true,
  });
  const lines = useHighlightedCode(written.code, 'ts');

  return (
    <CodeBlock ref={written.ref}>
      <CodeBlockHeader>
        <CodeBlockTitle>route.ts</CodeBlockTitle>
        <CodeBlockActions>
          <CodeBlockCopyButton value={code} />
        </CodeBlockActions>
      </CodeBlockHeader>
      <CodeBlockContent writing={written.writing} lineNumbers>
        {lines.map((line, index) => (
          <CodeBlockLine key={index}>{line}</CodeBlockLine>
        ))}
      </CodeBlockContent>
    </CodeBlock>
  );
}

Everything else is shadcn/ui's: the composition, plain string content, line numbers, wrapping, diff and highlighted lines, the small size and the destructive variant. Without useCodeWriting, the block shows its code at once, like shadcn/ui's.

useCodeWriting types the code out and returns what is written so far: highlight that, and give writing to CodeBlockContent for the cursor and the scroll. Its ref goes on the element whose entering the viewport starts the writing (with inView). The copy button copies the whole value, even while the code is being written.

Animation

  • With useCodeWriting, the code appears one character at a time, evenly over duration (after delay). With cadence: 'natural', it types like someone typing instead: quicker inside a word, pausing after punctuation and at the end of a line, the next line's indentation coming at once. Each new line is highlighted as it comes: useHighlightedCode tokenizes only the lines that are new.
  • While it writes, CodeBlockContent shows a cursor (|) at the end of the last line (cursor={false} hides it) and scrolls smoothly to follow the last line.
  • The last characters written fade in behind the cursor, on CodeBlockLines (plain string content gets the cursor only): transparent right behind it, opaque again 4 characters back. When the writing pauses (450ms without a new character) or moves to a new line, the fade slides off the end in 250ms (ease-out cubic-bezier(0.23, 1, 0.32, 1)), and the cursor pulses until it goes on.
  • With inView, writing starts when the block scrolls into view.
  • The copy button's icon shrinks into a blur as the check grows out of it when the code is copied, and swaps back after 2 seconds (Copy Button); the button scales to 0.97 while pressed.
  • Wrapping on or off (wrap), the lines glide to their new place and the block to its new height, in 300ms with the same ease-out.
  • Under reduced motion, the whole code is shown at once (onDone is still called), so there is no cursor or fade; wrapping changes at once; the copy icons crossfade through their blur without scaling, and the button doesn't scale.

API Reference

Every part takes shadcn/ui's props. Animate UI adds, through CodeBlockPrimitive.Content:

CodeBlockContent

PropTypeDefault
writing?
booleanfalse
cursor?
booleantrue

useCodeWriting

useCodeWriting(code, options, ref?) returns { ref, code, writing, done, isInView }: code is what is written so far, writing is true until the whole code is shown.

PropTypeDefault
writing?
booleantrue
duration?
number5000
delay?
number0
cadence?
'even' | 'natural''even'
onDone?
() => void-
onWrite?
(info: { index: number; length: number; done: boolean }) => void-
inView?
booleanfalse
inViewMargin?
string"0px"
inViewOnce?
booleantrue

Primitive

@animate-ui/react/primitives/code-block also exports the CodeBlock primitive: one element that highlights a whole code with Shiki's codeToHtml (a peer dependency, loaded on demand) and types it out.

import { CodeBlock } from '@animate-ui/react/primitives/code-block';

<CodeBlock code="console.log('Hello, world!');" lang="javascript" writing />;

CodeBlock (primitive)

PropTypeDefault
code
string-
lang
string-
theme?
"light" | "dark""light"
themes?
{ light: string; dark: string }{ light: "github-light", dark: "github-dark" }
writing?
booleanfalse
duration?
number5000
delay?
number0
onDone?
() => void-
onWrite?
(info: { index: number; length: number; done: boolean }) => void-
scrollContainerRef?
React.RefObject<HTMLElement | null>-
inView?
booleanfalse
inViewMargin?
string"0px"
inViewOnce?
booleantrue
...props?
React.ComponentProps<'div'>-

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/27/2026