Text Reveal
Four title + subtitle blocks in a 280 px box: each word (1.5 rem, weight 800) starts at translateY(100%) below a line set to overflow: clip and rises during the first 40% of its entry into the box — 11 px of scrolling — then its subtitle goes from 0 to 1 over the 10 → 50% of its own entry, 37 px lower. Two CSS animations on animation-timeline: view(), zero JavaScript.
Updated
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Scroll has 70 effects, including 12 free. Explore the category →
3 usage examples



How it works
The sold HTML: a .demo-preview frame (centered flex, min-height: 300px, position: relative, #0a0a0f background, white text, font-family: sans-serif), the .sdc-scroll-container box (100% × 280px, overflow-y: auto, scroll-behavior: smooth, a 4 px scrollbar styled with ::-webkit-scrollbar, purple thumb at 40%), the .sdc-textreveal-content stage (900 px tall, padding: 80px 24px, flex column, gap: 40px) and four divs each holding a .sdc-textreveal-line (overflow: clip) that masks a span.sdc-textreveal-word (inline-block, 1.5 rem = 24 px, weight 800, white, 28 px tall) and a p.sdc-textreveal-sub (0.82 rem = 13 px, white at 40%, margin-top: 8px). Below the box, span.sdc-scroll-hint “Scrollez ↓” (absolute, 8 px from the bottom of the frame, 10.4 px, white at 30%) pulses from .3 to .7 every 2 s. Measured on a blank page: travel 900 − 280 = 620 px, words at 80, 184, 288 and 392 px from the top of the stage — a 104 px pitch, because the p's default bottom margin (13 px) adds to the block's 28 + 8 + 15 px and the 40 px gap. No JavaScript, no id, no CSS variable, no console error.
Both texts share the same setup: animation-timeline: view(), animation-duration: auto, animation-fill-mode: both, linear easing. view() gives each element an anonymous timeline read on the nearest scroll container — here the box — and graduated on the element's crossing of its frame; animation-range picks the segment that drives the keyframes. The word starts at translateY(100%) (100% of its own height, 28 px, below the line that clips it) and opacity: 0, and reaches translateY(0) / 1 over entry entry 40%: from the moment its top edge touches the bottom of the box until 40% of its entry, i.e. 11.2 px of scrolling. Measured in 8 px steps in Chromium 151: the third word is at 0 / 28 px at scrollTop 8, .69 / 8.6 px at 16, 1 / 0 at 24; the fourth makes the same trip between 112 and 128. The subtitle has no starting transform (the translateY(0) in sdcFadeIn changes nothing): it goes from 0 to 1 over entry 10% entry 50%, 40% of its 15 px, i.e. 6 px (measured .38 at 48, 1 at 56 for the third; .35 at 152, 1 at 160 for the fourth). The title → detail offset comes from geometry more than from the percentages: the subtitle enters the box 36 px after the word (28 px of word + 8 px of margin), and its 10% add only 1.5 px. Past scrollTop 160 everything is visible; 460 px of empty travel remain.
Until 30 September 2026 the line was set to overflow: hidden, and the mask never opened: an element with overflow: hidden is a scroll container, so view() attached to the line itself, whose frame the word fills from the start. Measured on that code: getAnimations()[0].timeline.source = .sdc-textreveal-line, animation finished and frozen at 20%, all four words at opacity 1 with no transform at rest — even the one sitting at 392 px, outside the box. Only the subtitles, direct children of the blocks, attached to the box and appeared. overflow: clip clips identically without creating a scroll container: the source becomes .sdc-scroll-container and the word rises out of its line. Other traits of the code: nothing is keyed by id, every word carries its own timeline, so several copies coexist on a page and a fifth block needs no extra CSS; scroll-behavior: smooth only eases programmatic scrolls; the font is the system sans-serif (nothing is loaded); and the JavaScript field of the sold code is empty.
Accessibility
- prefers-reduced-motion not in the code: measured under the preference in Chromium, the third word still goes from 0 to 1 between scrollTop 8 and 24, the hint pulses (
animation-play-state: running) andscroll-behaviorstayssmooth. The motion follows the gesture and hides nothing lasting — a block that has entered the box stays displayed — which makes it an interaction-triggered animation (WCAG 2.3.3). For a motionless fallback:@media (prefers-reduced-motion: reduce) { .sdc-textreveal-word, .sdc-textreveal-sub { animation: none; opacity: 1; transform: none } .sdc-scroll-hint { animation: none } .sdc-scroll-container { scroll-behavior: auto } }. - Keyboard: measured, Tab focuses the box without a
tabindexin Chromium and Firefox (Arrow Down: 40 and 51 px, PageDown: 285 and 303 px — the reveal follows), not in WebKit (focus stays onbody, arrows do nothing). Addtabindex="0", anaria-label(“Four words, vertical scrolling”) and a:focus-visiblestyle on.sdc-scroll-container. With a finger, the gesture must start inside the box;overscroll-behavior: containkeeps the page from being dragged along at the end of the travel. - Contrast: the white word on
#0a0a0fmeasures 19.8:1. The subtitle at 40% white reads 3.8:1 at 13 px — below the 4.5:1 required for text of that size; 60% (rgba(255,255,255,.6)) gives 7.3:1. The hint reads 2.6:1 static, 1.2:1 to 1.8:1 under the pulse (the .3 to .7 opacity multiplies the color's 30% white): decorative, give itaria-hidden="true". The frame's#0a0a0fbackground is part of the code: on a white page, remove it and the white text vanishes (1:1) — change the three colors then, as the third scene does on light paper. - Screen readers and structure:
opacity: 0removes nothing from the accessibility tree, the eight texts are read in order before and after scrolling, and nothing is announced along the way — there is nothing to announce. The word is aspanin adiv, the subtitle ap: no heading in the page outline. For room or leg names, put the word in anh2orh3(the class is enough, the tag is free) and keep thep. The “Scrollez ↓” label is hard-coded in French in the HTML, whatever the page language.
Browser compatibility
CSS only: animation-timeline: view(), animation-duration: auto, animation-range with the entry keywords — scroll-driven animations, which landed in Chrome and Edge 115 (July 2023) and Safari 26 (September 2025). Firefox renders them in no version: measured in Firefox 153, CSS.supports('animation-timeline: view()') returns false, animation-duration: auto is rejected (computed duration 0 s) and the animation, finished on a document timeline, applies its last keyframe — all eight texts are shown at opacity 1, with no transform, from rest, and the box scrolls normally (620 px of travel). A Firefox visitor therefore sees a static, complete list, without the slide. ::-webkit-scrollbar is ignored by Firefox (native scrollbar). Zero dependencies, zero script.
Where animation-timeline and animation-duration: auto are unknown (Firefox, Safari before 26, Chrome before 115), both declarations are dropped, the animation lasts 0 s and animation-fill-mode: both freezes it on its last keyframe: opacity 1, translateY(0). The code has no @supports block and needs none, because the final state is the readable state; it would need one — @supports not (animation-timeline: view()) { … } — if you inverted the keyframes (text that fades out) or if the end of sdcTextReveal stopped being the reading state. Without overflow: clip (Chrome before 90, Firefox before 81, Safari before 16), the line no longer clips anything, but no word is translated there: nothing changes on screen.
The code
HTML structure to paste into your page (the CSS + JS come with Premium):
<div class="demo-preview">
<div class="sdc-scroll-container">
<div class="sdc-textreveal-content">
<div>
<div class="sdc-textreveal-line">
<span class="sdc-textreveal-word">…</span>
</div>
<p class="sdc-textreveal-sub">…</p>
</div>
<div>
<div class="sdc-textreveal-line">
<span class="sdc-textreveal-word">…</span>
</div>
<p class="sdc-textreveal-sub">…</p>
</div>
<div>
<div class="sdc-textreveal-line">
<span class="sdc-textreveal-word">…</span>
</div>
<p class="sdc-textreveal-sub">…</p>
</div>
<div>
<div class="sdc-textreveal-line">
<span class="sdc-textreveal-word">…</span>
</div>
<p class="sdc-textreveal-sub">…</p>
</div>
</div>
</div>
<span class="sdc-scroll-hint">…</span>
</div>
The full HTML + CSS + JS of Text Reveal, ready to paste — plus all 811 effects. Or €299 once, lifetime access.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Word range (CSS — .sdc-textreveal-word { animation-range: entry entry 40% }) |
entry 0% → entry 40% (11.2 px for a 28 px word) | The percentages are those of the word's entry into the box, hence proportional to its height, not the box's. entry 0% entry 100% stretches the rise over the whole entry (28 px, the setting of the three scenes); entry 0% cover 30% extends it until the word has climbed 30% of the box (about 92 px). |
Subtitle range (CSS — .sdc-textreveal-sub { animation-range: entry 10% entry 50% }) |
entry 10% → entry 50% (6 px for a 15 px height) | The lag behind the word comes mostly from the 36 px between their entries; the 10% add only 1.5 px. For a real pause, delay the start (entry 100% cover 30%) or raise margin-top. |
Word start (CSS — transform: translateY(100%) and opacity: 0) |
100% of its height (28 px), invisible | The word starts fully below its line. translateY(60%) lets a third of it show before any scrolling; opacity: 1 in the base rule gives a pure mask reveal, without fade — the final keyframe stays valid. |
Box and stage (CSS — .sdc-scroll-container { height: 280px }, .sdc-textreveal-content { height: 900px }) |
280 px / 900 px (620 px of travel, everything revealed at 160 px) | The stage is fixed at 900 px whatever the content: 460 px of empty travel after the last block. height: auto with a padding-bottom of at least half the box lets the last block enter completely (the scenes: 110 to 150 px). |
Size and colors (CSS — font-size: 1.5rem, color: #fff; .sdc-textreveal-sub: .82rem, rgba(255,255,255,.4)) |
24 px / 800 / white; 13 px / white at 40% | The range follows the word's height: at 44 px (scene 2), the 40% make 18 px of travel. The subtitle at 40% reads 3.8:1 on #0a0a0f; 60% rises to 7.3:1. On a light background, change the three colors and drop the frame's background. |
Line mask (CSS — .sdc-textreveal-line { overflow: clip }) |
clip | Keep it as is: hidden would make the line a scroll container and view() would attach to it — the words would stop moving (that was the code's defect before 30/09/2026). The line takes a rule (border-top, scene 3) or a padding-top without breaking anything. |
Hint and scrollbar (CSS — .sdc-scroll-hint, ::-webkit-scrollbar) |
“Scrollez ↓” 8 px from the bottom, 4 px bar | The hint anchors to the .demo-preview frame (position: relative): without that frame it drops to the bottom of the page — delete the span and the @keyframes sdcHintPulse. The bar only concerns Chrome and Safari; scrollbar-width: thin covers Firefox. |
FAQ
overflow: hidden — the code sold until 30 September 2026, and still the copy displayed on the category page. An element with overflow: hidden is a scroll container; view() takes the nearest one, hence the line, where the word fills the whole frame: the animation is over before the first gesture (measured: timeline source = .sdc-textreveal-line, state finished frozen at 20%, four words at opacity 1 at rest). The rule applies to any ancestor between the word and the box: a card with overflow: hidden, an accordion, a carousel capture the timeline the same way. Replace it with overflow: clip (or clip-path: inset(0)), which clips without creating a container, and check with el.getAnimations()[0].timeline.source that the source really is the box.animation-range: entry entry 40% is measured on the word's entry, that is on its own height: 40% of 28 px make 11.2 px of scrolling, and 6 px for the subtitle. A 100 px wheel notch crosses them in one or two frames; on a trackpad or with a finger, the slide shows. Measured in 8 px steps: 0 → .69 → 1 over three readings. To stretch it, widen the range (entry 0% entry 100%: 28 px, the choice of the three scenes; entry 0% cover 30%: about 92 px, the word finishes rising when it is already well inside the box) or enlarge the word — the range grows with it. Leave animation-duration: auto alone: with a scroll timeline, the range stands in for the duration.animation-timeline: view(), animation-duration: auto, animation-range, zero JavaScript, Firefox flat in both cases — but not the gesture. fx-0569 lines up 43 inline-block words in a 300 px paragraph and animates only their color: from white at 15% to #e0e7ff (the six accents from purple at 20% to #a78bfa) over entry cover 40%, a single range for all. The words are in place from the start and light up as they slide toward the top of the box — a reading that brightens. fx-0560 works in pairs: a word that moves (translateY(100%) → 0) behind a line mask, then a subtitle that appears on a second range, 36 px lower. It makes four titles with their detail, where fx-0569 makes running text. Pick fx-0569 for a paragraph to read, fx-0560 for a list of short titles each followed by its detail.