11 CSS animation-timeline Demos05 / 11

Pure CSSMIT licensed

Horizontal Scroll Section (Fake Horizontal Scroller)

The GSAP-ScrollTrigger showpiece, rebuilt in pure CSS: the user scrolls down normally, but inside a pinned full-viewport panel a wide gallery track glides horizontally — vertical progress through a tall wrapper is mapped 1:1 onto the track's translateX via a named view-timeline.

Published

Live Demo
Try it

The code

<section class="sda-05" aria-label="Horizontal scrolling gallery section" role="region" tabindex="0">
  <div class="sda-05__intro"><h2>Scroll down.<br>Travel sideways.</h2><p>A named view-timeline maps vertical progress onto horizontal motion.</p></div>
  <div class="sda-05__wrap">
    <div class="sda-05__pin">
      <div class="sda-05__track">
        <article class="sda-05__panel" style="--c:oklch(0.62 0.17 264)"><span>01</span><h3>Named timelines</h3><p>view-timeline: --sda-05-h on the tall wrapper broadcasts progress to every descendant.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.62 0.15 210)"><span>02</span><h3>Sticky pinning</h3><p>position:sticky holds this stage on screen while the wrapper scrolls past behind it.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.63 0.16 150)"><span>03</span><h3>One transform</h3><p>translateX(0 → -100% + 100vw): the end value self-adjusts to any number of panels.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.66 0.17 60)"><span>04</span><h3>No pin-spacers</h3><p>No resize listeners, no recalculation, no scroll hijack — momentum stays native.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.6 0.19 20)"><span>05</span><h3>Exit clean</h3><p>Past contain 100% the panel un-pins and normal vertical flow resumes.</p></article>
      </div>
      <div class="sda-05__dots" aria-hidden="true"><div class="sda-05__thumb"></div></div>
    </div>
  </div>
  <div class="sda-05__outro"><p>…and you're back to vertical. No hijack, no jank.</p></div>
</section>
.sda-05,
.sda-05 *,
.sda-05 *::before,
.sda-05 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sda-05 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: oklch(0.16 0.015 270);
  color: #eef0f8;
  width: 100%;
  height: 100vh;
  overflow-y: auto;
}

.sda-05__intro,
.sda-05__outro {
  min-height: 70vh;
  display: grid;
  place-content: center;
  text-align: center;
  gap: 14px;
  padding: 24px;
}

.sda-05__intro h2 {
  font-size: clamp(30px,5vw,52px);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -.025em;
}

.sda-05__intro p,
.sda-05__outro p {
  font-size: 16px;
  color: #9ca1bd;
}

.sda-05__wrap {
  height: 380vh;
  view-timeline: --sda-05-h block;
}

.sda-05__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.sda-05__track {
  display: flex;
  gap: 4vw;
  padding: 0 6vw;
  width: max-content;
}

.sda-05__panel {
  flex: none;
  width: min(560px,76vw);
  height: min(400px,62vh);
  border-radius: 26px;
  padding: 38px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 10px;
  background: linear-gradient(150deg,var(--c),oklch(from var(--c) calc(l - 0.2) c calc(h + 25)));
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
}

.sda-05__panel span {
  font: 800 15px ui-monospace,Menlo,monospace;
  letter-spacing: .2em;
  opacity: .65;
}

.sda-05__panel h3 {
  font-size: clamp(24px,3vw,34px);
  font-weight: 800;
  letter-spacing: -.02em;
}

.sda-05__panel p {
  font-size: 15.5px;
  line-height: 1.55;
  max-width: 44ch;
  opacity: .92;
  text-wrap: pretty;
}

.sda-05__dots {
  width: min(320px,60%);
  height: 4px;
  border-radius: 99px;
  background: rgba(255,255,255,.14);
  margin: 34px auto 0;
  display: none;
}

.sda-05__thumb {
  width: 25%;
  height: 100%;
  border-radius: 99px;
  background: #fff;
}

.sda-05:focus-visible {
  outline: 3px solid oklch(0.65 0.15 210);
  outline-offset: -3px;
}

@supports (animation-timeline: view()) {
  .sda-05__dots {
    display: block;
  }

  .sda-05__track {
    animation: sda-05-slide linear both;
    animation-timeline: --sda-05-h;
    animation-range: contain 0% contain 100%;
  }

  .sda-05__thumb {
    animation: sda-05-thumb linear both;
    animation-timeline: --sda-05-h;
    animation-range: contain 0% contain 100%;
  }
}

@supports not (animation-timeline: view()) {
  .sda-05__wrap {
    height: auto;
  }

  .sda-05__pin {
    position: static;
    height: auto;
    overflow-x: auto;
    padding: 40px 0;
    -webkit-overflow-scrolling: touch;
  }

  .sda-05__track {
    width: max-content;
  }
}

@keyframes sda-05-slide {
  to {
    transform: translateX(calc(-100% + 100vw));
  }
}

@keyframes sda-05-thumb {
  to {
    transform: translateX(300%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sda-05__track,
    .sda-05__thumb {
    animation-timing-function: steps(4);
  }
}
/* No JavaScript — a named view-timeline on the 380vh wrapper drives the pinned track's translateX; the end value calc(-100% + 100vw) self-adapts to any panel count. */
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 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: Horizontal Scroll Section (Fake Horizontal Scroller)
Source: https://codefronts.com/motion/css-animation-timeline/horizontal-scroll-section-fake-horizontal-scroller/

The GSAP-ScrollTrigger showpiece, rebuilt in pure CSS: the user scrolls down normally, but inside a pinned full-viewport panel a wide gallery track glides horizontally — vertical progress through a tall wrapper is mapped 1:1 onto the track's translateX via a named view-timeline.
## HTML
```html
<section class="sda-05" aria-label="Horizontal scrolling gallery section" role="region" tabindex="0">
  <div class="sda-05__intro"><h2>Scroll down.<br>Travel sideways.</h2><p>A named view-timeline maps vertical progress onto horizontal motion.</p></div>
  <div class="sda-05__wrap">
    <div class="sda-05__pin">
      <div class="sda-05__track">
        <article class="sda-05__panel" style="--c:oklch(0.62 0.17 264)"><span>01</span><h3>Named timelines</h3><p>view-timeline: --sda-05-h on the tall wrapper broadcasts progress to every descendant.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.62 0.15 210)"><span>02</span><h3>Sticky pinning</h3><p>position:sticky holds this stage on screen while the wrapper scrolls past behind it.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.63 0.16 150)"><span>03</span><h3>One transform</h3><p>translateX(0 → -100% + 100vw): the end value self-adjusts to any number of panels.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.66 0.17 60)"><span>04</span><h3>No pin-spacers</h3><p>No resize listeners, no recalculation, no scroll hijack — momentum stays native.</p></article>
        <article class="sda-05__panel" style="--c:oklch(0.6 0.19 20)"><span>05</span><h3>Exit clean</h3><p>Past contain 100% the panel un-pins and normal vertical flow resumes.</p></article>
      </div>
      <div class="sda-05__dots" aria-hidden="true"><div class="sda-05__thumb"></div></div>
    </div>
  </div>
  <div class="sda-05__outro"><p>…and you're back to vertical. No hijack, no jank.</p></div>
</section>
```
## CSS
```css
.sda-05,
.sda-05 *,
.sda-05 *::before,
.sda-05 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sda-05 {
  font-family: 'Segoe UI',system-ui,sans-serif;
  background: oklch(0.16 0.015 270);
  color: #eef0f8;
  width: 100%;
  height: 100vh;
  overflow-y: auto;
}

.sda-05__intro,
.sda-05__outro {
  min-height: 70vh;
  display: grid;
  place-content: center;
  text-align: center;
  gap: 14px;
  padding: 24px;
}

.sda-05__intro h2 {
  font-size: clamp(30px,5vw,52px);
  font-weight: 800;
  line-height: 1.1;
  letter-spacing: -.025em;
}

.sda-05__intro p,
.sda-05__outro p {
  font-size: 16px;
  color: #9ca1bd;
}

.sda-05__wrap {
  height: 380vh;
  view-timeline: --sda-05-h block;
}

.sda-05__pin {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.sda-05__track {
  display: flex;
  gap: 4vw;
  padding: 0 6vw;
  width: max-content;
}

.sda-05__panel {
  flex: none;
  width: min(560px,76vw);
  height: min(400px,62vh);
  border-radius: 26px;
  padding: 38px;
  display: flex;
  flex-direction: column;
  justify-content: flex-end;
  gap: 10px;
  background: linear-gradient(150deg,var(--c),oklch(from var(--c) calc(l - 0.2) c calc(h + 25)));
  box-shadow: 0 30px 60px -30px rgba(0,0,0,.7);
}

.sda-05__panel span {
  font: 800 15px ui-monospace,Menlo,monospace;
  letter-spacing: .2em;
  opacity: .65;
}

.sda-05__panel h3 {
  font-size: clamp(24px,3vw,34px);
  font-weight: 800;
  letter-spacing: -.02em;
}

.sda-05__panel p {
  font-size: 15.5px;
  line-height: 1.55;
  max-width: 44ch;
  opacity: .92;
  text-wrap: pretty;
}

.sda-05__dots {
  width: min(320px,60%);
  height: 4px;
  border-radius: 99px;
  background: rgba(255,255,255,.14);
  margin: 34px auto 0;
  display: none;
}

.sda-05__thumb {
  width: 25%;
  height: 100%;
  border-radius: 99px;
  background: #fff;
}

.sda-05:focus-visible {
  outline: 3px solid oklch(0.65 0.15 210);
  outline-offset: -3px;
}

@supports (animation-timeline: view()) {
  .sda-05__dots {
    display: block;
  }

  .sda-05__track {
    animation: sda-05-slide linear both;
    animation-timeline: --sda-05-h;
    animation-range: contain 0% contain 100%;
  }

  .sda-05__thumb {
    animation: sda-05-thumb linear both;
    animation-timeline: --sda-05-h;
    animation-range: contain 0% contain 100%;
  }
}

@supports not (animation-timeline: view()) {
  .sda-05__wrap {
    height: auto;
  }

  .sda-05__pin {
    position: static;
    height: auto;
    overflow-x: auto;
    padding: 40px 0;
    -webkit-overflow-scrolling: touch;
  }

  .sda-05__track {
    width: max-content;
  }
}

@keyframes sda-05-slide {
  to {
    transform: translateX(calc(-100% + 100vw));
  }
}

@keyframes sda-05-thumb {
  to {
    transform: translateX(300%);
  }
}

@media (prefers-reduced-motion: reduce) {
  .sda-05__track,
    .sda-05__thumb {
    animation-timing-function: steps(4);
  }
}
```

## JavaScript
```js
/* No JavaScript — a named view-timeline on the 380vh wrapper drives the pinned track's translateX; the end value calc(-100% + 100vw) self-adapts to any panel count. */
```

How this works

A tall wrapper (height:380vh) declares view-timeline: --sda-05-h block — a named timeline measuring the wrapper's progress through the scrollport. Inside it, a position:sticky; top:0; height:100vh panel pins to the screen for the wrapper's whole journey. The flex track inside the panel attaches animation-timeline: --sda-05-h (named timelines are visible to all descendants) with animation-range: contain 0% contain 100%, animating translateX(0 → calc(-100% + 100vw)).

The result: 280vh of vertical scroll becomes horizontal travel, perfectly scrubbed, reversible, and momentum-friendly — with none of ScrollTrigger's pin-spacers, resize recalculation, or bundle weight. A dot indicator rides the same timeline. Keyboard users can scroll the region natively (it's just scroll), and unsupported browsers get the honest fallback: the track becomes a normal horizontal overflow scroller.

Make it yours

  • More or fewer panels: the translateX(calc(-100% + 100vw)) end value adapts automatically to track width.
  • Trade scroll distance for speed via the wrapper's height (380vh → 250vh feels snappier).
  • Add per-panel parallax by giving children their own small translateX animations on the same named timeline.
  • Flip direction (RTL galleries) by animating from calc(-100% + 100vw) to 0.

Gotchas — read before shipping

  • The named timeline MUST be declared on the tall wrapper (the element whose view progress you want), not the sticky panel — a pinned element's own view progress is degenerate.
  • Use animation-range: contain so motion only runs while the panel is actually pinned.
  • Keep the sticky panel overflow:hidden or the track paints outside during the tween.
  • In the fallback, give the track real overflow-x:auto so content stays reachable on Firefox stable.

Browser support

ChromeSafariFirefoxEdge
115+26+pending (flag)115+

Fallback (gated with @supports not) converts the track into a native horizontal overflow scroller — all panels stay reachable everywhere.

Techniques used in this demo

Search CodeFronts

Loading…