A hero section led by an arc of tinted glass discs, solved from a count and a spread, that assembles on mount and then answers only the pointer.
Carbon Copy
A headline that is set, then copied: the first line inks up, and each line after it peels off the last with the changed words turning over.
CarbonCopy is the sentence a product opens with, made with the type itself. The first line is set as a hairline and inked — the weight of the letters rises from the thinnest cut of the face to the one the line rests at, so the words are seen to fill rather than to appear. Then a copy is taken: the next line starts exactly on top of the first, identical to it, and travels down one line to its place, lifting a little on the way and seating with a spring. While it travels, the words that differ turn over in windows — the old word rolls down and out, the new one drops in from above, the way a counter turns — so the copy lands already reading as the next line, and a third line is a copy of the second. It is built for the headline whose second line is a revision of its first: copy, keep; build fast, build right. The movement is the meaning, which is what keeps it from being an effect. Every window is measured from the rendered glyphs, so the copy lies on the original to the pixel in any face at any size, and the finished line is set the way it would have been set on its own. It plays once, when it is scrolled into view or when it mounts, and hands you a callback on its last seat so the rest of the section can be cued from it rather than from a timer.
Preview
The first line inks up from a hairline. Then a copy is taken: the second line peels off the first, travels down a line and seats with a spring, and on the way its first word turns over in a window — the old word rolls out below, the new one drops in from above. It plays when it comes into view.
Copy the code.Keep the code.
Each line is a copy of the one before, so a third line is taken from the second once it has seated. Only the words that differ turn over; the rest of the line comes down unchanged.
Build it fast.Build it right.Build it once.
ink={false}, alignWithout the ink the first line is simply there, and the copy is the whole event. Set from the left edge, at body size. These two lines have different numbers of words, so the copy turns over as a whole rather than word by word.
Deploys in seconds.Rolls back in one click.
trigger="mount", timing, onCompletePlayed the moment it mounts, on a slower clock, and what comes after the headline is cued from its last seat rather than from a timer of its own — the sentence and the call to action are set down only once the copy has landed.
Ship on Friday.Sleep on Friday.
Installation
Install Carbon Copy with the shadcn CLI. The registry item resolves its own dependencies and writes the file to components/joinui/carbon-copy.tsx.
pnpm dlx shadcn@latest add @joinui/carbon-copyUsage
import { CarbonCopy } from "@/components/joinui/carbon-copy"
export function Example() {
return (
<CarbonCopy
lines={["Copy the code.", "Keep the code."]}
className="text-[clamp(2.5rem,6vw,5rem)] leading-none tracking-[-0.042em]"
/>
)
}Props
CarbonCopy
| Prop | Type | Default | Description |
|---|---|---|---|
| lines (required) | readonly string[] | — | The lines, in the order they are made. The first is set; each one after it is a copy of the one before, with the words that differ turned over. Lines with the same number of words turn over word by word; lines with different counts turn over as a whole. |
| as | "h1" | "h2" | "h3" | "h4" | "p" | "div" | "h1" | The element the lines are set in. |
| align | "start" | "center" | "end" | "center" | Which edge the lines hang from. A copy lies on its original whichever it is. |
| ink | boolean | true | Whether the first line inks up from a hairline. Off, the first line is simply there and the copy is the whole event — the right choice on a static face, which can only step between the weights it has. |
| inkFrom | number | 100 | The weight the ink starts from. |
| weight | number | 600 | The weight the lines rest at, and the ink ends at. Set as the element's font-weight, so a className need not repeat it. |
| trigger | "view" | "mount" | "view" | Play when three fifths of the element has been scrolled into view, or as soon as it mounts. Either way it plays once. |
| timing | Partial<CarbonCopyTiming> | — | Overrides for the tempo, in milliseconds. Anything you leave out keeps its default. |
| onComplete | () => void | — | Fires once the last copy has seated — or at once, under reduced motion — so what follows the headline can be cued from it. |
| className | string | — | Merged onto the element through `cn`. Size, leading and tracking are yours to set; the component sets only the weight. |
CarbonCopyTiming
Shape of the `timing` prop. All values are milliseconds.
| Prop | Type | Default | Description |
|---|---|---|---|
| delay | number | 0 | Before anything starts. |
| ink | number | 1100 | How long the first line takes to ink up from a hairline. |
| copy | number | 800 | How long a copy takes to travel down one line and seat. |
| roll | number | 550 | How long a word takes to turn over in its window. |
| pause | number | 150 | The beat between one movement ending and the next beginning. |
Dependencies
npm packages
- motion
Registry items
- utils
Installed automatically by the CLI when they are missing.
Accessibility
- The lines are rendered as plain text inside the element you ask for — `as` — so the headline is a real heading at the level the page needs, and a second instance on the same page can be an `h2`.
- What is read is what is finally seen. The outgoing word in each window is `aria-hidden`, so assistive technology gets the finished line and never the one it was copied from.
- Nothing is hidden from the accessibility tree while it moves: a copy waiting for its turn is transparent, not `display: none`, so a reader who arrives mid-animation still finds every line.
- The window's edges are a mask, not an overflow clip, and they come off once the word has turned, so a finished line is unmasked text that reflows, selects and zooms like any other.
- It plays once and stops. Nothing loops, nothing responds to the pointer, and there is no timer left running after the last copy has seated.
- `prefers-reduced-motion` is honoured structurally: no line moves, nothing is measured, the outgoing words are not rendered at all, and `onComplete` fires at once so whatever is cued from it still appears.
- The ink is a weight, not a colour, so the contrast of the finished line is the contrast of your text and is never reduced by the entrance.
- Both themes are covered by the page's own text colour rather than by anything the component sets, so it keeps its contrast inside a forced-theme subtree.
Keyboard interactions
Carbon Copy renders no interactive controls of its own, so it adds nothing to the tab order.
Customization
Take more than one copy
Every line after the first is a copy of the one before it, taken a beat after that one has seated. Only the words that differ turn over, so keep the lines the same shape and let one word carry the change — three revisions of a sentence read as one thought sharpening, where three different sentences read as a slideshow.
<CarbonCopy
as="h2"
lines={["Build it fast.", "Build it right.", "Build it once."]}
className="text-5xl leading-none tracking-tight"
/>Skip the ink
The ink is drawn from the weights between the ones a face ships, which only a variable font has. On a static face the browser steps between the cuts it does have, and a headline that snaps from thin to bold is not what the ink means. Turn it off and the first line is simply there; the copy is the whole event, which at body size is often the better proportion anyway.
<CarbonCopy
as="p"
align="start"
ink={false}
weight={500}
lines={["Deploys in seconds.", "Rolls back in one click."]}
className="text-xl leading-tight text-muted-foreground"
/>Set the tempo
Five numbers own the clock, and the movement is written against them rather than in fixed durations, so a slower copy is still the same copy. A hero can afford a longer ink and a heavier seat; a line inside a card wants the defaults or quicker.
<CarbonCopy
lines={["Ship on Friday.", "Sleep on Friday."]}
timing={{ delay: 400, ink: 1400, copy: 950, roll: 650, pause: 240 }}
/>Cue the rest of the section from it
A headline that performs should be the one thing performing. onComplete fires on the last seat — at once under reduced motion — so the sentence and the call to action under it can be set down after the copy has landed instead of racing it on a timer that has to be kept in step by hand.
const [settled, setSettled] = React.useState(false)
<CarbonCopy
trigger="mount"
lines={["Copy the code.", "Keep the code."]}
onComplete={() => setSettled(true)}
/>
<p
className={cn(
"transition-[opacity,translate] duration-700",
settled ? "translate-y-0 opacity-100" : "-translate-y-2 opacity-0"
)}
>
Animated React components that install into your repo and stay there.
</p>Play it again
It has no notion of playing twice, and it should not — a headline is set once. When a demo or a route change needs it to run again, remount it: a new key is a fresh instance, measured and timed from nothing.
const [take, setTake] = React.useState(0)
<CarbonCopy key={take} lines={lines} />
<button onClick={() => setTake((n) => n + 1)}>Replay</button>Related components
A coil of pills that keeps one item in focus and lets the rest twist away, turned either by a timer or by the page scroll.