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

9 CSS Text Highlight Animations, Copy-Paste

A css text highlight animation calls attention to one word or line with a highlighter stroke, an underline, or a strikethrough, without touching the letters

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

These nine aren't ranked by popularity; they're ordered by when each one actually gets reached for. 01 through 03 paint a key phrase inside a block of text. 04 and 05 mark a warning or a finished state. 06 and 07 lead a reader's eye along a link or a step. 08 and 09 close things out by making a badge or a single number pop for a beat. Each of these css text animation examples keeps only the lines that matter; the complete nine files, plus a React version of each, sit in the zip. Moving the same emphasis into utility classes is what the Tailwind v4 animation utilities demonstrate, and when the motion has to report an error instead of praising a phrase, the shake animations sit on the opposite side of that idea.

01Highlight marker

What makes a css text highlight effect read as a marker

A yellow block sits behind the keyword and grows left to right with transform: scaleX(0→1), then mix-blend-mode: multiply lays that block over the letters so it reads as a highlighter stroke just laid down. A second noise layer of the same color zigs and zags across the top and bottom edges, so the stroke thickness wobbles unevenly from one end to the other like real pen pressure.

Highlight marker
Open in a new tab

01 · Highlight marker — Measured area 1.458% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform:scaleXmix-blend-mode2s

Where one word should land first

Reach for it on a key phrase inside terms or a notice, or on the one word a tutorial wants a reader's eye to land on first.

.hl {
  position: relative;
  isolation: isolate;
  &::before {
    content: ""; position: absolute; inset: 12% -2% 6%;
    background: $color; border-radius: $r-xs;
    transform: scaleX(0); transform-origin: left center;
    mix-blend-mode: multiply; z-index: -1;
    animation: mark $duration $easing infinite;
  }
}
@keyframes mark {
  0%   { transform: scaleX(0); }
  20%  { transform: scaleX(1); }
  75%  { transform: scaleX(1); }
  100% { transform: scaleX(0); }
}

02Marker overlap

Two passes, two delays

The same highlighter block appears twice, as ::before and ::after, each rotated a different couple of degrees and started on its own animation-delay so the two passes overlap unevenly. Each pass also wobbles its own top and bottom edges out of phase, which keeps the overlapped border alive instead of settling into a static shape.

Marker overlap
Open in a new tab

02 · Marker overlap — Measured area 1.945% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

::before·::afterrotate + scaleX지연 0/.3s

The single most important word

It suits the single most important word on a page — a product name, an FAQ's answer keyword — where two visible strokes carry more weight than one.

.hl::before, .hl::after {
  content: ""; position: absolute; inset: 10% -2% 4%;
  transform: scaleX(0); transform-origin: left center;
  mix-blend-mode: multiply; z-index: -1;
}
.hl::before { background: $stage-yellow; rotate: -1.2deg; animation: mark $duration $easing infinite; }
.hl::after  { background: $color; opacity: .55; rotate: 1deg; animation: mark $duration $easing infinite .3s; }

03Hand-drawn underline

Tracing an uneven curve from the left

An inline SVG path draws a slightly uneven curve under the word, and animating stroke-dashoffset from the path's own length down to zero traces that line in from the left exactly like a pen would. While it traces, stroke-width drifts between 3px and 4.5px so the line thickens and thins like hand pressure on a real pen.

Hand-drawn underline
Open in a new tab

03 · Hand-drawn underline — Measured area 0.301% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

SVG pathstroke-dashoffset600ms

A pull-quote or an about page

A pull-quote's key phrase or the emphasized word on an about page both benefit from the extra beat it takes to draw.

.hl__path {
  fill: none; stroke: $color; stroke-width: 3; stroke-linecap: round;
  stroke-dasharray: 140; stroke-dashoffset: 140;
  animation: draw $duration $easing infinite;
}
@keyframes draw {
  0%   { stroke-dashoffset: 140; }
  35%  { stroke-dashoffset: 0; }
  80%  { stroke-dashoffset: 0; }
  100% { stroke-dashoffset: -140; }
}

04Wavy underline

What @property adds to a native wave

Native text-decoration-line: underline and text-decoration-style: wavy draw the line itself, while a <length> value registered through @property animates text-underline-offset back and forth so the wave appears to breathe rather than sit still. Between those peaks the thickness also sways from 2px up toward 3px, leaving the whole line unevenly weighted like a brush stroke.

Wavy underline
Open in a new tab

04 · Wavy underline — Measured area 0.284% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

text-decoration-style:wavy@property <length>1s

Copy that keeps pulling the eye back

It reads naturally on a spell-check-style warning or any copy meant to keep pulling a reader's eye back.

@property --underline-offset {
  syntax: "<length>"; inherits: true; initial-value: 1px;
}
.hl {
  text-decoration-line: underline;
  text-decoration-style: wavy;
  text-decoration-color: $color;
  text-underline-offset: var(--underline-offset);
  animation: wave $duration linear infinite;
}
@keyframes wave {
  0%, 100% { --underline-offset: 0px; }
  50%      { --underline-offset: 5px; }
}

05Strikethrough draw-on

A bar at mid-text height

A separate .price__strike bar sits at mid-text height and grows with transform: scaleX(0→1) from the left edge, staging the instant a price gets struck through in front of the reader. Its height wobbles between 1.5px and 3px as it draws, so the strike reads as two or three presses of pressure rather than one clean rule.

Strikethrough draw-on
Open in a new tab

05 · Strikethrough draw-on — Measured area 0.204% · moving frames 19/23Original by GODRICH · SCSS and React ship in the zip · See the password

transform-origin: lefttransform:scaleX500ms

A discounted price or a finished task

It fits a discounted price sitting next to its original, or a completed line in a to-do list.

.price__strike {
  position: absolute; left: -2%; right: -2%; top: 52%; height: 2px;
  background: $color;
  transform: scaleX(0); transform-origin: left center;
  animation: strike $duration $easing infinite;
}
@keyframes strike {
  0%   { transform: scaleX(0); }
  30%  { transform: scaleX(1); }
  80%  { transform: scaleX(1); }
  100% { transform: scaleX(0); }
}

06Background fill

Growing background-size, pinned left

A linear-gradient sits behind a link and background-size grows from 0% 100% to 100% 100%, while background-position: left keeps the filled edge pinned so it never slides backward. The vertical size also climbs unevenly between 88% and 104% while it fills, so the color's edge blooms like wet paint instead of a hard bar.

Background fill
Open in a new tab

06 · Background fill — Measured area 3.281% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

background-sizebackground-position:left2s

Color arriving from the left

Navigation links and text buttons inside a card both read the fill as color simply arriving from the left, and moving the same rule onto :hover turns it into a css highlight text on hover effect.

.nav {
  background: linear-gradient(90deg, $color, $color);
  background-repeat: no-repeat;
  background-position: left center;
  background-size: 0% 100%;
  animation: fill $duration $easing infinite;
}
@keyframes fill {
  0%   { background-size: 0% 100%; }
  25%  { background-size: 100% 100%; }
  75%  { background-size: 100% 100%; }
  100% { background-size: 0% 100%; }
}

07Dotted line growing

A dotted bar stretched with scaleX

A thin bar painted with repeating-linear-gradient dots sits under the text and stretches out with transform: scaleX(0→1), so the dotted line looks like it's being laid down one dot at a time. The bar's height vibrates between 2.5px and 4px in rhythm with the grow, so each dot swells and shrinks with it.

Dotted line growing
Open in a new tab

07 · Dotted line growing — Measured area 0.312% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

repeating-linear-gradienttransform:scaleX700ms

A progress path or a connector

A step-by-step guide's progress path or a footnote's connector line both benefit from a line that visibly grows rather than just appearing.

.hl__dots {
  background-image: repeating-linear-gradient(90deg, $color 0 3px, transparent 3px 8px);
  transform: scaleX(0); transform-origin: left center;
  animation: grow $duration $easing infinite;
}
@keyframes grow {
  0%   { transform: scaleX(0); }
  40%  { transform: scaleX(1); }
  80%  { transform: scaleX(1); }
  100% { transform: scaleX(0); }
}

08Color sweep

A gradient 250% wider than the badge

A diagonal gradient behind a badge spans 250% of the badge's own width, and sliding background-position from 150% down to -150% on a linear timing keeps one strip of light sweeping across with no visible seam. The strip's vertical thickness also drifts by about a pixel on either side as it passes, giving the light a wet-paint grain instead of glass.

Color sweep
Open in a new tab

08 · Color sweep — Measured area 3.189% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

대각선 그러데이션background-position2s·linear

Drawing a glance back

Promo badges and new-or-limited labels use the sweep to keep drawing a glance back without ever fully stopping.

.badge::after {
  background: linear-gradient(115deg, transparent 30%, $color 48%, transparent 66%);
  background-size: 250% 100%;
  mix-blend-mode: overlay;
  animation: sweep $duration linear infinite;
}
@keyframes sweep {
  0%   { background-position: 150% 0; }
  100% { background-position: -150% 0; }
}

09Text shadow pop

Blur radius driven by a registered length

A <length> value registered through @property drives the blur radius inside filter: drop-shadow() from 0 up to 10px, and pairing that with transform: scale(1→1.06→1) makes a keyword pop once rather than glow the whole time. The blur's falloff stutters in small steps rather than fading on one clean curve, so the glow reads as ink drying instead of a machine's single decay.

Text shadow pop
Open in a new tab

09 · Text shadow pop — Measured area 3.196% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

@property <length>filter:drop-shadow1s

One moment, not a constant glow

It suits a pricing table's final number or a countdown digit that needs one moment of emphasis, not a constant loop.

@property --shadow-blur {
  syntax: "<length>"; inherits: false; initial-value: 0px;
}
.hl {
  filter: drop-shadow(0 0 var(--shadow-blur) $color);
  animation: pop $duration $easing infinite;
}
@keyframes pop {
  0%   { transform: scale(1);    --shadow-blur: 0px; }
  40%  { transform: scale(1.06); --shadow-blur: 10px; }
  70%  { transform: scale(1);    --shadow-blur: 4px; }
  100% { transform: scale(1);    --shadow-blur: 0px; }
}

Where does the highlight actually break?

Why a negative z-index vanished

Two of these nine, 01 and 02, rendered as nothing at all the first time they ran. Both hide their highlighter block behind the word with z-index: -1, and without isolation: isolate declared on .hl itself, that negative index doesn't get a stacking context of its own. It falls back to whichever ancestor opens one first, which here was the demo stage's own background layer. The block painted underneath the entire stage instead of just behind its own letters, and render measurement caught it exactly: 0% of the frame ever changed on either item. Adding one line, isolation: isolate, on .hl gave the block its own stacking context, and the same measurement then read 0.81% and 1.12% once the highlight was actually sitting on top of the text. MDN's isolation reference covers why a negative z-index needs a stacking context of its own before it means anything at all.

When subtle reads as broken

Text sinking into its own backdrop is the same contrast arithmetic that governs the HUD terminal landing sections, where every panel glows over a dark field.

Item 04 broke in a quieter way. The first pass animated text-underline-offset only between 1px and 3px, and that 2px swing landed under this project's per-pixel change threshold often enough that the render check counted just 4 of 23 sampled frames as moving. That fell short of the 6 needed to call it an animation rather than a still image. Widening the range to 0px–5px and switching the easing to linear fixed it outright, and the next pass read 23 of 23 frames as moving. Both text-underline-offset and its sibling text-decoration-thickness are safe to ship today: MDN's compatibility table for text-underline-offset puts support at Chrome and Edge 89, Safari 15, and Firefox 70, and MDN's table for text-decoration-thickness lists the same three numbers. But a range that's too subtle can still read as broken, even on a browser that supports the property fully. The full set, with both fixes already baked in, unlocks with hx9yh6mr exactly as it's spelled on this line, nothing capitalized and nothing added.

Accessibility

The emphasis stays, the motion goes

Every one of these nine still shows the emphasis itself under prefers-reduced-motion: reduce; only the motion getting there disappears. 01 and 02 lock their highlighter blocks at scaleX(1), already painted rather than caught mid-stroke. 04's wave settles on a fixed text-underline-offset instead of oscillating, and 09 keeps its drop-shadow blur steady at 4px with the bounce removed. 03 and 07 each add a decorative layer — an SVG path, a dotted bar — that carries no text of its own. Both mark that layer aria-hidden="true", so a screen reader skips the drawing and reads straight through to the sentence, which none of the nine ever touches. MDN's prefers-reduced-motion page lists the operating-system settings that flip this media query on.

Where the rest lives

More CSS-only text effects live on the CSS category hub; how this site is run, and what a zip actually contains, is on the about page.

FAQ

Can I use just one of these css highlight text examples, not all nine?

Yes — every item is scoped to its own class with no shared parent state, so copying one item's SCSS along with its matching HTML span is enough on its own. 01's highlighter marker and 06's background fill are the simplest starting points if only one effect is needed on a page.

Does a css text highlight animation still read fine on a screen reader?

The letters themselves are never touched by any of the nine — only a background layer, an underline, or a shadow moves around them, so the sentence reads exactly as written underneath. Items 03 and 07 add their own SVG or dotted-bar layer and mark it aria-hidden="true", so that decoration gets skipped instead of announced.

Do any of these need a build step, or does plain CSS work?

Plain CSS carries all nine; nothing depends on Sass beyond variables such as $duration and $color at the top of each file, which compile down to fixed values either way. 04 and 09 lean on @property, which registers directly in CSS with no preprocessor step, so both keep working if the SCSS is swapped for its compiled output untouched. Underlines moving on their own, with no highlighter sweep behind them, are collected in the CSS link underline animations.

Enter the archive password

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