Text✨ Premium

Stroke Animation

Progressive left-to-right fill of outlined text via a pseudo-element and animated clip-path — 100% CSS, zero JavaScript required.

CSSStrokeFill

You'll get access to the interactive demo with a free account.

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Text has 56 effects, including 4 free. Explore the category →

3 usage examples

Creative agency — Large display headline — Stroke Animation example 1

① Creative agency — Large display headline

WhenHero section of a portfolio or agency website, when a single word captures the brand's visual identity against a dark background.
WhyThe hollow outline animates the word's reveal like a stamp pressing ink — a memorable signature that doesn't overpower the rest of the composition, since the outline remains always visible.
SettingsStroke/fill color #c026d3 (fuchsia brand), font-size: 6rem; font-weight: 900, animation-duration: 3s, animation-direction: normal + animation-fill-mode: forwards + animation-iteration-count: 1 for a one-shot reveal.
Splash screen — Product name on startup — Stroke Animation example 2

② Splash screen — Product name on startup

WhenLoading screen during the initialization of a web or mobile app — the name fills and empties on loop while the app loads.
WhyThe alternating loop creates a breathing visual rhythm without a spinning indicator — a single CSS <code>&lt;span&gt;</code>, no animation library.
SettingsStroke/fill color #06b6d4 (cyan-500), font-size: 3.5rem; letter-spacing: 0.06em, animation-duration: 2.5s ease-in-out, direction alternate infinite (default kept).
End screen — Dramatic reveal of the result — Stroke Animation example 3

③ End screen — Dramatic reveal of the result

WhenResult screen of a quiz or competitive game, when the winning word is unveiled after a moment of suspense.
WhyThe left-to-right wipe mimics the unveiling of a printed verdict; with <code>direction: normal</code> and <code>fill-mode: forwards</code>, the reveal never goes back — a sense of finality.
SettingsStroke/fill color #f59e0b (amber-400), font-size: 4rem; font-weight: 900, animation-duration: 1.5s cubic-bezier(.4,0,.2,1), animation-iteration-count: 1, animation-fill-mode: forwards.

How it works

The effect relies on two stacked layers: the .text-stroke element carries -webkit-text-stroke: 2px #6366f1 and -webkit-text-fill-color: transparent — the text appears as a hollow outline. A ::before pseudo-element is generated with content: attr(data-text) and positioned absolute at left: 0; top: 0, forming an exact filled copy of the text on top of the outline.

The reveal is achieved via an animated clip-path: ::before starts at clip-path: inset(0 100% 0 0) (100% clipping from the right — the filled text is fully hidden) and the @keyframes strokeFill animation progresses it to clip-path: inset(0) (no clipping — text fully visible). The visual result is a left-to-right wipe that progressively reveals the fill color through the outline.

The animation-direction: alternate property reverses the cycle at each iteration: the text fills, then empties right-to-left, then fills again. Switching to animation-direction: normal with animation-iteration-count: 1 and animation-fill-mode: forwards produces a one-shot reveal — the text stays filled at the end.

The embedded JS defines a helper function splitChars(el) that splits the element's text into individual <span class="char"> elements for character-by-character animations with staggered delays. It is not called on startup — the default animation applies to the whole word via CSS. The document.getElementById('fadeInText') call returns null in the supplied configuration (no fadeInText id in the HTML): no JS side effects on initialization.

Accessibility

  • prefers-reduced-motion absent: no media query is present in the supplied code — the animation runs unconditionally. Recommended fix: @media (prefers-reduced-motion: reduce) { .text-stroke::before { animation: none; clip-path: inset(0) } } (text stays visible and static).
  • The text is real DOM content (the <span> element's textContent) — read natively by screen readers without additional ARIA attributes.
  • The fill color #6366f1 on background #0a0a0f reaches a contrast ratio of ≈ 3.5:1 — meets WCAG AA for large text (≥ 24 px / weight ≥ 700), insufficient for body text. During the outline-only phase, the thin stroke may fall below threshold.
  • No keyboard interaction — the effect is 100% CSS, no implicit focus generated.
  • No aria-label or role is set on the element. Add an aria-label to the wrapper if the text is heavily styled or if color alone makes it ambiguous (e.g. text on a complex background).

Browser compatibility

Relies on -webkit-text-stroke, -webkit-text-fill-color, and clip-path: inset() — supported in all modern browsers since 2019. The -webkit- prefix is kept by convention but works without it on Firefox and Edge.

Chrome 88+✓ Full
Firefox 90+✓ Full
Safari 15+✓ Full
Edge 88+✓ Full
Mobile iOS✓ Full
Android Chrome✓ Full

Without <code>-webkit-text-fill-color</code> support, the text displays with its inherited CSS color (no transparent outline). Without <code>clip-path</code>, the <code>::before</code> pseudo-element is fully visible on load — the text remains readable, but the reveal animation does not occur.

The code

HTML structure to paste into your page (CSS + JS available with a premium account):

index.html — structure
<div class="demo-preview">

  <!-- data-text must exactly match the element's textContent
       (both are read by screen readers) -->
  <span
    class="demo-text text-stroke"
    data-text="YOUR TEXT"
  >YOUR TEXT</span>

</div>
🔒 Unlock the full code — from €2.99 the first month

Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
-webkit-text-stroke on .text-stroke 2px #6366f1 Stroke thickness (px) and color (hex). 1px for a fine line, 4px for large display use. The color may differ from the fill for a two-tone effect.
-webkit-text-fill-color on .text-stroke::before #6366f1 Fill color. Usually matches the stroke color, but can differ intentionally (e.g. white stroke, gold fill).
animation-duration on .text-stroke::before 2s Duration of one half-cycle (fill or empty). 1s = fast and punchy, 4s = slow and cinematic.
animation-direction on .text-stroke::before alternate Set to normal for a one-way left-to-right wipe without reversal. Combine with animation-iteration-count: 1 and animation-fill-mode: forwards.
animation-timing-function on .text-stroke::before ease-in-out Wipe speed curve. linear = steady progression, cubic-bezier(.4,0,.2,1) = slow start then dramatic acceleration.
font-size on .demo-text 2.5rem Text size. The effect reads best from 2rem up — below that, the thin outline can make the text hard to read.
data-text on the element STROKE Displayed text AND the value fed into content: attr(data-text) on the pseudo-element. Must match the textContent — both are read by accessibility tools.
Initial clip-path on .text-stroke::before inset(0 100% 0 0) Sets the wipe start edge. inset(0 0 0 100%) reverses direction (right-to-left reveal). inset(100% 0 0 0) produces a bottom-to-top wipe.

FAQ

Yes, entirely. The fill animation is 100% CSS. The JS included in the effect defines a helper function splitChars(el) for letter-by-letter animation, but it is not called on startup. You can omit that JS block with no impact on the base animation.
Replace animation: 2s ease-in-out 0s infinite alternate with animation: 2s ease-in-out 0s 1 normal none forwards strokeFill. Key values: 1 (single iteration), normal (left-to-right fill without reversal), forwards (the text stays filled at the end).
Yes, using the splitChars(el) function provided in the JS: it splits the text into individual <span class="char"> elements and returns the NodeList. Then apply an increasing animation-delay to each: chars.forEach((c, i) => c.style.animationDelay = i * 0.12 + 's'). Add position: relative to .char so the CSS ::before positions correctly on each letter.