← Back to gallery
SVG

SVG Path Fill Reveal

Two-stage SVG draw — outline traces first via stroke-dashoffset, then the matching fill reveals as a separate phase. Demonstrated on three production icons (heart, star, checkmark) so the technique reads at a glance.

svg-pathstroke-dasharraystroke-dashoffsetdraw-then-fillicon-revealpathLengthprefers-reduced-motion

Draw-to-fill / PathLength

SVG Path Fill Reveal

A two-stage SVG pattern — a custom path outline draws first via stroke-dashoffset, then the matching fill reveals as a separate phase. Demonstrated on three production-icon shapes (heart, star, checkmark) so the technique reads at a glance without being buried in scene context.

Like / Favorite toggle

Like Heart

A heart contour draws first with stroke-dashoffset, then the matching gradient fill reveals — the universal like icon as a clean two-stage technique demo. Single closed cubic-Bezier path (the simplest case for the draw-to-fill mechanic).

  • like icon
  • closed Bezier
  • two-phase

Rating / Favorite toggle

Favorite Star

A 5-point star outline draws first, then the gradient fill reveals — the universal rating / favorite icon as a clean technique demo. The polygon path is computed from outer / inner radii and 36° angle steps so the geometry is mathematically exact.

  • rating icon
  • 5-point polygon
  • two-phase

Symmetric crystal / 6-fold polygon

Crystal Snowflake

A 12-point dual-radius polygon (6-fold radial — outer vertices at 60° intervals, inner notches at the half-step angles) draws and fills as a single closed path. Demonstrates the technique on a more ornamental geometry while staying within the simple "one path → one fill" boundary.

  • snowflake
  • 12-point polygon
  • 6-fold symmetry

Save / Collection toggle

Save Bookmark

A bookmark rectangle (with the classic V notch at the bottom) draws as a single closed path — straight-edge geometry contrasts with the curved heart and angular polygons. Demonstrates the technique on a path that mixes line segments and a sharp inner notch.

  • save icon
  • rect + notch
  • two-phase

Power / Notification icon

Lightning Bolt

A 7-vertex zigzag bolt draws as a single closed path with sharp angle changes. Demonstrates the technique on an asymmetric, high-contrast geometry that still shares the same outline-then-fill rhythm.

  • bolt icon
  • zigzag
  • two-phase

Verified / Confirmation toggle

Verified Check

An outer ring draws first, the inner checkmark draws inside, then both fill simultaneously with a sky-cyan gradient. Two paths sharing one cycle — demonstrates the technique on a multi-element composition while staying simple enough to read as a single icon.

  • verified icon
  • 2-path icon
  • sequential draw

Path fill inspector

Like Heart

  • like icon
  • closed Bezier
  • two-phase

A heart contour draws first with stroke-dashoffset, then the matching gradient fill reveals — the universal like icon as a clean two-stage technique demo. Single closed cubic-Bezier path (the simplest case for the draw-to-fill mechanic).

Helped you ship something? 🐟 Send my cat a churu

.heart-stroke {
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: heart-draw 4.0s ease infinite both;
}

.heart-shape {
  opacity: 0;
  animation: heart-fill 4.0s ease infinite both;
}

@keyframes heart-draw {
  0%   { stroke-dashoffset: 1; opacity: 0; }
  6%   { opacity: 1; }
  32%  { stroke-dashoffset: 0; }
  88%, 100% { stroke-dashoffset: 0; opacity: 0.4; }
}

@keyframes heart-fill {
  0%, 28%   { opacity: 0; transform: scale(0.94); }
  48%       { opacity: 1; transform: scale(1); }
  88%       { opacity: 1; transform: scale(1); }
  98%, 100% { opacity: 0; transform: scale(0.96); }
}

How to make this

An SVG path fill reveal uses two matching paths: a normalized stroke path draws with stroke-dashoffset, then the filled copy fades or scales in as the second phase.

html
1<svg class="path-reveal" viewBox="0 0 180 180" aria-hidden="true">  <defs>    <linearGradient id="path-reveal-fill" x1="0" x2="1" y1="0" y2="1">      <stop offset="0%" stop-color="#fb7185" />      <stop offset="100%" stop-color="#f472b6" />    </linearGradient>  </defs>8  <path class="path-reveal__fill"    d="M90 158 C30 110 12 72 38 46 C58 26 82 38 90 60 C98 38 122 26 142 46 C168 72 150 110 90 158 Z"    fill="url(#path-reveal-fill)" />  <path class="path-reveal__stroke"    d="M90 158 C30 110 12 72 38 46 C58 26 82 38 90 60 C98 38 122 26 142 46 C168 72 150 110 90 158 Z"    pathLength="1" /></svg> <style>17.path-reveal {  width: 160px;  overflow: visible;  filter: drop-shadow(0 0 18px rgba(251, 113, 133, .28));}.path-reveal__fill {  opacity: 0;  transform-box: fill-box;  transform-origin: center;  animation: svg-fill-heart-fill 4s cubic-bezier(.4,0,.2,1) infinite both;}.path-reveal__stroke {  fill: none;  stroke: #fb7185;  stroke-width: 4;  stroke-linecap: round;  stroke-linejoin: round;  stroke-dasharray: 1;35  stroke-dashoffset: 1;  animation: svg-fill-heart-draw 4s cubic-bezier(.4,0,.2,1) infinite both;}38@keyframes svg-fill-heart-draw {  0% { stroke-dashoffset: 1; opacity: 0; }  6% { opacity: 1; }  32% { stroke-dashoffset: 0; opacity: 1; }  50%, 88% { stroke-dashoffset: 0; opacity: .4; }  96%, 100% { stroke-dashoffset: 0; opacity: 0; }}@keyframes svg-fill-heart-fill {  0%, 28% { opacity: 0; transform: scale(.94); }  48%, 88% { opacity: 1; transform: scale(1); }  98%, 100% { opacity: 0; transform: scale(.96); }}50@media (prefers-reduced-motion: reduce) {  .path-reveal__fill,  .path-reveal__stroke { animation: none; }  .path-reveal__fill { opacity: 1; transform: none; }  .path-reveal__stroke { opacity: 0; }}</style>

Annotated snippet

  1. Line 1The SVG is decorative in this example. If the icon represents a toggle state such as liked or saved, expose that state on the button with aria-pressed or an equivalent text label.
  2. Line 8The fill path is a separate copy of the same geometry. Separating fill from stroke lets the outline draw first while the solid body waits for its own reveal phase.
    PitfallWhy use two paths for a draw-to-fill icon?

    A stroke path and a fill path need different timing and paint properties. Keeping them separate lets the outline draw first, fade down, and then reveal the filled copy without fighting fill, stroke, opacity, and scale on one element.

  3. Line 17pathLength="1" normalizes the path math, so stroke-dasharray: 1 means one full path instead of one SVG unit.

    Same dashoffset 0 ↔ 1 animation on both. Without pathLength, 1 is one SVG unit — the shift is sub-pixel and the arc looks frozen. With pathLength="1", 1 is one full contour, so the same keyframe cleanly draws and erases the path.

    PitfallWhy does stroke-dasharray: 1 not draw my whole SVG path?

    Without pathLength, dash values are measured in SVG user units, so 1 is usually just a tiny dash. Add pathLength="1" to the path, then stroke-dasharray: 1 and stroke-dashoffset: 1 map to the entire contour.

  4. Line 35stroke-dasharray and stroke-dashoffset both start at 1. With the normalized path, that hides the complete stroke until the keyframe drives offset down to 0.
    PitfallWhy does stroke-dasharray: 1 not draw my whole SVG path?

    Without pathLength, dash values are measured in SVG user units, so 1 is usually just a tiny dash. Add pathLength="1" to the path, then stroke-dasharray: 1 and stroke-dashoffset: 1 map to the entire contour.

  5. Line 38The draw keyframe finishes early, leaving time for the fill to become the dominant state. This avoids the outline and fill competing for attention through the whole loop.

    Running both phases over the full loop lets the fill swallow the half-drawn stroke; staggering — stroke 0–32%, fill 28–88% — gives each phase its own solo moment before they hand off.

    PitfallWhy use two paths for a draw-to-fill icon?

    A stroke path and a fill path need different timing and paint properties. Keeping them separate lets the outline draw first, fade down, and then reveal the filled copy without fighting fill, stroke, opacity, and scale on one element.

  6. Line 50Reduced motion skips both phases and shows the final filled icon. The user gets the semantic end state without watching a draw animation.
    PitfallHow should a path fill reveal handle reduced motion?

    Skip the draw and fill phases, then show the completed icon immediately. If the icon represents user state, keep that state exposed through text or ARIA; the animation should never be the only confirmation.

Other pitfalls

Are SVG path draw animations expensive?
Animating stroke-dashoffset on a small icon is usually fine. Problems start with very complex paths, large glowing filters, or many icons looping at once. Keep the path simple, use a held final state, and avoid running decorative loops in large lists.
Which browsers support SVG pathLength for this pattern?
Modern Chromium, Firefox, and Safari support pathLength on SVG paths. Rendering can differ slightly on complex curves or transformed paths, so test the actual icon. If support fails, the fallback is still a visible static fill or stroke.
Advanced

Trace the outline with a spark on the pen tip, then bloom the fill from center

Same heart shape, same overall 4.8s cycle, BOTH start at 0% and END at 100% in sync. BEFORE follows the base recipe: stroke draws (0-32%), fill fades in via opacity (28-48%), shape sits with constant soft drop-shadow during hold, fades out at the end. AFTER stages four layers within the same cycle: outline traces (0-28%), a spark rides the pen tip via offset-path, a wider after-glow stroke trails behind, then the fill blooms from center with a springy overshoot (28-44%) and breathes via drop-shadow (44-82%). Identical phase boundaries; the AFTER just packs more story per beat.

View explanation and full code111 lines

The base recipe handles a quiet two-phase reveal: stroke draws, then the fill fades in by opacity. Production "saved/liked" affordances (App Store, Threads, Twitter) stage the moment with FOUR layers in one cycle: (1) the outline traces itself with stroke-dashoffset, (2) a small bright spark rides the pen tip via offset-path so the eye follows the line being drawn, (3) a wider after-glow stroke trails behind the outline for ink-trail feel, and (4) once the trace completes, a fill heart blooms from the center with a springy ease (cubic-bezier(.3, 1.5, .5, 1)) that overshoots past 1 and settles. After the bloom lands, the heart breathes via a drop-shadow filter pulse. All four layers stay pure CSS — offset-path replaces JS path sampling.

Paste this as a complete alternative to the base recipe — different SVG structure (single shared <path> referenced via <use>, plus a <circle> spark with offset-path) and a richer four-layer animation.

html
<figure class="heart-reveal" aria-labelledby="heart-reveal-caption">  <svg class="heart-reveal__svg" viewBox="0 0 100 100" aria-hidden="true">    <defs>      <path id="heart-reveal-path"        d="M50 84 C 18 60, 6 44, 6 28 C 6 14, 16 6, 28 6 C 38 6, 46 12, 50 22 C 54 12, 62 6, 72 6 C 84 6, 94 14, 94 28 C 94 44, 82 60, 50 84 Z"        pathLength="1" />      <radialGradient id="heart-reveal-spark-fill">        <stop offset="0%" stop-color="#fff" />        <stop offset="45%" stop-color="#ffd6e8" />        <stop offset="100%" stop-color="rgba(255, 94, 156, 0)" />      </radialGradient>    </defs>    <g class="heart-reveal__body">      <use class="heart-reveal__fill" href="#heart-reveal-path" fill="#ff4d8d" />      <use class="heart-reveal__afterglow" href="#heart-reveal-path"        fill="none" stroke="#ff6fa5" stroke-width="7" />      <use class="heart-reveal__outline" href="#heart-reveal-path"        fill="none" stroke="#ff5e9c" stroke-width="3" />    </g>    <circle class="heart-reveal__spark" cx="0" cy="0" r="4.5"      fill="url(#heart-reveal-spark-fill)" />  </svg>  <figcaption id="heart-reveal-caption">Saved to favorites</figcaption></figure> <style>.heart-reveal {  display: inline-grid;  justify-items: center;  gap: .6rem;  color: #f6d6e2;  font: 700 .9rem/1.2 ui-sans-serif, system-ui;}.heart-reveal__svg {  width: 220px;  height: 220px;  overflow: visible;}.heart-reveal__body {  transform-box: view-box;  transform-origin: 50px 50px;  animation: heart-reveal-breathe 4.8s ease-in-out infinite;}.heart-reveal__outline {  stroke-linecap: round;  stroke-linejoin: round;  stroke-dasharray: 1;  stroke-dashoffset: 1;  animation: heart-reveal-trace 4.8s cubic-bezier(.45, 0, .25, 1) infinite;}.heart-reveal__afterglow {  stroke-linecap: round;  stroke-linejoin: round;  stroke-dasharray: 1;  stroke-dashoffset: 1;  opacity: 0;  animation: heart-reveal-afterglow 4.8s ease-out infinite;}.heart-reveal__fill {  transform-box: view-box;  transform-origin: 50px 50px;  transform: scale(0);  animation: heart-reveal-bloom 4.8s cubic-bezier(.3, 1.5, .5, 1) infinite;}.heart-reveal__spark {  opacity: 0;  offset-path: path('M50 84 C 18 60, 6 44, 6 28 C 6 14, 16 6, 28 6 C 38 6, 46 12, 50 22 C 54 12, 62 6, 72 6 C 84 6, 94 14, 94 28 C 94 44, 82 60, 50 84 Z');  offset-distance: 0%;  offset-rotate: 0deg;  animation: heart-reveal-spark 4.8s cubic-bezier(.45, 0, .25, 1) infinite;}@keyframes heart-reveal-trace {  0%, 2%    { stroke-dashoffset: 1; }  28%       { stroke-dashoffset: 0; }  88%       { stroke-dashoffset: 0; }  94%, 100% { stroke-dashoffset: 1; }}@keyframes heart-reveal-afterglow {  0%, 4%    { opacity: 0; stroke-dashoffset: 1; }  10%       { opacity: .5; }  32%       { opacity: .28; stroke-dashoffset: 0; }  40%, 100% { opacity: 0; stroke-dashoffset: 0; }}@keyframes heart-reveal-spark {  0%, 2%    { opacity: 0; offset-distance: 0%; }  6%        { opacity: 1; }  26%       { opacity: 1; offset-distance: 100%; }  30%, 100% { opacity: 0; offset-distance: 100%; }}@keyframes heart-reveal-bloom {  0%, 28%   { transform: scale(0); }  44%       { transform: scale(1); }  88%       { transform: scale(1); }  94%, 100% { transform: scale(0); }}@keyframes heart-reveal-breathe {  0%, 44%   { filter: drop-shadow(0 0 6px rgba(244, 63, 126, .4)); }  62%       { filter: drop-shadow(0 0 18px rgba(244, 63, 126, .75)); }  82%, 100% { filter: drop-shadow(0 0 6px rgba(244, 63, 126, .4)); }}@media (prefers-reduced-motion: reduce) {  .heart-reveal__body,  .heart-reveal__outline,  .heart-reveal__afterglow,  .heart-reveal__fill,  .heart-reveal__spark { animation: none; }  .heart-reveal__outline,  .heart-reveal__afterglow { stroke-dashoffset: 0; }  .heart-reveal__fill { transform: scale(1); }}</style>

Notes

Overview

Two-stage SVG path reveal: the outline draws first via stroke-dashoffset, then a matching fill reveals as a separate phase. Demonstrated on three universal icons (heart, star, checkmark) so the technique reads at a glance without burying the mechanic in scene context.

When to use it

Reach for draw-then-fill on like / favorite / save toggles, success confirmations, achievement unlocks — binary state changes that deserve a tactile reveal. Skip it for any icon that toggles rapidly (e.g. real-time mute / unmute); the two-stage timing makes rapid toggles feel sluggish.

How it works

The SVG element has its stroke-dasharray set to the path length and stroke-dashoffset animated from full length to zero across the first phase (~60% of the timeline). The path’s fill starts at transparent and animates to its final color in the second phase (the remaining ~40%), via either a @keyframes rule on fill or a fill-opacity transition. Two-phase animation-delay chains the second behind the first so the outline completes before the fill begins — this is the tactile reveal.

Production gotchas

Animating fill from transparent to a color does not interpolate in older Firefox (the value just jumps at 100%); animate fill-opacity from 0 to 1 instead while keeping the fill color set to its final value. Toggling the animation backwards (un-fill, un-draw) for the unselected state requires reversed delays, which CSS does not handle cleanly — use animation-direction: reverse on a single combined keyframe, or accept a fade-out for the off state. Icons with fill-rule: evenodd can produce unexpected cut-out shapes during fill-in — preview both stages.

Accessibility

Toggle buttons that use this pattern need aria-pressed reflecting like/saved state. Don’t rely on color alone for the on/off state — the outline-only vs filled distinction does double duty here, which is good. Under prefers-reduced-motion: reduce skip both phases and jump to the final filled state for the on toggle, or to the outline-only for off. Screen readers announce the button label, not the icon, so make sure the accessible name carries the meaning.

References

Implementation depth

The draw effect depends on a measurable path. pathLength can normalize authoring so stroke-dasharray and stroke-dashoffset use predictable values even when icons have very different physical lengths.

The fill phase should start after the outline has visually arrived. If stroke and fill animate together, the reveal looks like a fade rather than a drawn icon. Reduced motion can skip straight to the filled final mark.