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.