25 CSS Text Animations 24 / 25
CSS Mask Wipe Text Reveal Animation
A sharp-edged mask wipes across text to reveal it — using CSS mask-image with a gradient that hard-transitions at a moving boundary.
The code
<div class="ta-24">
<div class="ta-24__stage">
<p class="ta-24__eyebrow">Revealing</p>
<h2 class="ta-24__title" id="ta-24-title">The Future</h2>
<h2 class="ta-24__title ta-24__title--2" id="ta-24-title2">Is Now</h2>
<button class="ta-24__btn" id="ta-24-replay">↺ Replay wipe</button>
</div>
</div> <div class="ta-24">
<div class="ta-24__stage">
<p class="ta-24__eyebrow">Revealing</p>
<h2 class="ta-24__title" id="ta-24-title">The Future</h2>
<h2 class="ta-24__title ta-24__title--2" id="ta-24-title2">Is Now</h2>
<button class="ta-24__btn" id="ta-24-replay">↺ Replay wipe</button>
</div>
</div>.ta-24, .ta-24 *, .ta-24 *::before, .ta-24 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-24 ::selection { background: #0f766e; color: #fff; }
.ta-24 {
--bg: #030d0b;
--teal: #2dd4bf;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'Syne', 'Helvetica Neue', sans-serif;
}
.ta-24__stage { text-align: left; }
.ta-24__eyebrow {
font-size: 0.7rem;
color: #134e4a;
letter-spacing: 0.25em;
text-transform: uppercase;
margin-bottom: 0.4rem;
}
.ta-24__title {
font-size: clamp(2.2rem, 7vw, 4.5rem);
font-weight: 900;
letter-spacing: -0.02em;
color: var(--teal);
line-height: 1.05;
-webkit-mask-image: linear-gradient(to right, #000 50%, transparent 50%);
mask-image: linear-gradient(to right, #000 50%, transparent 50%);
-webkit-mask-size: 200% 100%;
mask-size: 200% 100%;
-webkit-mask-position: -100% 0;
mask-position: -100% 0;
}
.ta-24__title--2 {
color: #e2e8f0;
-webkit-mask-position: -100% 0;
mask-position: -100% 0;
}
.ta-24__title.wipe-in {
animation: ta-24-wipe 0.9s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.ta-24__title--2.wipe-in {
animation: ta-24-wipe 0.9s cubic-bezier(0.4, 0, 0.2, 1) 0.25s forwards;
}
@keyframes ta-24-wipe {
from { -webkit-mask-position: -100% 0; mask-position: -100% 0; }
to { -webkit-mask-position: 0% 0; mask-position: 0% 0; }
}
.ta-24__btn {
margin-top: 1.2rem;
display: block;
font-family: inherit;
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
background: none;
border: 1px solid #134e4a;
color: #0f766e;
padding: 0.4rem 1rem;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.ta-24__btn:hover { border-color: #2dd4bf; color: #2dd4bf; }
@media (prefers-reduced-motion: reduce) {
.ta-24__title { -webkit-mask-image: none; mask-image: none; animation: none !important; }
} .ta-24, .ta-24 *, .ta-24 *::before, .ta-24 *::after {
margin: 0; padding: 0; box-sizing: border-box;
}
.ta-24 ::selection { background: #0f766e; color: #fff; }
.ta-24 {
--bg: #030d0b;
--teal: #2dd4bf;
min-height: 100vh;
background: var(--bg);
display: flex;
align-items: center;
justify-content: center;
padding: 2rem;
font-family: 'Syne', 'Helvetica Neue', sans-serif;
}
.ta-24__stage { text-align: left; }
.ta-24__eyebrow {
font-size: 0.7rem;
color: #134e4a;
letter-spacing: 0.25em;
text-transform: uppercase;
margin-bottom: 0.4rem;
}
.ta-24__title {
font-size: clamp(2.2rem, 7vw, 4.5rem);
font-weight: 900;
letter-spacing: -0.02em;
color: var(--teal);
line-height: 1.05;
-webkit-mask-image: linear-gradient(to right, #000 50%, transparent 50%);
mask-image: linear-gradient(to right, #000 50%, transparent 50%);
-webkit-mask-size: 200% 100%;
mask-size: 200% 100%;
-webkit-mask-position: -100% 0;
mask-position: -100% 0;
}
.ta-24__title--2 {
color: #e2e8f0;
-webkit-mask-position: -100% 0;
mask-position: -100% 0;
}
.ta-24__title.wipe-in {
animation: ta-24-wipe 0.9s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
.ta-24__title--2.wipe-in {
animation: ta-24-wipe 0.9s cubic-bezier(0.4, 0, 0.2, 1) 0.25s forwards;
}
@keyframes ta-24-wipe {
from { -webkit-mask-position: -100% 0; mask-position: -100% 0; }
to { -webkit-mask-position: 0% 0; mask-position: 0% 0; }
}
.ta-24__btn {
margin-top: 1.2rem;
display: block;
font-family: inherit;
font-size: 0.72rem;
letter-spacing: 0.12em;
text-transform: uppercase;
background: none;
border: 1px solid #134e4a;
color: #0f766e;
padding: 0.4rem 1rem;
border-radius: 4px;
cursor: pointer;
transition: border-color 0.2s, color 0.2s;
}
.ta-24__btn:hover { border-color: #2dd4bf; color: #2dd4bf; }
@media (prefers-reduced-motion: reduce) {
.ta-24__title { -webkit-mask-image: none; mask-image: none; animation: none !important; }
}(function() {
const t1 = document.getElementById('ta-24-title');
const t2 = document.getElementById('ta-24-title2');
const btn = document.getElementById('ta-24-replay');
if (!t1 || !t2) return;
function play() {
[t1, t2].forEach(el => {
el.classList.remove('wipe-in');
void el.offsetWidth;
el.classList.add('wipe-in');
});
}
play();
if (btn) btn.addEventListener('click', play);
})(); (function() {
const t1 = document.getElementById('ta-24-title');
const t2 = document.getElementById('ta-24-title2');
const btn = document.getElementById('ta-24-replay');
if (!t1 || !t2) return;
function play() {
[t1, t2].forEach(el => {
el.classList.remove('wipe-in');
void el.offsetWidth;
el.classList.add('wipe-in');
});
}
play();
if (btn) btn.addEventListener('click', play);
})();How this works
A mask-image: linear-gradient() is applied to the text element with a hard-stop from fully opaque to fully transparent at a single pixel boundary. Unlike a soft gradient, the stop is placed at the same position twice — e.g., rgba(0,0,0,1) 50%, rgba(0,0,0,0) 50% — creating a razor-sharp wipe edge. The mask-position is then animated from -100% 0 to 100% 0, sweeping this hard boundary across the text from left to right.
JavaScript triggers the reveal by adding an animation class after a configurable delay, enabling scroll-triggered or interaction-triggered reveals. The mask-size is set to 200% 100% so the gradient pattern is double the element width, giving room to pan the hard edge fully across without the opaque region disappearing before the wipe completes.
Customize
- Change wipe direction by using a
mask-image: linear-gradient(to bottom, ...)and animatingmask-position-yfrom-100%to100%for a vertical curtain reveal. - Soften the wipe edge by spreading the gradient stop over
10%— e.g.,100% 45%, 0% 55%— for a semi-transparent leading edge like a cloth wipe. - Trigger the wipe on button click by attaching the animation class to a click handler on a sibling button element.
- Chain multiple wipes on multiple lines with staggered delays to reveal a full paragraph progressively from top to bottom.
- Add a coloured wipe-bar element — a thin
divthat tracks the mask boundary — by syncing atranslateXanimation of the same duration to visually show the wipe front.
Watch out for
mask-imagerequires the-webkit-mask-imageprefix in Safari and older Chrome builds — always declare both properties in the same rule block.- The mask applies to the element and all its children — if the element has a background, that too will be masked. Apply only to text-only elements or use a wrapper to isolate the mask.
- Animating
mask-positiondirectly is not universally composited — in Firefox it may trigger layout-dependent repaints. Prefermask-positionovermask-sizechanges for smoother frame rates.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 53+ | 15.4+ | 53+ | 53+ |
mask-image is supported in all modern browsers; use -webkit-mask-image prefix for Safari. Firefox added unprefixed support in v53 but prefixed versions work from earlier.