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.
Installation
Install the packages
Add the shadcn/ui component
Animate it
The CLI finds your shadcn/ui components through components.json and makes the edits the Manual tab lists for you.
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 overduration(afterdelay). Withcadence: '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:useHighlightedCodetokenizes only the lines that are new. - While it writes,
CodeBlockContentshows 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-outcubic-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 (
onDoneis 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
| Prop | Type | Default |
|---|---|---|
writing? | boolean | false |
cursor? | boolean | true |
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.
| Prop | Type | Default |
|---|---|---|
writing? | boolean | true |
duration? | number | 5000 |
delay? | number | 0 |
cadence? | 'even' | 'natural' | 'even' |
onDone? | () => void | - |
onWrite? | (info: { index: number; length: number; done: boolean }) => void | - |
inView? | boolean | false |
inViewMargin? | string | "0px" |
inViewOnce? | boolean | true |
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)
| Prop | Type | Default |
|---|---|---|
code | string | - |
lang | string | - |
theme? | "light" | "dark" | "light" |
themes? | { light: string; dark: string } | { light: "github-light", dark: "github-dark" } |
writing? | boolean | false |
duration? | number | 5000 |
delay? | number | 0 |
onDone? | () => void | - |
onWrite? | (info: { index: number; length: number; done: boolean }) => void | - |
scrollContainerRef? | React.RefObject<HTMLElement | null> | - |
inView? | boolean | false |
inViewMargin? | string | "0px" |
inViewOnce? | boolean | true |
...props? | React.ComponentProps<'div'> | - |