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.
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.
Draw-to-fill / PathLength
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
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).
Rating / Favorite toggle
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.
Symmetric crystal / 6-fold polygon
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.
Save / Collection toggle
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.
Power / Notification icon
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.
Verified / Confirmation toggle
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.
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.
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;35stroke-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>
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.
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.
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.
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.
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.
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.
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.
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.
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.
<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>
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.
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.
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.
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.
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.
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.