Reveal on Scroll
A vertical timeline whose entries fade, rise and un-rotate into place as they cross into view — each one self-timed by a per-element view() timeline, so they stagger naturally. A slim rail on the side tracks overall scroll progress. No JS.
- animation-timeline: view()
- animation-range: entry
- scroll() progress
Source
A single self-contained .astro component. It uses Prism's design tokens (--spectrum-*, --ink…) with sensible fallbacks, so it renders even outside this site.
---
export const meta = {
title: "Reveal on Scroll",
tags: ["scroll", "reveal", "timeline"],
description:
"A vertical timeline whose entries fade, rise and un-rotate into place as they cross into view — each one self-timed by a per-element view() timeline, so they stagger naturally. A slim rail on the side tracks overall scroll progress. No JS.",
tech: ["animation-timeline: view()", "animation-range: entry", "scroll() progress"],
height: 600,
};
const items = [
{ time: "0.0s", title: "Signal acquired", body: "A single pointer event enters the pipeline and is stamped with a monotonic clock." },
{ time: "0.3s", title: "Intent resolved", body: "Gestures are classified against the last few frames of motion history." },
{ time: "0.9s", title: "State reconciled", body: "The optimistic update is merged and broadcast to every connected peer." },
{ time: "1.4s", title: "Layout committed", body: "Only the dirty region repaints; everything else stays on the compositor." },
{ time: "2.1s", title: "Motion settled", body: "Springs relax to rest and the frame budget is handed back to the browser." },
{ time: "2.8s", title: "Idle reached", body: "Work drains, listeners quiesce, and the surface waits for the next signal." },
];
---
<section class="reveal">
<div class="reveal__rail" aria-hidden="true">
<span class="reveal__rail-fill"></span>
</div>
<div class="reveal__body">
<header class="reveal__intro">
<p class="reveal__eyebrow">Frame lifecycle</p>
<h2>Everything, in order.</h2>
</header>
<ol class="reveal__list">
{items.map((it) => (
<li class="reveal__item">
<span class="reveal__dot" aria-hidden="true"></span>
<span class="reveal__time">{it.time}</span>
<div class="reveal__card">
<h3>{it.title}</h3>
<p>{it.body}</p>
</div>
</li>
))}
</ol>
</div>
</section>
<style>
.reveal {
position: relative;
display: grid;
grid-template-columns: auto 1fr;
gap: clamp(1rem, 3vw, 2rem);
padding: clamp(2rem, 6vw, 4rem) clamp(1.25rem, 5vw, 3.5rem);
background: var(--bg, #0e0f1a);
color: var(--ink, #f2f2f7);
}
/* progress rail */
.reveal__rail {
position: sticky;
top: clamp(2rem, 6vw, 4rem);
align-self: start;
width: 4px;
height: clamp(14rem, 60vh, 24rem);
border-radius: var(--radius-round, 999px);
background: var(--border, rgba(255,255,255,0.12));
overflow: hidden;
}
.reveal__rail-fill {
display: block;
width: 100%;
height: 100%;
transform-origin: top center;
transform: scaleY(0);
border-radius: inherit;
background: linear-gradient(
var(--spectrum-1, #8b5cf6),
var(--spectrum-3, #45d3e8),
var(--spectrum-5, #fbbf24)
);
}
.reveal__body { min-width: 0; }
.reveal__intro { margin-bottom: clamp(1.5rem, 5vw, 3rem); }
.reveal__eyebrow {
margin: 0 0 0.4rem;
font-family: var(--font-mono, monospace);
font-size: var(--step--1, 0.85rem);
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--spectrum-3, #45d3e8);
}
.reveal__intro h2 {
margin: 0;
font-size: var(--step-5, 2.8rem);
line-height: 1.03;
letter-spacing: -0.03em;
text-wrap: balance;
}
.reveal__list {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: clamp(1.25rem, 3vw, 2rem);
}
.reveal__item {
position: relative;
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
gap: 0.75rem 1rem;
padding-left: 1.25rem;
}
/* connecting line */
.reveal__item::before {
content: "";
position: absolute;
left: 0.28rem;
top: 0.9rem;
bottom: -1.4rem;
width: 2px;
background: linear-gradient(var(--border-strong, rgba(255,255,255,0.2)), transparent);
}
.reveal__item:last-child::before { display: none; }
.reveal__dot {
grid-row: 1;
width: 0.7rem;
height: 0.7rem;
margin-top: 0.35rem;
border-radius: 50%;
background: var(--spectrum-3, #45d3e8);
box-shadow: 0 0 0 4px color-mix(in oklab, var(--spectrum-3, #45d3e8) 22%, transparent);
}
.reveal__time {
grid-row: 1;
font-family: var(--font-mono, monospace);
font-size: var(--step--1, 0.85rem);
color: var(--ink-faint, #8a8c9e);
padding-top: 0.15rem;
}
.reveal__card {
grid-column: 2;
background: var(--surface, #14151f);
border: 1px solid var(--border, rgba(255,255,255,0.1));
border-radius: var(--radius-m, 14px);
padding: clamp(0.9rem, 2vw, 1.35rem);
box-shadow: var(--shadow-m, 0 8px 24px rgba(0,0,0,0.25));
}
.reveal__card h3 {
margin: 0 0 0.35rem;
font-size: var(--step-1, 1.3rem);
letter-spacing: -0.02em;
}
.reveal__card p {
margin: 0;
line-height: 1.55;
color: var(--ink-muted, #b7b9c9);
}
/* ---- Enhanced: scroll-driven reveal + progress ---- */
@supports (animation-timeline: view()) {
.reveal__item {
animation: reveal-in linear both;
animation-timeline: view();
animation-range: entry 5% entry 60%;
}
.reveal__rail-fill {
animation: reveal-progress linear both;
animation-timeline: scroll(nearest block);
}
}
@keyframes reveal-in {
from {
opacity: 0;
transform: translateY(42px) rotate(-2.5deg) scale(0.97);
filter: blur(4px);
}
to {
opacity: 1;
transform: none;
filter: blur(0);
}
}
@keyframes reveal-progress {
from { transform: scaleY(0); }
to { transform: scaleY(1); }
}
@media (prefers-reduced-motion: reduce) {
@supports (animation-timeline: view()) {
.reveal__item { animation: none; opacity: 1; transform: none; filter: none; }
.reveal__rail-fill { animation: none; transform: scaleY(1); }
}
}
</style>