Text Scrim Reveal / Focal Point Highlight
The Apple-keynote reading experience: each line of copy sits dimmed at low contrast until it scrolls into a focal band mid-screen, where it blooms to full brightness — then dims again as it leaves. Line-by-line view() timelines with a contain-range focal window; the GSAP SplitText upsell, free.
Published
The code
<section class="sda-09" aria-label="Scroll to highlight text line by line" role="region" tabindex="0">
<div class="sda-09__flow">
<p class="sda-09__kicker">Manifesto · scroll to read</p>
<p class="sda-09__line">We believe motion is grammar, not decoration.</p>
<p class="sda-09__line">A line of text deserves the reader's full attention —</p>
<p class="sda-09__line">so we give it a stage, one line at a time.</p>
<p class="sda-09__line">The page dims what you have read,</p>
<p class="sda-09__line">holds what you are reading in perfect focus,</p>
<p class="sda-09__line">and keeps what comes next in quiet reserve.</p>
<p class="sda-09__line">No observers. No thresholds. No timeouts.</p>
<p class="sda-09__line">Just your scroll position, mapped to light.</p>
<p class="sda-09__line sda-09__line--em">Read at your own speed. The page keeps up.</p>
</div>
</section><section class="sda-09" aria-label="Scroll to highlight text line by line" role="region" tabindex="0">
<div class="sda-09__flow">
<p class="sda-09__kicker">Manifesto · scroll to read</p>
<p class="sda-09__line">We believe motion is grammar, not decoration.</p>
<p class="sda-09__line">A line of text deserves the reader's full attention —</p>
<p class="sda-09__line">so we give it a stage, one line at a time.</p>
<p class="sda-09__line">The page dims what you have read,</p>
<p class="sda-09__line">holds what you are reading in perfect focus,</p>
<p class="sda-09__line">and keeps what comes next in quiet reserve.</p>
<p class="sda-09__line">No observers. No thresholds. No timeouts.</p>
<p class="sda-09__line">Just your scroll position, mapped to light.</p>
<p class="sda-09__line sda-09__line--em">Read at your own speed. The page keeps up.</p>
</div>
</section>.sda-09,
.sda-09 *,
.sda-09 *::before,
.sda-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sda-09 {
font-family: 'Segoe UI',system-ui,sans-serif;
background: oklch(0.17 0.02 280);
color: oklch(0.95 0.01 280);
width: 100%;
height: 100vh;
overflow-y: auto;
}
.sda-09__flow {
width: min(680px,100%);
margin: 0 auto;
padding: 44vh 28px 52vh;
}
.sda-09__kicker {
font: 700 11px ui-monospace,Menlo,monospace;
letter-spacing: .2em;
text-transform: uppercase;
color: oklch(0.65 0.12 300);
margin-bottom: 7vh;
}
.sda-09__line {
font-size: clamp(24px,3.6vw,38px);
font-weight: 700;
line-height: 1.35;
letter-spacing: -.015em;
margin-bottom: 5.5vh;
text-wrap: pretty;
}
.sda-09__line--em {
color: oklch(0.8 0.14 300);
}
.sda-09:focus-visible {
outline: 3px solid oklch(0.65 0.12 300);
outline-offset: -3px;
}
@supports (animation-timeline: view()) {
.sda-09__line {
opacity: .32;
transform: translateX(0);
animation: sda-09-bloom linear both;
animation-timeline: view();
animation-range: contain 20% contain 55%;
}
}
@keyframes sda-09-bloom {
from {
opacity: .32;
color: oklch(0.6 0.02 280);
transform: translateX(0);
}
50% {
opacity: 1;
color: oklch(0.98 0.01 280);
transform: translateX(10px);
}
to {
opacity: .45;
color: oklch(0.7 0.02 280);
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.sda-09__line {
animation: none;
opacity: 1;
}
}.sda-09,
.sda-09 *,
.sda-09 *::before,
.sda-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sda-09 {
font-family: 'Segoe UI',system-ui,sans-serif;
background: oklch(0.17 0.02 280);
color: oklch(0.95 0.01 280);
width: 100%;
height: 100vh;
overflow-y: auto;
}
.sda-09__flow {
width: min(680px,100%);
margin: 0 auto;
padding: 44vh 28px 52vh;
}
.sda-09__kicker {
font: 700 11px ui-monospace,Menlo,monospace;
letter-spacing: .2em;
text-transform: uppercase;
color: oklch(0.65 0.12 300);
margin-bottom: 7vh;
}
.sda-09__line {
font-size: clamp(24px,3.6vw,38px);
font-weight: 700;
line-height: 1.35;
letter-spacing: -.015em;
margin-bottom: 5.5vh;
text-wrap: pretty;
}
.sda-09__line--em {
color: oklch(0.8 0.14 300);
}
.sda-09:focus-visible {
outline: 3px solid oklch(0.65 0.12 300);
outline-offset: -3px;
}
@supports (animation-timeline: view()) {
.sda-09__line {
opacity: .32;
transform: translateX(0);
animation: sda-09-bloom linear both;
animation-timeline: view();
animation-range: contain 20% contain 55%;
}
}
@keyframes sda-09-bloom {
from {
opacity: .32;
color: oklch(0.6 0.02 280);
transform: translateX(0);
}
50% {
opacity: 1;
color: oklch(0.98 0.01 280);
transform: translateX(10px);
}
to {
opacity: .45;
color: oklch(0.7 0.02 280);
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.sda-09__line {
animation: none;
opacity: 1;
}
}/* No JavaScript — each line owns a view() timeline restricted to a focal band via animation-range: contain 20% contain 55%; color and opacity bloom exactly while the line crosses it. */
/* No JavaScript — each line owns a view() timeline restricted to a focal band via animation-range: contain 20% contain 55%; color and opacity bloom exactly while the line crosses it. */Here's a working CSS animation-timeline Demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Text Scrim Reveal / Focal Point Highlight
Source: https://codefronts.com/motion/css-animation-timeline/text-scrim-reveal-focal-point-highlight/
The Apple-keynote reading experience: each line of copy sits dimmed at low contrast until it scrolls into a focal band mid-screen, where it blooms to full brightness — then dims again as it leaves. Line-by-line view() timelines with a contain-range focal window; the GSAP SplitText upsell, free.
## HTML
```html
<section class="sda-09" aria-label="Scroll to highlight text line by line" role="region" tabindex="0">
<div class="sda-09__flow">
<p class="sda-09__kicker">Manifesto · scroll to read</p>
<p class="sda-09__line">We believe motion is grammar, not decoration.</p>
<p class="sda-09__line">A line of text deserves the reader's full attention —</p>
<p class="sda-09__line">so we give it a stage, one line at a time.</p>
<p class="sda-09__line">The page dims what you have read,</p>
<p class="sda-09__line">holds what you are reading in perfect focus,</p>
<p class="sda-09__line">and keeps what comes next in quiet reserve.</p>
<p class="sda-09__line">No observers. No thresholds. No timeouts.</p>
<p class="sda-09__line">Just your scroll position, mapped to light.</p>
<p class="sda-09__line sda-09__line--em">Read at your own speed. The page keeps up.</p>
</div>
</section>
```
## CSS
```css
.sda-09,
.sda-09 *,
.sda-09 *::before,
.sda-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sda-09 {
font-family: 'Segoe UI',system-ui,sans-serif;
background: oklch(0.17 0.02 280);
color: oklch(0.95 0.01 280);
width: 100%;
height: 100vh;
overflow-y: auto;
}
.sda-09__flow {
width: min(680px,100%);
margin: 0 auto;
padding: 44vh 28px 52vh;
}
.sda-09__kicker {
font: 700 11px ui-monospace,Menlo,monospace;
letter-spacing: .2em;
text-transform: uppercase;
color: oklch(0.65 0.12 300);
margin-bottom: 7vh;
}
.sda-09__line {
font-size: clamp(24px,3.6vw,38px);
font-weight: 700;
line-height: 1.35;
letter-spacing: -.015em;
margin-bottom: 5.5vh;
text-wrap: pretty;
}
.sda-09__line--em {
color: oklch(0.8 0.14 300);
}
.sda-09:focus-visible {
outline: 3px solid oklch(0.65 0.12 300);
outline-offset: -3px;
}
@supports (animation-timeline: view()) {
.sda-09__line {
opacity: .32;
transform: translateX(0);
animation: sda-09-bloom linear both;
animation-timeline: view();
animation-range: contain 20% contain 55%;
}
}
@keyframes sda-09-bloom {
from {
opacity: .32;
color: oklch(0.6 0.02 280);
transform: translateX(0);
}
50% {
opacity: 1;
color: oklch(0.98 0.01 280);
transform: translateX(10px);
}
to {
opacity: .45;
color: oklch(0.7 0.02 280);
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.sda-09__line {
animation: none;
opacity: 1;
}
}
```
## JavaScript
```js
/* No JavaScript — each line owns a view() timeline restricted to a focal band via animation-range: contain 20% contain 55%; color and opacity bloom exactly while the line crosses it. */
```Here's a working CSS animation-timeline Demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Text Scrim Reveal / Focal Point Highlight
Source: https://codefronts.com/motion/css-animation-timeline/text-scrim-reveal-focal-point-highlight/
The Apple-keynote reading experience: each line of copy sits dimmed at low contrast until it scrolls into a focal band mid-screen, where it blooms to full brightness — then dims again as it leaves. Line-by-line view() timelines with a contain-range focal window; the GSAP SplitText upsell, free.
## HTML
```html
<section class="sda-09" aria-label="Scroll to highlight text line by line" role="region" tabindex="0">
<div class="sda-09__flow">
<p class="sda-09__kicker">Manifesto · scroll to read</p>
<p class="sda-09__line">We believe motion is grammar, not decoration.</p>
<p class="sda-09__line">A line of text deserves the reader's full attention —</p>
<p class="sda-09__line">so we give it a stage, one line at a time.</p>
<p class="sda-09__line">The page dims what you have read,</p>
<p class="sda-09__line">holds what you are reading in perfect focus,</p>
<p class="sda-09__line">and keeps what comes next in quiet reserve.</p>
<p class="sda-09__line">No observers. No thresholds. No timeouts.</p>
<p class="sda-09__line">Just your scroll position, mapped to light.</p>
<p class="sda-09__line sda-09__line--em">Read at your own speed. The page keeps up.</p>
</div>
</section>
```
## CSS
```css
.sda-09,
.sda-09 *,
.sda-09 *::before,
.sda-09 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.sda-09 {
font-family: 'Segoe UI',system-ui,sans-serif;
background: oklch(0.17 0.02 280);
color: oklch(0.95 0.01 280);
width: 100%;
height: 100vh;
overflow-y: auto;
}
.sda-09__flow {
width: min(680px,100%);
margin: 0 auto;
padding: 44vh 28px 52vh;
}
.sda-09__kicker {
font: 700 11px ui-monospace,Menlo,monospace;
letter-spacing: .2em;
text-transform: uppercase;
color: oklch(0.65 0.12 300);
margin-bottom: 7vh;
}
.sda-09__line {
font-size: clamp(24px,3.6vw,38px);
font-weight: 700;
line-height: 1.35;
letter-spacing: -.015em;
margin-bottom: 5.5vh;
text-wrap: pretty;
}
.sda-09__line--em {
color: oklch(0.8 0.14 300);
}
.sda-09:focus-visible {
outline: 3px solid oklch(0.65 0.12 300);
outline-offset: -3px;
}
@supports (animation-timeline: view()) {
.sda-09__line {
opacity: .32;
transform: translateX(0);
animation: sda-09-bloom linear both;
animation-timeline: view();
animation-range: contain 20% contain 55%;
}
}
@keyframes sda-09-bloom {
from {
opacity: .32;
color: oklch(0.6 0.02 280);
transform: translateX(0);
}
50% {
opacity: 1;
color: oklch(0.98 0.01 280);
transform: translateX(10px);
}
to {
opacity: .45;
color: oklch(0.7 0.02 280);
transform: translateX(0);
}
}
@media (prefers-reduced-motion: reduce) {
.sda-09__line {
animation: none;
opacity: 1;
}
}
```
## JavaScript
```js
/* No JavaScript — each line owns a view() timeline restricted to a focal band via animation-range: contain 20% contain 55%; color and opacity bloom exactly while the line crosses it. */
```How this works
Every line is its own element with animation-timeline: view() and animation-range: contain 20% contain 55% — the keyframes run only while the line travels through a horizontal band of the scrollport. The animation ramps color, opacity, and a registered custom property feeding a gradient underline, so the 'currently read' line glows while neighbours sit at scrim level.
The base state is the dimmed one ONLY inside @supports; outside it, text renders at full contrast — no reader is ever left with grey-on-grey. Since the highlight is scrubbed rather than toggled, slow readers and momentum-flickers both get exactly proportional emphasis, and scrolling back re-illuminates previous lines in reverse.
Make it yours
- Move the focal band by shifting the range:
contain 30% contain 70%centres it lower. - Widen the bloom (more lines lit at once) by stretching the range distance.
- Swap the color ramp for a
background-clip:textgradient sweep for a karaoke-style fill. - Per-word granularity: wrap words instead of lines — same CSS, smaller spans.
Gotchas — read before shipping
- Dim via
color/opacity, notfilter:blur(), if the block is long — dozens of simultaneous blurs add up. - Keep the dimmed state ≥ WCAG-ish legibility (this demo idles at 38% opacity, never invisible) — the scrim guides, it must not gate reading.
- Lines must be block-level elements for view() to measure them individually — a wrapping span won't do.
- The @supports gate on the dim state is non-negotiable: without it, unsupported browsers show permanently faded text.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 26+ | pending (flag) | 115+ |
Unsupported browsers show all lines at full contrast — a plain, perfectly readable manifesto.