Back to Learn SVG Animation

Hero Wordmark Reveal with a Quiet Idle Shine

A restrained, brand-forward hero wordmark reveal that draws via a stroke-path over 900ms, fills shortly after, and runs a very soft left-to-right shine every 6s — implemented with CSS-first techniques and minimal JavaScript, and fully respectful of prefers-reduced-motion.

Overview

This pattern is ideal for a landing-page hero where motion should feel deliberate and refined. The wordmark strokes draw in (900ms), the fill fades in after a 120ms pause, and a subtle gloss sweeps left-to-right every 6 seconds. The stack is CSS-first with a tiny bit of JS to sequence the draw+fill and to respect reduced-motion preferences.

Key ideas

  • Draw the outlines using stroke-dasharray / stroke-dashoffset.
  • Apply the fill via a separate style transition (fill-opacity) after the draw completes.
  • Create the shine as a gradient-filled rectangle masked to the wordmark and animate the rectangle’s transform. Use keyframes so the shine only runs in a short portion of a long (6s) animation cycle.
  • Minimal JS only to kick off the initial sequencing and to short-circuit animations when prefers-reduced-motion is set.

SVG + CSS + a pinch of JS

The example below uses an inline SVG with path elements for the wordmark. Replace the sample paths with your actual logo/wordmark paths.

<!-- HTML / SVG fragment -->
<figure class="hero-wordmark" aria-hidden="false">
  <svg viewBox="0 0 600 150" class="wordmark-svg" role="img" aria-label="Brand name">

    <defs>
      <linearGradient id="shineGrad" x1="0" x2="1" y1="0" y2="0">
        <stop offset="0%" stop-color="rgba(255,255,255,0)" />
        <stop offset="50%" stop-color="rgba(255,255,255,0.65)" />
        <stop offset="100%" stop-color="rgba(255,255,255,0)" />
      </linearGradient>

      <mask id="textMask" maskUnits="userSpaceOnUse">
        <!-- solid white where the shine should be visible -->
        <g fill="#fff">
          <!-- duplicate the same paths used for the wordmark fill -->
          <path d="M20,120 L80,20 ..." /> <!-- replace with actual path(s) -->
        </g>
      </mask>
    </defs>

    <g class="wordmark-group">
      <!-- STROKE LAYER: visible while drawing -->
      <g class="stroke-layer" stroke="#0A0A0A" stroke-width="3" fill="none" stroke-linecap="round" stroke-linejoin="round">
        <path class="draw-path" d="M20,120 L80,20 ..." /> <!-- same path(s) -->
      </g>

      <!-- FILL LAYER: starts transparent, fades in after the draw -->
      <g class="fill-layer" fill="#0A0A0A" fill-opacity="0">
        <path d="M20,120 L80,20 ..." /> <!-- same path(s) -->
      </g>

      <!-- SHINE LAYER: a rectangle with a gradient, masked to the wordmark -->
      <rect class="shine-rect" x="-200" y="0" width="400" height="150" fill="url(#shineGrad)" mask="url(#textMask)" />
    </g>
  </svg>
</figure>

Now the styling and animation. Copy these into your CSS file or a <style> block. The code uses CSS variables to make tuning easy.

/* CSS */
:root {
  --draw-duration: 900ms;
  --fill-delay: 120ms; /* after draw completes */
  --fill-duration: 220ms;
  --shine-cycle: 6000ms; /* total cycle: shine once every 6s */
  --shine-active: 750ms; /* how long the visible sweep lasts within the cycle */
}

.wordmark-svg { width: 420px; height: auto; display: block; }

/* Stroke drawing using dash technique */
.draw-path {
  stroke-dasharray: 1200; /* tune this to be >= path length */
  stroke-dashoffset: 1200;
  transition: stroke-dashoffset var(--draw-duration) ease-in-out;
}

/* After JS adds .is-drawn, the stroke draws in */
.wordmark-svg.is-drawn .draw-path {
  stroke-dashoffset: 0;
}

/* Fill layer fades in slightly after the draw completes */
.fill-layer {
  transition: fill-opacity var(--fill-duration) ease-in-out var(--fill-delay);
}
.wordmark-svg.is-filled .fill-layer { fill-opacity: 1; }

/* Keep stroke subtle once filled */
.wordmark-svg.is-filled .stroke-layer { opacity: 0.85; }

/* Shine: full animation cycle is --shine-cycle. The visible sweep lives in the final portion
   of that cycle (using keyframes) so the shine only runs briefly every 6s. */
@keyframes softShine {
  0%, 80% { transform: translateX(-40%); opacity: 0; }
  85% { opacity: 1; }
  88% { transform: translateX(-10%); opacity: 1; }
  95% { transform: translateX(60%); opacity: 1; }
  100% { transform: translateX(120%); opacity: 0; }
}

.shine-rect {
  transform-origin: 0 0;
  animation: softShine var(--shine-cycle) linear infinite;
  opacity: 0; /* invisible except during the brief sweep */
  mix-blend-mode: screen; /* subtle effect on dark fills */
  pointer-events: none;
}

/* Respect prefers-reduced-motion: skip shine and compress timings */
@media (prefers-reduced-motion: reduce) {
  :root {
    --draw-duration: 0ms;
    --fill-delay: 0ms;
    --fill-duration: 0ms;
    --shine-cycle: 0ms;
  }
  .shine-rect { display: none; }
}

Finally, a tiny JS snippet to sequence the draw and fill. It also ensures the full wordmark is visible immediately if the user requests reduced motion.

/* Minimal JS sequencing */
document.addEventListener('DOMContentLoaded', function () {
  var svg = document.querySelector('.wordmark-svg');
  if (!svg) return;

  var prefersReduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
  if (prefersReduced) {
    // Immediately show the finished mark
    svg.classList.add('is-drawn', 'is-filled');
    return;
  }

  // Trigger stroke draw
  requestAnimationFrame(function () {
    svg.classList.add('is-drawn');
  });

  // After the draw duration + fill delay, add the filled state
  var drawMs = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--draw-duration')) || 900;
  var fillDelayMs = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--fill-delay')) || 120;

  setTimeout(function () {
    svg.classList.add('is-filled');
  }, drawMs + fillDelayMs);
});

Tuning and accessibility notes

  • Path length: stroke-dasharray must be at least the path length. You can use JS to compute exact path lengths via path.getTotalLength() and set dasharray/dashoffset dynamically for precise results.
  • Color and contrast: choose a shine color and blend mode that complements your brand palette. For dark type, a soft white with screen blend works well; for light type, try a faint semi-transparent dark edge or an overlay using multiply.
  • Reduced motion: the pattern defaults to fully visible, non-animated state when prefers-reduced-motion is set. This is crucial for users who are motion-sensitive and for automated accessibility audits.
  • Performance: keep the shine element simple (one rect + mask) and avoid animating geometry properties of many nodes. The technique above animates a transform on a single rect which is GPU-friendly.

Conclusion

This approach balances brand polish and restraint: a confident stroke reveal, a quick, tasteful fill, and an occasional, very soft shine to imply premium finish. Because it’s CSS-driven with only a small sequencing script and respects prefers-reduced-motion, it fits neatly into hero sections where brand presence must feel finished but never fussy.