← Back to gallery
SVG

Irregular Handwritten Mask Reveal

Hand-drawn brush-stroke mask reveals text/image underneath via mask-image + an animated stroke-dasharray on the mask path. Three brush styles: a soft watercolor wash, a confident marker stroke, and a textured chalk reveal.

svg-maskmask-imagebrush-strokestroke-dasharraypathLengthirregular-maskprefers-reduced-motion

Brush mask · stroke-dashoffset reveal

Irregular Handwritten Mask Reveal

A wide animated mask stroke uncovers a filled brush-script word so the text appears to fill in along the writing path instead of just outlining. Three variants demonstrate a bold word sweep, a slower lowercase note, and an energetic campaign script.

Wide sweep · bold word

Brush Word

A bold brush-script word ("Scribe") that fills in along a single broad sweep. The mask is wider than the glyph strokes so no thin gaps appear during intermediate frames — the word looks painted on, not drawn.

  • brush
  • wide mask
  • bold word

Lowercase · slower pass

Notebook Note

A softer lowercase note ("noted") with a gentler horizontal mask pass. The reveal is slower and curves slightly along the writing line, giving the editorial feel of someone underlining as they read.

  • lowercase
  • editorial
  • curved mask

Thick stroke · diagonal reveal

Campaign Script

A thick energetic reveal for campaign lettering ("launch"). The mask cuts across at a slight upward diagonal and overshoots the glyph bounds — the SVG viewport clips the final reveal cleanly so the fill arrives with momentum.

  • campaign
  • thick stroke
  • diagonal

Handwritten mask inspector

Brush Word

  • brush
  • wide mask
  • bold word

A bold brush-script word ("Scribe") that fills in along a single broad sweep. The mask is wider than the glyph strokes so no thin gaps appear during intermediate frames — the word looks painted on, not drawn.

Helped you ship something? 🐟 Send my cat a churu

.handwritten-stage__mask-stroke {
  fill: none;
  stroke: white;
  stroke-width: 110px;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 1;
  stroke-dashoffset: 1;
  animation: handwrittenReveal 5.00s cubic-bezier(0.55, 0.05, 0.35, 0.95) infinite;
}

.handwritten-stage__word {
  fill: var(--handwritten-accent);
  font-family: "Brush Script MT", "Segoe Script", "Snell Roundhand", cursive;
  font-weight: 700;
}

.handwritten-stage__guide {
  stroke: var(--handwritten-accent-2);
  stroke-width: 1.4px;
  opacity: 0.18;
}

@keyframes handwrittenReveal {
  0%, 6%    { stroke-dashoffset: 1; }
  /* draw across the word, hold the filled state, fade reset while invisible */
  46%       { stroke-dashoffset: 0; }
  82%       { stroke-dashoffset: 0; }
  92%, 100% { stroke-dashoffset: 1; }
}

How to make this

An irregular handwritten mask reveal paints a filled SVG word through a wide animated mask stroke, so the text appears to fill along a brush path instead of wiping in as a rectangle.

html
1<svg class="handwritten-mask-recipe" viewBox="0 0 320 180" role="img"  aria-labelledby="handwritten-mask-title handwritten-mask-desc">  <title id="handwritten-mask-title">Brush-script reveal</title>  <desc id="handwritten-mask-desc">The word Scribe is revealed by a wide curved mask stroke.</desc>  <defs>6    <mask id="handwritten-mask-recipe-mask" maskUnits="userSpaceOnUse">7      <rect width="320" height="180" fill="black" />8      <path class="handwritten-mask-recipe__stroke"        d="M20 90 C100 60 220 60 300 90"        pathLength="1" fill="none" stroke="white" stroke-width="110" />    </mask>  </defs>  <text class="handwritten-mask-recipe__word"    x="160" y="110" text-anchor="middle"    mask="url(#handwritten-mask-recipe-mask)"16    font-size="88" textLength="240" lengthAdjust="spacingAndGlyphs">    Scribe  </text></svg> <style>.handwritten-mask-recipe {  width: min(100%, 320px);  overflow: visible;  filter: drop-shadow(0 0 16px rgba(251, 191, 36, .22));}.handwritten-mask-recipe__word {  fill: #fbbf24;  font-family: "Brush Script MT", "Segoe Script", "Snell Roundhand", cursive;  font-weight: 700;  letter-spacing: 0;}.handwritten-mask-recipe__stroke {  stroke-linecap: round;  stroke-linejoin: round;  stroke-dasharray: 1;37  stroke-dashoffset: 1;  animation: handwritten-mask-recipe-reveal 5s cubic-bezier(.55,.05,.35,.95) infinite;}@keyframes handwritten-mask-recipe-reveal {  0%, 6% { stroke-dashoffset: 1; }  46% { stroke-dashoffset: 0; }  82% { stroke-dashoffset: 0; }  92%, 100% { stroke-dashoffset: 1; }}@media (prefers-reduced-motion: reduce) {47  .handwritten-mask-recipe__stroke {    animation: none;    stroke-dashoffset: 0;  }}</style>

Annotated snippet

  1. Line 1The SVG has a real title and description because the revealed word is the subject. Use aria-hidden only when the same word is already present as adjacent text.
    PitfallIs SVG text reliable across platforms?

    The exact cursive font can differ. Use textLength and lengthAdjust for layout stability, or convert final brand lettering to authored paths when the exact shape matters.

  2. Line 6maskUnits="userSpaceOnUse" makes the mask path use the same 320 by 180 coordinate system as the word. That keeps the brush pass predictable.
  3. Line 7The black rectangle hides everything by default. Only the white animated stroke opens a visible region through the mask.
  4. Line 8The mask path is intentionally wider than the letter strokes. A narrow mask would leave pinholes in cursive joins and descenders.

    Same brush path, same dashoffset animation, same word. Only the stroke-width on the mask path changes. 20px is narrower than the cursive glyph height — ascenders and descenders fall outside the mask and stay hidden, so the reveal looks chewed and uneven. 52px overshoots the glyph height comfortably, so the whole word resolves cleanly as the brush sweeps.

    PitfallWhy does my reveal leave gaps in the letters?

    The mask stroke is too thin or the path does not overshoot ascenders, descenders, and joins. Make the white mask stroke wider than the glyph strokes and test the intermediate frames.

  5. Line 16textLength and lengthAdjust constrain script fonts that vary by platform. Without this, fallback cursive glyphs can overflow the viewBox.
    PitfallIs SVG text reliable across platforms?

    The exact cursive font can differ. Use textLength and lengthAdjust for layout stability, or convert final brand lettering to authored paths when the exact shape matters.

  6. Line 37The reveal is dashoffset on the mask stroke, not on the visible text. The word is filled from the start; the mask controls what part can be seen.

    Same final word, same revealing motion duration. A rectangular wipe drags a vertical edge across the box — the geometry has nothing to do with how the word is written, so the reveal reads as a wipe. A wide mask stroke follows the writing path, so the visible region opens along the brush direction and reads as the word being written.

    PitfallWhy not animate the text fill directly?

    Text fill changes color everywhere at once. The handwritten illusion needs a spatial reveal, so keep the word filled and animate a mask stroke that decides which region is visible.

  7. Line 47Reduced motion leaves the mask fully open, so the final filled word is available without watching the brush pass.
    PitfallHow should reduced motion behave?

    Show the completed word immediately by setting stroke-dashoffset to 0 or rendering a static unmasked fallback. The word should remain readable without motion.

Other pitfalls

Are SVG masks expensive?
One small mask is usually fine. Large masked areas, heavy filters, many simultaneous loops, or text over video can increase paint cost. Keep the mask bounded and avoid unnecessary glow stacks.

Notes

Overview

Hand-drawn brush stroke mask reveal: an inline SVG <mask> contains a path that animates via stroke-dasharray + pathLength, and a visible content div references it via mask-image: url(#brush-reveal). The path acts like a paint stroke painting the content into view.

When to use it

Reach for brush-stroke masks on editorial heroes, gallery introductions, and brand reveals that want a human-made signature feel. Skip it for utility chrome — the hand-drawn brush is loud enough to feel marketing-y. Skip it for performance-critical surfaces; the mask path animation costs a re-rasterization per keyframe step.

How it works

Define an SVG mask in the document head: <svg><defs><mask id="brush"><path d="..." stroke="white" stroke-width="80" fill="none" pathLength="1" stroke-dasharray="1" stroke-dashoffset="1" /></mask></defs></svg>. Reference it from CSS: .hero { mask-image: url(#brush); -webkit-mask-image: url(#brush) }. Animate the mask path’s stroke-dashoffset from 1 down to 0 via CSS or JS — as the dash recedes, the white “paint” expands along the path, revealing more of the masked content underneath.

Production gotchas

Mask animations require a re-rasterization per keyframe step in older browsers — cap the path complexity (under ~5 control points) for smooth playback on mid-range mobile. Safari iOS still requires the -webkit-mask-image prefix alongside the unprefixed property. The brush stroke width determines how much of the underlying content reveals at each step; too narrow and the reveal feels stingy, too wide and the hand-drawn character is lost. The masked content needs explicit width + height on the wrapper or the mask positioning drifts on responsive viewports.

Accessibility

The content under the mask is real DOM and remains announced by screen readers from the start — the mask only changes what is visually painted. Under prefers-reduced-motion: reduce set stroke-dashoffset: 0 immediately (skip the animation) so the full content appears without the brush reveal. Verify masked-out content does not become a focus trap; tab order should match visual reveal order or skip masked regions until they paint in.

References

Implementation depth

The mask should behave like a brush, not a rectangular wipe. A wide stroked SVG path inside a mask gives the reveal irregular edges while the underlying content remains normal DOM and can be read immediately.

Mask sizing is the fragile part. Tie the mask viewBox and the content box together, then test responsive widths; otherwise the brush stroke drifts away from the text it is meant to reveal.