11 CSS animation-timeline Demos08 / 11

Pure CSSMIT licensed

Split Column Reverse Scrolling

The 'expensive agency site' look: as the user scrolls down, the left editorial column rises normally while the right image column glides DOWN in the opposite direction — an asymmetric split-screen built from one negative translateY hooked to a scroll() timeline.

Published

Live Demo
Try it

The code

<section class="sda-08" aria-label="Split columns scrolling in opposite directions" role="region" tabindex="0">
  <div class="sda-08__cols">
    <div class="sda-08__left">
      <p class="sda-08__kicker">FW/26 Collection</p>
      <h2>Counter<br>current</h2>
      <p class="sda-08__copy">Scroll down. The words rise, the imagery sinks. Two flows, one gesture — the asymmetry is the message.</p>
      <p class="sda-08__copy">Under the hood it's a single negative <code>translateY</code> bound to the scroller's timeline: the column doesn't scroll backwards, it just out-runs the scroll in paint space.</p>
      <p class="sda-08__copy">No wheel hijack. No inverted wheel math. Native momentum, native accessibility — the DOM never moves, only pixels do.</p>
      <p class="sda-08__copy">Because the browser owns the mapping, fast flicks, rubber-banding and scroll-snap all keep working exactly as the OS intends.</p>
      <p class="sda-08__end">— fin —</p>
    </div>
    <div class="sda-08__right" aria-hidden="false">
      <div class="sda-08__strip">
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-a/460/560" width="460" height="560" alt="Model in oversized wool coat"><figcaption>Look 01 — Wool, oversized</figcaption></figure>
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-b/460/560" width="460" height="560" alt="Detail of pleated fabric"><figcaption>Look 02 — Pleats, close</figcaption></figure>
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-c/460/560" width="460" height="560" alt="Silhouette against concrete"><figcaption>Look 03 — Concrete study</figcaption></figure>
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-d/460/560" width="460" height="560" alt="Layered knitwear texture"><figcaption>Look 04 — Layered knit</figcaption></figure>
      </div>
    </div>
  </div>
</section>
.sda-08,
.sda-08 *,
.sda-08 *::before,
.sda-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sda-08 {
  font-family: Georgia,'Times New Roman',serif;
  background: oklch(0.97 0.005 90);
  color: oklch(0.22 0.01 60);
  width: 100%;
  height: 100vh;
  overflow-y: auto;
}

.sda-08__cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(24px,5vw,72px);
  width: min(1020px,100%);
  margin: 0 auto;
  padding: 0 24px;
  overflow: clip;
}

.sda-08__left {
  padding: 16vh 0 30vh;
}

.sda-08__kicker {
  font: 700 11px 'Segoe UI',system-ui,sans-serif;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: oklch(0.5 0.1 40);
  margin-bottom: 20px;
}

.sda-08__left h2 {
  font-size: clamp(40px,6vw,72px);
  line-height: .98;
  letter-spacing: -.02em;
  font-weight: 400;
  font-style: italic;
  margin-bottom: 36px;
}

.sda-08__copy {
  font-size: 16.5px;
  line-height: 1.8;
  margin-bottom: 26px;
  max-width: 42ch;
  text-wrap: pretty;
}

.sda-08__copy code {
  font: 600 13.5px ui-monospace,Menlo,monospace;
  background: oklch(0.92 0.01 90);
  padding: 2px 6px;
  border-radius: 5px;
}

.sda-08__end {
  font-style: italic;
  color: oklch(0.55 0.02 60);
  padding-top: 8vh;
}

.sda-08__right {
  position: relative;
}

.sda-08__strip {
  display: flex;
  flex-direction: column;
  gap: 28px;
  padding: 6vh 0;
}

.sda-08__fig img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  filter: saturate(.85);
}

.sda-08__fig figcaption {
  font: italic 13px Georgia,serif;
  color: oklch(0.5 0.02 60);
  padding-top: 8px;
}

.sda-08:focus-visible {
  outline: 3px solid oklch(0.5 0.1 40);
  outline-offset: -3px;
}

@supports (animation-timeline: scroll()) {
  .sda-08__strip {
    transform: translateY(-42%);
    animation: sda-08-sink linear both;
    animation-timeline: scroll();
  }
}

@keyframes sda-08-sink {
  from {
    transform: translateY(-42%);
  }

  to {
    transform: translateY(0);
  }
}

@media (max-width: 640px) {
  .sda-08__cols {
    grid-template-columns: 1fr;
  }

  .sda-08__strip {
    transform: none;
    animation: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sda-08__strip {
    transform: none;
    animation: none;
  }
}
/* No JavaScript — the image strip is pre-translated up and animated back to 0 on a scroll() timeline; scroll moves it up, the animation pushes it down, and the net motion reads as reverse scrolling. */
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: Split Column Reverse Scrolling
Source: https://codefronts.com/motion/css-animation-timeline/split-column-reverse-scrolling/

The 'expensive agency site' look: as the user scrolls down, the left editorial column rises normally while the right image column glides DOWN in the opposite direction — an asymmetric split-screen built from one negative translateY hooked to a scroll() timeline.
## HTML
```html
<section class="sda-08" aria-label="Split columns scrolling in opposite directions" role="region" tabindex="0">
  <div class="sda-08__cols">
    <div class="sda-08__left">
      <p class="sda-08__kicker">FW/26 Collection</p>
      <h2>Counter<br>current</h2>
      <p class="sda-08__copy">Scroll down. The words rise, the imagery sinks. Two flows, one gesture — the asymmetry is the message.</p>
      <p class="sda-08__copy">Under the hood it's a single negative <code>translateY</code> bound to the scroller's timeline: the column doesn't scroll backwards, it just out-runs the scroll in paint space.</p>
      <p class="sda-08__copy">No wheel hijack. No inverted wheel math. Native momentum, native accessibility — the DOM never moves, only pixels do.</p>
      <p class="sda-08__copy">Because the browser owns the mapping, fast flicks, rubber-banding and scroll-snap all keep working exactly as the OS intends.</p>
      <p class="sda-08__end">— fin —</p>
    </div>
    <div class="sda-08__right" aria-hidden="false">
      <div class="sda-08__strip">
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-a/460/560" width="460" height="560" alt="Model in oversized wool coat"><figcaption>Look 01 — Wool, oversized</figcaption></figure>
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-b/460/560" width="460" height="560" alt="Detail of pleated fabric"><figcaption>Look 02 — Pleats, close</figcaption></figure>
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-c/460/560" width="460" height="560" alt="Silhouette against concrete"><figcaption>Look 03 — Concrete study</figcaption></figure>
        <figure class="sda-08__fig"><img src="https://picsum.photos/seed/rev-d/460/560" width="460" height="560" alt="Layered knitwear texture"><figcaption>Look 04 — Layered knit</figcaption></figure>
      </div>
    </div>
  </div>
</section>
```
## CSS
```css
.sda-08,
.sda-08 *,
.sda-08 *::before,
.sda-08 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.sda-08 {
  font-family: Georgia,'Times New Roman',serif;
  background: oklch(0.97 0.005 90);
  color: oklch(0.22 0.01 60);
  width: 100%;
  height: 100vh;
  overflow-y: auto;
}

.sda-08__cols {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: clamp(24px,5vw,72px);
  width: min(1020px,100%);
  margin: 0 auto;
  padding: 0 24px;
  overflow: clip;
}

.sda-08__left {
  padding: 16vh 0 30vh;
}

.sda-08__kicker {
  font: 700 11px 'Segoe UI',system-ui,sans-serif;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: oklch(0.5 0.1 40);
  margin-bottom: 20px;
}

.sda-08__left h2 {
  font-size: clamp(40px,6vw,72px);
  line-height: .98;
  letter-spacing: -.02em;
  font-weight: 400;
  font-style: italic;
  margin-bottom: 36px;
}

.sda-08__copy {
  font-size: 16.5px;
  line-height: 1.8;
  margin-bottom: 26px;
  max-width: 42ch;
  text-wrap: pretty;
}

.sda-08__copy code {
  font: 600 13.5px ui-monospace,Menlo,monospace;
  background: oklch(0.92 0.01 90);
  padding: 2px 6px;
  border-radius: 5px;
}

.sda-08__end {
  font-style: italic;
  color: oklch(0.55 0.02 60);
  padding-top: 8vh;
}

.sda-08__right {
  position: relative;
}

.sda-08__strip {
  display: flex;
  flex-direction: column;
  gap: 28px;
  padding: 6vh 0;
}

.sda-08__fig img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: 6px;
  filter: saturate(.85);
}

.sda-08__fig figcaption {
  font: italic 13px Georgia,serif;
  color: oklch(0.5 0.02 60);
  padding-top: 8px;
}

.sda-08:focus-visible {
  outline: 3px solid oklch(0.5 0.1 40);
  outline-offset: -3px;
}

@supports (animation-timeline: scroll()) {
  .sda-08__strip {
    transform: translateY(-42%);
    animation: sda-08-sink linear both;
    animation-timeline: scroll();
  }
}

@keyframes sda-08-sink {
  from {
    transform: translateY(-42%);
  }

  to {
    transform: translateY(0);
  }
}

@media (max-width: 640px) {
  .sda-08__cols {
    grid-template-columns: 1fr;
  }

  .sda-08__strip {
    transform: none;
    animation: none;
  }
}

@media (prefers-reduced-motion: reduce) {
  .sda-08__strip {
    transform: none;
    animation: none;
  }
}
```

## JavaScript
```js
/* No JavaScript — the image strip is pre-translated up and animated back to 0 on a scroll() timeline; scroll moves it up, the animation pushes it down, and the net motion reads as reverse scrolling. */
```

How this works

Both columns live in a normal two-column grid inside the scroller. The right column's inner strip is pre-translated upward (translateY(-50%) territory — here, its content is stacked so half sits above the fold) and animated back toward 0 on animation-timeline: scroll(). Since normal scrolling moves everything up while the animation pushes the strip down, the net visual velocity is negative: the column appears to scroll in reverse.

The exact end value is chosen so both columns 'run out' at the same moment — the animation distance equals the strip's overflow height. Tab order and reading order stay top-to-bottom in the DOM (screen readers and keyboards are unaffected — this is purely a paint-time transform). The fallback is a plain, respectable two-column layout.

Make it yours

  • Reverse-scroll speed = the translateY distance in sda-08-sink; larger distance reads faster.
  • Run THREE columns at alternating velocities for a full mosaic wall.
  • Add subtle rotate(0.001deg) to hint compositing if you see rasterization shimmer on old GPUs.
  • Give each figure its own view() fade for a layered double effect.

Gotchas — read before shipping

  • The moving strip must be taller than its column (overflow) or it exposes gaps as it sinks — the top offset pre-hides the excess.
  • Wrap the columns in overflow:clip so the translated strip never paints outside the section.
  • Keep interactive elements out of the reversed column or ensure they remain within the viewport long enough to be clickable.
  • This is decorative motion — the DOM order stays logical, so accessibility is untouched.

Browser support

ChromeSafariFirefoxEdge
115+26+pending (flag)115+

Fallback is a static two-column editorial grid — fully readable, no motion.

Techniques used in this demo

Search CodeFronts

Loading…