snapcn
23 components
DocsComponentsTemplatesVideo EditorShowcaseMarketplaceRoadmapChangelog

Getting started

IntroductionInstallationAgent skill
GitHub

Star snapcn

Free, open-source components.

Production-ready Remotion animations, transitions and backgrounds — you own the code.

Star on GitHubXFollow

MIT licensed · own your code.

© 2026 snapcn

DocsComponentsTemplatesVideo EditorShowcaseMarketplaceRoadmapChangelog
Getting startedIntroductionInstallationAgent skill

Word Flip

A headline types itself out, then one word cycles on a 3D flip — sinking into an anticipation dip, throwing up and away about its baseline under motion blur, while the next word unfurls from below.

Installation

pnpm
$ pnpm dlx shadcn@latest add @snapcn/word-flip

Component code

The exact file shadcn add copies into your project.

Usage

import { WordFlip } from "@/components/snap-cn/word-flip";

export function Hero() {
  return (
    <WordFlip
      prefix="Looking For A"
      words={["Modern", "Stunning", "Minimal"]}
      suffix="Portfolio"
    />
  );
}

How it moves

The sentence types itself a character at a time, holds for a beat, and then the word in the middle starts cycling. Four things happen in the nine frames of a flip, and they are all one gesture.

The slot is reserved before the first word exists

The whole point of this animation is that nothing except the word ever moves — no horizontal shift, no baseline shift, no reflow. That cannot be achieved by swapping text in and out of a line, because the words have different widths and the line would breathe every time one arrived.

So the slot is an inline-block whose width is the widest word's, fixed from the first frame. While the sentence is still typing, the slot is simply empty — the gap you see between "A" and "Portfolio" is the word's seat, waiting for it. Every word is then uniformly scaled to fill that seat, which is why the widest word sits at scale 1 and the rest are scaled up to meet it, and why both edges of the slot are pinned no matter which word is showing.

Measured across the whole clip: the left edge of "Portfolio" moves 1px, and that 1px is the antialiasing on its own fade-in.

The word sinks before it flies

The flip does not start by lifting the word. It starts by dropping it about a tenth of an em — a backswing, the same thing a person does with their arm before they throw. It is what makes the flip read as weight instead of as a cut.

That is not a separate keyframe. One easeInBack progress drives the translate and the rotation, and the curve's own overshoot is the dip: its minimum is p = -0.100 at t = 0.42, so the dip is worth exactly a tenth of the exit travel and lands a little under halfway through it.

The blur is a shutter, not a ramp

Blur is proportional to speed — easeInBackSpeed is the analytic derivative of the curve above. It matters because the derivative is zero at the turnaround: at the bottom of the dip the word has stopped to change direction, and it is sharp there. Three frames later it is travelling as fast as it ever will, and it is smeared. A blur keyed to progress instead of speed would blur the word while it is standing still, which reads as a focus pull rather than motion.

The two words overlap

The incoming word starts before the outgoing one is gone (overlap, in frames), so the swap is continuous rather than sequential. It arrives rotated -90° about its baseline, a touch below where it belongs and a touch small, and unfurls into place on a moderate decelerate — never an expo-out, which on a frame clock spends most of its frames moving less than a pixel and reads as the word stopping dead partway up and waiting.

Everything pivots on the baseline, and the baseline is measured, never derived from a line-height ratio. Pivot anywhere else and the glyph origins climb the pixel grid in whole-pixel jumps as the scale moves them — the word sits still, jumps, sits still.

Props

PropTypeDefaultDescription
prefixstring"Looking For A"Text before the flipping word.
wordsstring[]["Modern", "Stunning", "Minimal"]The words that cycle through the slot.
suffixstring"Portfolio"Text after the flipping word.
gradientstring[]["#2A8BF5", "#D382B0"]Gradient painted on the flipping word, left to right.
cpsnumber9Typing speed, characters per second.
typeStartnumber4Frames before the first keystroke.
charFadenumber6Frames each character takes to fade in.
jitternumber0.18Keystroke unevenness, as a fraction of one keystroke. 0 is a metronome.
pausenumber6Frames held after the sentence completes, before the first flip. 6 frames is the 200ms beat.
cyclenumber35Frames from one flip to the next.
exitDurationnumber9Frames the outgoing word takes to leave.
enterDurationnumber9Frames the incoming word takes to settle.
overlapnumber3Frames the incoming word starts before the outgoing one is gone.
caretbooleantrueBlinking caret while typing.
loopbooleantrueKeep cycling the words forever.
motionPartial<WordFlipMotion>see belowDistances, in em.
perspectivenumber6.53D depth, in em. Smaller is a stronger perspective.
fontFamilystringapp sans stackThe typeface.
fontSizenumber72
colorstringtheme.foregroundThe static part of the sentence.
themePartial<SnapCnTheme>—Design-system token overrides.
mode"light" | "dark""light"Which end of the system to resolve against.
fontWeightnumber600
speednumber1

motion

Every distance is in em, so the gesture scales with the type instead of being tied to one font size.

KeyDefaultDescription
exitY-1.29How far the outgoing word is thrown. Negative is up. Its first tenth, inverted, is the dip.
enterY0.135How far below rest the incoming word starts.
rotate90Degrees about the baseline.
scale0.98What the outgoing word shrinks to, and the incoming grows from.
blur0.085Peak blur, reached at peak speed.

Notes

  • The sentence is one line and never wraps. fontSize is the knob: at the default 72 the reference sentence fills about 83% of a 1280-wide composition.
  • caret has no effect on layout — it is a zero-width box with an absolutely positioned bar inside, so the sentence's geometry is identical with it and without it.
  • The words are measured once, behind delayRender(), so frame 0 is never captured against the wrong slot width.