Build the shimmer text effect
The trick is to animate a large gradient background, then clip that moving background into the text shape.
1. Make the highlight background
Start with a gradient. The bright center is the highlight that will later pass through the letters.
<!-- NEW in Step 1: add the text element. -->
<p class="shimmer">CSS is awesome, right?</p>
<style>
/* NEW in Step 1: create a static highlight background. */
.shimmer {
background: linear-gradient(
90deg,
#8a8a8a,
#ffffff,
#8a8a8a
);
}
</style>
2. Clip the background into text
Use transparent text plus background-clip:text. The text becomes a window that reveals the gradient behind it.
.shimmer {
/* NEW in Step 2: hide the original text fill. */
color: transparent;
/* NEW in Step 2: use a colorful gradient behind the transparent text. */
background: linear-gradient(
90deg,
#ff4d6d,
#ffd166,
#06d6a0,
#4cc9f0,
#8a5cf6
);
/* NEW in Step 2: reveal the background through the glyphs. */
background-clip: text;
-webkit-background-clip: text;
}
3. Give the gradient room to move
The background is wider than the text. The yellow area represents extra travel distance so the highlight can enter and leave cleanly.
<style>
/* NEW in Step 3: make the background wider than the text. */
.shimmer {
background-size: 400% 100%;
}
</style>
<script>
const root = document.documentElement;
const text = document.querySelector(".shimmer");
const backgroundScale = 4;
const textWidth = text.getBoundingClientRect().width;
const textHeight = text.getBoundingClientRect().height;
const shineWidth = 48;
const angle = 45 * Math.PI / 180;
// NEW in Step 3: calculate how far the shine must travel outside the text.
const safeDistance =
(shineWidth + textHeight * Math.abs(Math.cos(angle)) / 2)
/ Math.abs(Math.sin(angle));
// NEW in Step 3: use safeDistance to create safe start and end positions.
const backgroundWidth = textWidth * backgroundScale;
const travelWidth = backgroundWidth - textWidth;
const startPosition = (backgroundWidth / 2 + safeDistance) / travelWidth;
const endPosition =
(backgroundWidth / 2 - textWidth - safeDistance) / travelWidth;
root.style.setProperty("--start-position", `${startPosition * 100}%`);
root.style.setProperty("--end-position", `${endPosition * 100}%`);
</script>
4. Animate background-position
The letters do not move. Only the oversized background moves behind the clipped text.
<!-- NEW in Step 4: combine the pieces in one document. -->
<main>
<p class="shimmer">CSS is awesome, right?</p>
</main>
<style>
/* NEW in Step 4: receive the safe positions calculated in Step 3. */
:root {
--start-position: 72%;
--end-position: 28%;
}
/* NEW in Step 4: use a diagonal, narrow highlight. */
.shimmer {
color: transparent;
background: linear-gradient(
45deg,
#8a8a8a calc(50% - 2ch),
#ffffff 50%,
#8a8a8a calc(50% + 2ch)
);
background-size: 400% 100%;
background-clip: text;
-webkit-background-clip: text;
/* NEW in Step 4: animate the background, not the letters. */
animation: shimmer 2.4s infinite ease-in-out;
}
/* NEW in Step 4: finish at the safe side with a smooth hold. */
@keyframes shimmer {
0% { background-position: var(--start-position) 0; }
72%, 100% { background-position: var(--end-position) 0; }
}
</style>