32 CSS Timelines06 / 32

Pure CSSMIT licensed

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

Live Demo
Try it

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>
.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. */
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
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

ChromeSafariFirefoxEdge
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.

Techniques used in this demo

Search CodeFronts

Loading…