GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Text Animation Effects, One Line, Copy-Paste

These nine css text animation effects move a heading into view, into color, or into a short wiggle using only transform, opacity, and filter — no JavaScript,

Auto-plays · click a tile to jump to its section · all nine in one zip

The order below follows what the text itself goes through rather than a popularity ranking. Items 01 through 05 are first-appearance treatments — a mask lift, a letter bounce, a rotate-in, a 3D flip, and a blur-to-focus. Item 06 is built to keep looping once a heading is already on screen, items 07 and 08 pull a second look toward text that never left, and item 09 closes the set by making a phrase look like it briefly broke. Every demo below sits inside a small, non-scrollable iframe and already loops on a fixed cycle, since none of these nine wait on a pointer. Watch the grid above for a couple of seconds and each one has already played through. For sentences that type themselves out instead, that's the CSS typing effects set, and for the background doing the moving while the words hold still, the CSS background animations carry that half.

01Mask reveal from below

How it works

The heading sits inside a box clipped with overflow: hidden, and only the text inside slides up from translateY(115%) to its resting position on a repeating cycle. Because the box itself never moves, nothing else on the page shifts while the reveal happens — each letter inside that text is now split into its own span at load time and sharpens out of a blur as it rises, staggered by its --i index and a per-letter --n noise value so the letters don't all snap into focus together.

Where it fits

It reads best as a hero headline's first entrance or a section title that reveals itself once scrolled into view.

Mask reveal from below
Open in a new tab

01 · Mask reveal from below — Measured area 5.417% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

overflow:hiddentranslateY 마스크0.5초
.mask {
  overflow: hidden;
  max-width: min(380px, 90vw);
}

.mask__text {
  color: $color;
  animation: mask-up $duration $easing infinite;
}

@keyframes mask-up {
  0%, 4%    { transform: translateY(115%); }
  30%, 78%  { transform: translateY(0); }
  96%, 100% { transform: translateY(115%); }
}

02Letter-by-letter bounce

How it works

Each letter of the heading lives in its own inline span, and a single --i custom property multiplied by 40ms inside animation-delay staggers every letter's identical bounce keyframes. Because the delay math lives on the letters themselves rather than a parent element, adding or removing letters never touches the keyframes — a second keyframe layered onto that same span now sharpens each letter out of a blur while it bounces, offset by a --n noise value so the focus doesn't resolve at the exact instant every letter lands.

Where it fits

It suits a logo intro or a landing hero title where the letters should feel like they're arriving one at a time.

Letter-by-letter bounce
Open in a new tab

02 · Letter-by-letter bounce — Measured area 8.615% · moving frames 12/23Original by GODRICH · SCSS and React ship in the zip · See the password

--i 변수40ms 간격translateY 바운스
.letters span {
  display: inline-block;
  animation: letter-bounce $duration $easing infinite;
  animation-delay: calc(var(--i) * 40ms);
}

@keyframes letter-bounce {
  0%, 4%    { opacity: 0; transform: translateY(22px); }
  22%, 70%  { opacity: 1; transform: translateY(0); }
  88%, 100% { opacity: 0; transform: translateY(-14px); }
}

03Rotate-in entrance

How it works

Every letter transitions rotate and opacity together, starting tilted at -25 degrees and fully transparent before settling flat at 0 degrees and full opacity, each one delayed 90ms further than the last through the same --i variable used in item 02. The tilt reads as a letter flying in slightly off-balance rather than sliding in a straight line — that same span also runs a blur-to-focus keyframe alongside the rotation, so each letter is still slightly soft when it starts tilting and only reaches full sharpness near the end of its swing.

Where it fits

Badge labels and short card titles are a natural fit, since the effect reads clearly even across just a handful of characters.

Rotate-in entrance
Open in a new tab

03 · Rotate-in entrance — Measured area 4.82% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform: rotateopacity-25deg→0
.rot span {
  animation: rotate-in $duration $easing infinite;
  animation-delay: calc(var(--i) * 90ms);
}

@keyframes rotate-in {
  0%, 4%    { opacity: 0; transform: rotate(-25deg); }
  26%, 72%  { opacity: 1; transform: rotate(0deg); }
  90%, 100% { opacity: 0; transform: rotate(18deg); }
}

043D letter flip

How it works

Inside a parent carrying perspective, each letter's own backface-visibility: hidden lets rotateX swing it from 90 degrees down to 0, so every character flips into place like a tiny card turning face-up. The perspective lives on the shared parent while the rotation and its --i delay live on each letter individually — each letter now sharpens out of a blur over that same flip, so a character reads as a soft smear mid-turn and only snaps into full focus once it's lying flat facing forward.

Where it fits

A price that just updated and a game-style title card both benefit from the flip reading as a discrete change rather than a fade.

3D letter flip
Open in a new tab

04 · 3D letter flip — Measured area 3.824% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

perspectiverotateX 90→0backface-visibility
.flip {
  perspective: 480px;
}
.flip span {
  backface-visibility: hidden;
  animation: flip-in $duration $easing infinite;
  animation-delay: calc(var(--i) * 70ms);
}

@keyframes flip-in {
  0%, 4%    { opacity: 0; transform: rotateX(90deg); }
  30%, 74%  { opacity: 1; transform: rotateX(0deg); }
  94%, 100% { opacity: 0; transform: rotateX(-90deg); }
}

05Blur to focus

How it works

filter: blur runs from 14px down to 0 while opacity climbs from 0 to 1 in the same keyframes block, so the heading looks like a lens racking into focus rather than simply fading in. Position never moves — only sharpness and visibility change, which keeps the layout completely still while the text resolves, and that same blur-focus keyframe now runs on each letter's own span instead of the whole phrase at once, so every character racks into focus at a slightly different moment set by its --i and --n values rather than the entire heading snapping sharp on one frame.

Where it fits

Modal titles and photo captions that need to feel deliberate, not instant, are where this one earns its place.

Blur to focus
Open in a new tab

05 · Blur to focus — Measured area 6.333% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

filter: bluropacity12px→0
.blur {
  animation: blur-focus $duration $easing infinite;
}

@keyframes blur-focus {
  0%, 4%    { opacity: 0; filter: blur(14px); }
  30%, 76%  { opacity: 1; filter: blur(0); }
  96%, 100% { opacity: 0; filter: blur(14px); }
}

06Wave text

Keeping a css text animation loop running

Every letter shares one wave-up keyframes block that moves translateY between 0 and -16px, and only the --i-based animation-delay tells each letter when to start its own copy of that identical motion. Because there's no entrance or exit built in, the line just keeps rippling for as long as it stays on screen — a light blur now rides along near the top of each letter's arc too, easing in and back out right at the crest of its bounce so the peak reads faintly like motion blur rather than a sharp bounce.

Where it fits

It's built for a loading hint or empty-state text that needs to feel alive while someone waits.

Wave text
Open in a new tab

06 · Wave text — Measured area 6.247% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

같은 keyframes지연차 파도translateY
.wave span {
  animation: wave-up $duration $easing infinite;
  animation-delay: calc(var(--i) * 90ms);
}

@keyframes wave-up {
  0%, 100% { transform: translateY(0); }
  50%      { transform: translateY(-16px); }
}

07Wiggle emphasis

How it works

The whole phrase sits dead still for the first 60% of the loop, then rocks between rotate(-2deg) and translateX(-2px) and their mirrored opposites for a handful of short steps before settling flat again. That long pause between wiggles is deliberate — it reads as an occasional nudge rather than a constant tremor, and before any of that wiggling starts each letter now runs a one-time blur-to-focus pass on entrance so the phrase resolves into sharpness first.

Where it fits

A hint next to a new badge or a discount label that needs a glance, not a stare, is the right home for it.

Wiggle emphasis
Open in a new tab

07 · Wiggle emphasis — Measured area 4.14% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

rotate 1~2degtranslateX 2px짧은 왕복
.wiggle {
  animation: wiggle $duration $easing infinite;
}

@keyframes wiggle {
  0%, 60%, 100% { transform: rotate(0) translateX(0); }
  66% { transform: rotate(-2deg) translateX(-2px); }
  72% { transform: rotate(2deg) translateX(2px); }
  78% { transform: rotate(-2deg) translateX(-2px); }
  84% { transform: rotate(2deg) translateX(2px); }
  90% { transform: rotate(0) translateX(0); }
}

08Two-tone sweep

How it works

The same phrase is stacked twice in flat, solid colors, and only the top copy's clip-path wipes from fully hidden to fully shown left-to-right, so the color underneath appears to get painted on rather than blended into it. There's no gradient anywhere in the mix — the edge between the two colors stays hard the entire time, and both stacked copies are now split into per-letter spans too, each one blurring in on entrance before the clip-path wipe reaches it without softening that hard edge.

Where it fits

Pricing highlights and campaign copy that need one sharp burst of color change use it well.

Two-tone sweep
Open in a new tab

08 · Two-tone sweep — Measured area 6.474% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-path 스윕두 겹 텍스트단색 경계
.sweep__top {
  position: absolute;
  inset: 0;
  animation: sweep-wipe $duration $easing infinite;
}

@keyframes sweep-wipe {
  0%, 4%    { clip-path: inset(0 100% 0 0); }
  40%, 62%  { clip-path: inset(0 0 0 0); }
  96%, 100% { clip-path: inset(0 100% 0 0); }
}

09Glitch

How it works

Two color-shifted copies of the same phrase sit in mix-blend-mode: screen, and each one gets clip-path sliced into thin horizontal bands that briefly offset with translateX before snapping back into alignment. The two copies run that offset twice per loop rather than once, which is what makes the break read as an interruption instead of a single stumble — each letter across both color layers also gets a short blur-in on entrance now, so the phrase arrives looking momentarily out of focus before the RGB split kicks in.

Where it fits

Error states and tech-brand logo moments are the two places where a screen genuinely looking broken for an instant actually helps.

Glitch
Open in a new tab

09 · Glitch — Measured area 6.18% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-path 슬라이스translateX 어긋남색 분리
.glitch__r, .glitch__b {
  position: absolute;
  inset: 0;
  mix-blend-mode: screen;
}

.glitch__r { animation: glitch-r $duration linear infinite; }

@keyframes glitch-r {
  0%   { transform: translateX(0);    clip-path: inset(0 0 0 0); }
  8%   { transform: translateX(-6px); clip-path: inset(12% 0 58% 0); }
  16%  { transform: translateX(4px);  clip-path: inset(48% 0 14% 0); }
  24%  { transform: translateX(0);    clip-path: inset(0 0 0 0); }
  50%  { transform: translateX(-4px); clip-path: inset(20% 0 40% 0); }
  58%  { transform: translateX(5px);  clip-path: inset(55% 0 8% 0); }
  66%  { transform: translateX(0);    clip-path: inset(0 0 0 0); }
}

Where it breaks — the trap

All nine — every item here now that each one splits its text into per-letter spans — compute their stagger entirely through animation-delay on each letter's own span, driven by that span's own --i value alongside a --n noise value for the blur layer. It's tempting to also drop a plain opacity transition onto the shared parent, the way most css text transition effects are wired, expecting the fade to stagger along with them the same way. Instead every letter changes at once, because a transition sitting on the parent has no --i of its own to read — the stagger only ever exists on whichever element's own animation-delay carries it.

Why glitch needs a dark stage

Item 09's glitch leans on mix-blend-mode: screen to split its two color layers apart, and screen's own blending math lightens toward white wherever the backdrop underneath is already bright. That's exactly why 09 only ships on a dark stage here — drop the same two layers onto a light background and screen washes the color split out to almost nothing. Swapping in mix-blend-mode: difference, or giving each layer a flat, semi-opaque fill instead of screen, is what keeps the glitch visible on a lighter surface.

What the frame counts showed

Measuring all nine after render backs that emphasis up unevenly: item 06's wave kept moving across 23 of its 24 captured frames, while item 07's wiggle moved in 15 of them — higher than the wiggle alone would produce, because the new per-letter blur-in pass changes pixels across additional early frames even though the wiggle itself still spends most of its loop sitting still on purpose. The nine files here already carry that difference tuned in — grab them from the archive, unlocked with 97me82wy entered exactly as it's spelled right in this sentence, nothing added and nothing left out. Carrying the layered-word aesthetic all the way into overflowing sections is what the maximalist collage landing kit does with it.

Accessibility

What each one rests on

All nine turn off their looping keyframes under prefers-reduced-motion: reduce, but each settles on a different resting frame rather than one shared rule. 01's mask and 05's blur snap straight to their fully revealed, fully sharp end state — transform: none and filter: none put the heading exactly where the animation would have left it anyway. 02, 03, and 04 do the same per letter: animation: none paired with opacity: 1 and transform: none puts every span in its resting position at once, with none of the stagger left to see. 06's wave drops to animation: none and transform: none alone — wave never touched opacity to begin with, so there's nothing to restore. 07's wiggle turns off completely rather than freezing on a rotated frame, since it exists purely to catch a glance and carries no information a static heading would lose.

When the outcome must stay

08 is the one built to keep its outcome legible. Its clip-path locks to inset(0 0 0 0) so the swept-in color shows in full rather than freezing mid-wipe, because here the color itself is the point. 09 sets both glitch layers to opacity: 0, leaving only the plain base text on screen with no split-color layers left to confuse anyone. MDN's write-up on the prefers-reduced-motion media feature covers the operating-system settings behind that toggle.

The rest of this site's animation notes live in the CSS category hub, and what actually ships inside the archive — and what doesn't — is spelled out on the about page.

FAQ

Do these css text animation effects still work if the heading's text length changes?

Yes, all nine now. Every item carries a data-split attribute, and a small script that runs once at page load splits its text into per-letter spans, sizing itself to however many characters are actually there and assigning each span its own --i index and --n noise value as it goes — nothing about the span count is fixed ahead of time or hand-written into the markup. A longer or shorter phrase, including this English copy running against a different letter count than the original, just works without anyone touching a single span by hand. The animation itself still runs entirely on CSS keyframes and animation-delay reading those custom properties; the script's only job is generating the spans and their --i/--n values, not driving any of the motion. When the thing under the pointer is a picture rather than a phrase, the CSS image hover effects fill that side of the same slot.

Will these work as tailwind css text animation effects, or do they need SCSS?

The demos compile from SCSS down to plain CSS classes ahead of time, so the shipped rules don't depend on a build tool at all — they sit fine next to Tailwind's own utility classes on the same element. All nine are built from keyframes, so each one still needs a small custom animation defined outside Tailwind's default utility set, since staggered per-letter delays aren't something its utilities generate by themselves.

Is there a css text animation generator inside the zip, or just the nine files?

Just the nine — the archive hands over each effect's SCSS, its compiled CSS, and a React version, rather than a tool for dialing up new variants on demand. Changing the pace or the per-letter delay means opening one file's SCSS variables directly, mainly $duration and $stagger, instead of regenerating the animation from a form. The gap between letters itself is a plain clamp() value inline in the same rule, not a named variable, so that one gets edited directly.

Enter the archive password

The password is inside this article. You will find it as you read.