32 CSS Timelines06 / 32
Scroll-Driven Reveal Timeline — animation-timeline: view() Entrances
The scroll-driven flagship: milestones fade-and-rise into place and their markers pop exactly as they scroll into view — driven entirely by the CSS animation-timeline: view() API, zero IntersectionObserver, zero JavaScript. Scrub the page up and down and the entrances play forwards and backwards like a scrubbing video, because the scrollbar IS the timeline.
Published
The code
<section class="tls-06" aria-label="Scroll-driven reveal timeline demo">
<div class="tls-06__inner">
<p class="tls-06__eyebrow">Scroll to reveal — then scroll back up</p>
<h3 class="tls-06__title">Product journey</h3>
<ol class="tls-06__list" role="list">
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-03">March 2024</time>
<h4 class="tls-06__h">First prototype</h4>
<p class="tls-06__p">A weekend build shared with 12 friends. Eight replied the same day.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-09">September 2024</time>
<h4 class="tls-06__h">Private beta</h4>
<p class="tls-06__p">400 teams on a waitlist. The onboarding survived all of them.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-02">February 2025</time>
<h4 class="tls-06__h">Public launch</h4>
<p class="tls-06__p">#1 on launch day. Servers held. Founders did not sleep.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-11">November 2025</time>
<h4 class="tls-06__h">10,000 customers</h4>
<p class="tls-06__p">Profitability, a team of nine, and the roadmap the users wrote.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2026-06">June 2026</time>
<h4 class="tls-06__h">Platform v2</h4>
<p class="tls-06__p">The rewrite nobody noticed — which was exactly the point.</p>
</div>
</li>
</ol>
</div>
</section><section class="tls-06" aria-label="Scroll-driven reveal timeline demo">
<div class="tls-06__inner">
<p class="tls-06__eyebrow">Scroll to reveal — then scroll back up</p>
<h3 class="tls-06__title">Product journey</h3>
<ol class="tls-06__list" role="list">
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-03">March 2024</time>
<h4 class="tls-06__h">First prototype</h4>
<p class="tls-06__p">A weekend build shared with 12 friends. Eight replied the same day.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-09">September 2024</time>
<h4 class="tls-06__h">Private beta</h4>
<p class="tls-06__p">400 teams on a waitlist. The onboarding survived all of them.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-02">February 2025</time>
<h4 class="tls-06__h">Public launch</h4>
<p class="tls-06__p">#1 on launch day. Servers held. Founders did not sleep.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-11">November 2025</time>
<h4 class="tls-06__h">10,000 customers</h4>
<p class="tls-06__p">Profitability, a team of nine, and the roadmap the users wrote.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2026-06">June 2026</time>
<h4 class="tls-06__h">Platform v2</h4>
<p class="tls-06__p">The rewrite nobody noticed — which was exactly the point.</p>
</div>
</li>
</ol>
</div>
</section>.tls-06,
.tls-06 *,
.tls-06 *::before,
.tls-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-06 {
min-height: 100vh;
width: 100%;
--tl-accent: #6d28d9;
--tl-line: #e4e0ee;
--tl-bg: #faf9fc;
--tl-ink: #241f35;
--tl-mut: #7a7490;
font-family: 'Segoe UI',system-ui,sans-serif;
background: var(--tl-bg);
padding: 72px 24px 96px;
display: grid;
place-items: center;
}
.tls-06__inner {
width: min(560px,100%);
}
.tls-06__eyebrow {
font-size: 11px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tl-accent);
margin-bottom: 8px;
}
.tls-06__title {
font-size: clamp(24px,4vw,32px);
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.02em;
margin-bottom: 36px;
}
.tls-06__list {
list-style: none;
display: flex;
flex-direction: column;
}
.tls-06__item {
position: relative;
padding: 0 0 40px 40px;
}
.tls-06__item::after {
content: '';
position: absolute;
left: 7px;
top: 26px;
bottom: -8px;
width: 2px;
background: var(--tl-line);
}
.tls-06__item:last-child::after {
display: none;
}
.tls-06__item::before {
content: '';
position: absolute;
left: 0;
top: 10px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--tl-bg);
border: 4px solid var(--tl-accent);
z-index: 1;
}
.tls-06__date {
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--tl-accent);
}
.tls-06__h {
font-size: 18px;
font-weight: 750;
color: var(--tl-ink);
letter-spacing: -.01em;
margin: 6px 0 6px;
}
.tls-06__p {
font-size: 14px;
line-height: 1.6;
color: var(--tl-mut);
text-wrap: pretty;
}
@supports (animation-timeline: view()) {
.tls-06__card {
animation: tls-06-rise linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
.tls-06__item::before {
animation: tls-06-pop linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
@keyframes tls-06-rise {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes tls-06-pop {
0% {
transform: scale(0);
}
60% {
transform: scale(1.35);
}
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.tls-06__card,
.tls-06__item::before {
animation: none;
}
}
}.tls-06,
.tls-06 *,
.tls-06 *::before,
.tls-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-06 {
min-height: 100vh;
width: 100%;
--tl-accent: #6d28d9;
--tl-line: #e4e0ee;
--tl-bg: #faf9fc;
--tl-ink: #241f35;
--tl-mut: #7a7490;
font-family: 'Segoe UI',system-ui,sans-serif;
background: var(--tl-bg);
padding: 72px 24px 96px;
display: grid;
place-items: center;
}
.tls-06__inner {
width: min(560px,100%);
}
.tls-06__eyebrow {
font-size: 11px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tl-accent);
margin-bottom: 8px;
}
.tls-06__title {
font-size: clamp(24px,4vw,32px);
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.02em;
margin-bottom: 36px;
}
.tls-06__list {
list-style: none;
display: flex;
flex-direction: column;
}
.tls-06__item {
position: relative;
padding: 0 0 40px 40px;
}
.tls-06__item::after {
content: '';
position: absolute;
left: 7px;
top: 26px;
bottom: -8px;
width: 2px;
background: var(--tl-line);
}
.tls-06__item:last-child::after {
display: none;
}
.tls-06__item::before {
content: '';
position: absolute;
left: 0;
top: 10px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--tl-bg);
border: 4px solid var(--tl-accent);
z-index: 1;
}
.tls-06__date {
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--tl-accent);
}
.tls-06__h {
font-size: 18px;
font-weight: 750;
color: var(--tl-ink);
letter-spacing: -.01em;
margin: 6px 0 6px;
}
.tls-06__p {
font-size: 14px;
line-height: 1.6;
color: var(--tl-mut);
text-wrap: pretty;
}
@supports (animation-timeline: view()) {
.tls-06__card {
animation: tls-06-rise linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
.tls-06__item::before {
animation: tls-06-pop linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
@keyframes tls-06-rise {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes tls-06-pop {
0% {
transform: scale(0);
}
60% {
transform: scale(1.35);
}
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.tls-06__card,
.tls-06__item::before {
animation: none;
}
}
}/* No JavaScript — entrances are animation-timeline: view() with animation-range: entry 0% entry 65%; the @supports gate owns the hidden start state so unsupported browsers see the finished layout. */
/* No JavaScript — entrances are animation-timeline: view() with animation-range: entry 0% entry 65%; the @supports gate owns the hidden start state so unsupported browsers see the finished layout. */Here's a working CSS Timeline 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: Scroll-Driven Reveal Timeline — animation-timeline: view() Entrances
Source: https://codefronts.com/layouts/css-timelines/scroll-driven-reveal-timeline-animation-timeline-view-entrances/
The scroll-driven flagship: milestones fade-and-rise into place and their markers pop exactly as they scroll into view — driven entirely by the CSS animation-timeline: view() API, zero IntersectionObserver, zero JavaScript. Scrub the page up and down and the entrances play forwards and backwards like a scrubbing video, because the scrollbar IS the timeline.
## HTML
```html
<section class="tls-06" aria-label="Scroll-driven reveal timeline demo">
<div class="tls-06__inner">
<p class="tls-06__eyebrow">Scroll to reveal — then scroll back up</p>
<h3 class="tls-06__title">Product journey</h3>
<ol class="tls-06__list" role="list">
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-03">March 2024</time>
<h4 class="tls-06__h">First prototype</h4>
<p class="tls-06__p">A weekend build shared with 12 friends. Eight replied the same day.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-09">September 2024</time>
<h4 class="tls-06__h">Private beta</h4>
<p class="tls-06__p">400 teams on a waitlist. The onboarding survived all of them.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-02">February 2025</time>
<h4 class="tls-06__h">Public launch</h4>
<p class="tls-06__p">#1 on launch day. Servers held. Founders did not sleep.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-11">November 2025</time>
<h4 class="tls-06__h">10,000 customers</h4>
<p class="tls-06__p">Profitability, a team of nine, and the roadmap the users wrote.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2026-06">June 2026</time>
<h4 class="tls-06__h">Platform v2</h4>
<p class="tls-06__p">The rewrite nobody noticed — which was exactly the point.</p>
</div>
</li>
</ol>
</div>
</section>
```
## CSS
```css
.tls-06,
.tls-06 *,
.tls-06 *::before,
.tls-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-06 {
min-height: 100vh;
width: 100%;
--tl-accent: #6d28d9;
--tl-line: #e4e0ee;
--tl-bg: #faf9fc;
--tl-ink: #241f35;
--tl-mut: #7a7490;
font-family: 'Segoe UI',system-ui,sans-serif;
background: var(--tl-bg);
padding: 72px 24px 96px;
display: grid;
place-items: center;
}
.tls-06__inner {
width: min(560px,100%);
}
.tls-06__eyebrow {
font-size: 11px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tl-accent);
margin-bottom: 8px;
}
.tls-06__title {
font-size: clamp(24px,4vw,32px);
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.02em;
margin-bottom: 36px;
}
.tls-06__list {
list-style: none;
display: flex;
flex-direction: column;
}
.tls-06__item {
position: relative;
padding: 0 0 40px 40px;
}
.tls-06__item::after {
content: '';
position: absolute;
left: 7px;
top: 26px;
bottom: -8px;
width: 2px;
background: var(--tl-line);
}
.tls-06__item:last-child::after {
display: none;
}
.tls-06__item::before {
content: '';
position: absolute;
left: 0;
top: 10px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--tl-bg);
border: 4px solid var(--tl-accent);
z-index: 1;
}
.tls-06__date {
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--tl-accent);
}
.tls-06__h {
font-size: 18px;
font-weight: 750;
color: var(--tl-ink);
letter-spacing: -.01em;
margin: 6px 0 6px;
}
.tls-06__p {
font-size: 14px;
line-height: 1.6;
color: var(--tl-mut);
text-wrap: pretty;
}
@supports (animation-timeline: view()) {
.tls-06__card {
animation: tls-06-rise linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
.tls-06__item::before {
animation: tls-06-pop linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
@keyframes tls-06-rise {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes tls-06-pop {
0% {
transform: scale(0);
}
60% {
transform: scale(1.35);
}
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.tls-06__card,
.tls-06__item::before {
animation: none;
}
}
}
```
## JavaScript
```js
/* No JavaScript — entrances are animation-timeline: view() with animation-range: entry 0% entry 65%; the @supports gate owns the hidden start state so unsupported browsers see the finished layout. */
```Here's a working CSS Timeline 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: Scroll-Driven Reveal Timeline — animation-timeline: view() Entrances
Source: https://codefronts.com/layouts/css-timelines/scroll-driven-reveal-timeline-animation-timeline-view-entrances/
The scroll-driven flagship: milestones fade-and-rise into place and their markers pop exactly as they scroll into view — driven entirely by the CSS animation-timeline: view() API, zero IntersectionObserver, zero JavaScript. Scrub the page up and down and the entrances play forwards and backwards like a scrubbing video, because the scrollbar IS the timeline.
## HTML
```html
<section class="tls-06" aria-label="Scroll-driven reveal timeline demo">
<div class="tls-06__inner">
<p class="tls-06__eyebrow">Scroll to reveal — then scroll back up</p>
<h3 class="tls-06__title">Product journey</h3>
<ol class="tls-06__list" role="list">
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-03">March 2024</time>
<h4 class="tls-06__h">First prototype</h4>
<p class="tls-06__p">A weekend build shared with 12 friends. Eight replied the same day.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2024-09">September 2024</time>
<h4 class="tls-06__h">Private beta</h4>
<p class="tls-06__p">400 teams on a waitlist. The onboarding survived all of them.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-02">February 2025</time>
<h4 class="tls-06__h">Public launch</h4>
<p class="tls-06__p">#1 on launch day. Servers held. Founders did not sleep.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2025-11">November 2025</time>
<h4 class="tls-06__h">10,000 customers</h4>
<p class="tls-06__p">Profitability, a team of nine, and the roadmap the users wrote.</p>
</div>
</li>
<li class="tls-06__item">
<div class="tls-06__card">
<time class="tls-06__date" datetime="2026-06">June 2026</time>
<h4 class="tls-06__h">Platform v2</h4>
<p class="tls-06__p">The rewrite nobody noticed — which was exactly the point.</p>
</div>
</li>
</ol>
</div>
</section>
```
## CSS
```css
.tls-06,
.tls-06 *,
.tls-06 *::before,
.tls-06 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.tls-06 {
min-height: 100vh;
width: 100%;
--tl-accent: #6d28d9;
--tl-line: #e4e0ee;
--tl-bg: #faf9fc;
--tl-ink: #241f35;
--tl-mut: #7a7490;
font-family: 'Segoe UI',system-ui,sans-serif;
background: var(--tl-bg);
padding: 72px 24px 96px;
display: grid;
place-items: center;
}
.tls-06__inner {
width: min(560px,100%);
}
.tls-06__eyebrow {
font-size: 11px;
font-weight: 700;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--tl-accent);
margin-bottom: 8px;
}
.tls-06__title {
font-size: clamp(24px,4vw,32px);
font-weight: 800;
color: var(--tl-ink);
letter-spacing: -.02em;
margin-bottom: 36px;
}
.tls-06__list {
list-style: none;
display: flex;
flex-direction: column;
}
.tls-06__item {
position: relative;
padding: 0 0 40px 40px;
}
.tls-06__item::after {
content: '';
position: absolute;
left: 7px;
top: 26px;
bottom: -8px;
width: 2px;
background: var(--tl-line);
}
.tls-06__item:last-child::after {
display: none;
}
.tls-06__item::before {
content: '';
position: absolute;
left: 0;
top: 10px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--tl-bg);
border: 4px solid var(--tl-accent);
z-index: 1;
}
.tls-06__date {
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
color: var(--tl-accent);
}
.tls-06__h {
font-size: 18px;
font-weight: 750;
color: var(--tl-ink);
letter-spacing: -.01em;
margin: 6px 0 6px;
}
.tls-06__p {
font-size: 14px;
line-height: 1.6;
color: var(--tl-mut);
text-wrap: pretty;
}
@supports (animation-timeline: view()) {
.tls-06__card {
animation: tls-06-rise linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
.tls-06__item::before {
animation: tls-06-pop linear both;
animation-timeline: view();
animation-range: entry 0% entry 65%;
}
@keyframes tls-06-rise {
from {
opacity: 0;
transform: translateY(24px);
}
to {
opacity: 1;
transform: none;
}
}
@keyframes tls-06-pop {
0% {
transform: scale(0);
}
60% {
transform: scale(1.35);
}
100% {
transform: scale(1);
}
}
@media (prefers-reduced-motion: reduce) {
.tls-06__card,
.tls-06__item::before {
animation: none;
}
}
}
```
## JavaScript
```js
/* No JavaScript — entrances are animation-timeline: view() with animation-range: entry 0% entry 65%; the @supports gate owns the hidden start state so unsupported browsers see the finished layout. */
```How this works
Each <li> carries a plain keyframe animation (opacity 0→1, translateY(24px)→0) but instead of a duration it declares animation-timeline: view() — the animation's progress is mapped to the item's journey through the viewport, not to a clock. animation-range: entry 0% entry 65% confines the whole effect to the first 65% of the element's entry, so items are fully settled well before they reach reading position.
The markers run a SECOND view-timeline animation on the same element's ::before — a scale pop with an overshoot keyframe at 60% — proving that multiple scroll-driven animations can ride one scroller. Because view() progress is bidirectional, scrolling back up reverses everything for free; the page feels scrubbed rather than triggered, which is precisely what JS observer implementations struggle to fake.
The entire block sits inside @supports (animation-timeline: view()), so browsers without the API simply show the finished layout — content is never hidden behind an entrance that can't play. And because opacity:0 only ever appears INSIDE the @supports gate, there is no flash-of-invisible-content anywhere. A prefers-reduced-motion block inside the gate switches the animations off again for motion-sensitive users.
Techniques used: animation-timeline: view() scroll-driven entrances · animation-range: entry clamping · dual scroll-driven animations per item (content + ::before marker) · @supports progressive enhancement (opacity:0 only inside the gate) · prefers-reduced-motion override inside the gate · semantic ol/li + time[datetime] rail
Make it yours
- Settle point: the
entry 65%range end — lower values (40%) finish sooner;cover 50%makes items animate until mid-viewport. - Rise distance: the 24px in the keyframe; 12px whispers, 48px announces.
- Marker pop: the 1.35 overshoot at 60% — set to 1 for a strict fade.
- Stagger illusion: give even items
animation-range: entry 0% entry 80%so columns settle at different scroll depths.
Gotchas — read before shipping
- NEVER put opacity:0 on items outside the @supports gate — unsupported browsers would show a blank page forever. The gate must own both the hidden start state and the animation.
- animation-range needs the same gate as animation-timeline — old Safari parses one but not the other and items freeze mid-entrance.
- view() tracks the nearest SCROLLER: if a parent gains overflow:hidden for a border-radius clip, the timeline silently dies — use overflow:clip for clipping instead.
- Keep entrance keyframes to transform/opacity only; animating height or margin in a scroll-driven animation causes layout thrash on every scroll frame.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 18.2+ (partial 17.4) | 128+ (flag; 2025 stable) | 115+ |
animation-timeline is Baseline newly-available 2025. The @supports gate means every other browser gets the identical timeline, statically rendered — nothing is lost.