# Editorial Scrollytelling

> A sticky editorial media rail that morphs between section-defined aspect ratios with Flip, directional image wipes, layered drift and a velocity-reactive settle while the prose stays in normal document flow.

Canonical: https://gsapvault.com/effects/editorial-scrollytelling
Live demo: https://gsapvault.com/demos/editorial-scrollytelling/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £5 |
| Difficulty | advanced |
| Plugins | ScrollTrigger, Flip |
| Techniques | flip-layout, sticky-scroll-story, mask-reveal, velocity |
| Uses Lenis | Yes |

## Overview

A reusable editorial scrollytelling effect built around a media handoff rather than a crossfade. The newsroom typography, palette and article copy belong to the demo and can be replaced; the product is the sticky media rail, shape morph, directional wipe and layered settle attached through documented data hooks. The prose remains in ordinary document flow, so a section can carry several real paragraphs rather than a single caption.

When the next section takes over, the picture panel does not fade. Every section declares a different shape, and GSAP Flip morphs the panel's real width and height from the outgoing shape into the incoming one, so the photograph reflows into the new frame instead of being stretched into it. The demo opens on a 21:9 panorama and steps through portrait, 3:2 and square, so the first handoff a reader sees is the largest change the effect makes.

The morph overshoots its target and relaxes into it, the new photograph wipes in behind a moving mask whose direction follows the scroll direction, and the section markers move on the frame the shape lands. Scrolling back reverses all three.

Nothing is pinned and nothing is clipped, so the article is readable at any resting scroll position, prints, and survives with no JavaScript at all. Scroll velocity adds a clamped skew and stretch to the panel that springs back out through gsap.quickTo. Section count is read from the DOM, so adding or removing a section re-fits the article on refresh.

## Features

- A sticky media rail beside prose in ordinary document flow, holding each photograph for however long its section takes to read
- Shape-to-shape handoffs with Flip animating real width and height, so each photograph reflows into its new crop instead of stretching
- Direction-aware mask wipes, a moving accent edge and a slower image drift that settles after the frame for a layered transition
- One replaceable image, picture or video slot per section, with six aspect-ratio presets and support for custom CSS-defined shapes
- One ScrollTrigger per section with function-based handoff points, so variable-length chapters and scroll reversals need no fixed timeline
- Velocity-reactive skew, stretch and lag on fine pointers, clamped and released through gsap.quickTo with an elastic settle
- A stacked mobile composition with the same shape morph, plus keyboard-operable section controls and an aria-live announcement
- Complete reduced-motion and no-JavaScript fallbacks that preserve every photograph and paragraph in readable document order

## Use Cases

- News features and long-reads where each beat is carried by a photograph in a different crop, opening on a panorama
- Brand and magazine storytelling that needs several paragraphs per image rather than a caption
- Case studies and annual reports where the supporting image changes format from section to section
- Documentary or campaign pages built around a commissioned photography set
- Editorial product stories where the writing has to stay readable, printable and indexable

## Vibe-Code Ready Setup

This effect includes `START-HERE-AI.md`, a product-specific copy-paste setup prompt for Cursor, Claude Code, ChatGPT, GitHub Copilot, Windsurf, and other coding assistants. It tells the assistant to inspect the existing stack, integrate the supplied files, preserve the design, scope selectors, retain accessibility and responsive behaviour, add framework-appropriate GSAP cleanup, and report what it tested.

[How AI-assisted setup works](https://gsapvault.com/vibe-coding)

## How It Works

The article is ordinary document flow and the picture column is a `position: sticky` block beside it, so the photograph holds for exactly as long as its section's prose takes to pass. Nothing is pinned, nothing is translated and nothing is clipped, which is what allows several paragraphs per section and what makes the page correct with no JavaScript at all.

One ScrollTrigger per section spans that section's own prose, starting and ending at a handover line measured from the viewport. A section takes the picture over when its first line crosses that line going down and takes it back when its last line crosses it going up, so a reversal is the same code path as an advance, and a section that is four paragraphs long simply holds for longer. The line is a function value under invalidateOnRefresh, so a resize or a mobile address bar sliding away re-measures it rather than reusing a cached number.

On a change, Flip.getState records the panel's live rect, any half-finished morph is killed and its inline styles cleared, the new shape's data attribute is applied, and Flip.from animates the frame's width and height into place with a back.out ease. The incoming photograph layer starts fully clipped from the side the scroll came from and tweens its clip-path open beneath the accent edge, while the image itself drifts in from the same direction and settles just after the frame. That layered timing is what makes the handoff feel composed, and a reversal wipes and drifts back rather than replaying forwards.

Nothing is cached in pixels. The stuck column is a size container and each shape's height is min(a share of that column's height, the height at which that shape would exactly fill its width), so every aspect ratio stays true at any window size. Scroll velocity from getVelocity is clamped, mapped to degrees and pushed through gsap.quickTo with a back.out ease, driving a skew, a scale stretch and a lag on the panel from one number; a ticker watchdog guarantees the release, since scroll events stop firing the moment the page settles.

## Integration Preview

How this effect integrates into a page. The full documentation (examples, events, programmatic API, customization guide) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`:**

```html
<link rel="stylesheet" href="path/to/style.css">
```

**2. Add the section to your `<body>`:**

```html
<section class="rail" data-rail aria-labelledby="rail-title">

  <header class="rail__head">
    <h2 class="rail__title" id="rail-title">The city that clocks on at midnight</h2>
  </header>

  <div class="rail__cols">
    <!-- The picture column. It sticks. It starts empty: the script moves
         each section's media into the frame. -->
    <div class="rail__sticky">
      <div class="rail__stage" data-rail-stage>
        <div class="rail__frame" data-rail-frame>
          <div class="rail__canvas" data-rail-canvas></div>
          <span class="rail__wipe" data-rail-wipe aria-hidden="true"></span>
        </div>
      </div>
      <!-- Optional. The script fills this with one button per section. -->
      <div class="rail__marks" data-rail-marks role="group" aria-label="Article sections"></div>
    </div>

    <div class="rail__chapters">

      <article class="chapter" data-rail-chapter>
        <div class="media" data-rail-media data-rail-shape="wide">
          <img src="lead.jpg" alt="What the picture shows">
        </div>
        <div class="chapter__copy">
          <h3 class="chapter__title">Half past one, and the city changes hands</h3>
          <p class="chapter__text chapter__text--lead">The lead paragraph.</p>
          <p class="chapter__text">As many more as the section needs.</p>
        </div>
      </article>

      <!-- repeat for each section, each with a different data-rail-shape -->

    </div>
  </div>

  <p class="sr-only" aria-live="polite" data-rail-status></p>
</section>
```

**3. Add before the closing `</body>` tag:**

```html
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/Flip.min.js"></script>
<script src="path/to/script.js"></script>
```

The script finds every `[data-rail]` section, counts the sections in the DOM, moves each section's media into the frame, and morphs the frame from shape to shape as each section reaches the handover line. The picture column does the sticking itself, in CSS.

### Options

All set on the `[data-rail]` section, except `data-rail-shape`, which goes on each media slot.

| Attribute | Values | Default | Description |
|---|---|---|---|
| `data-rail-shape` | `portrait` (3:4), `landscape` (16:9), `classic` (3:2), `split` (4:3), `square` (1:1), `wide` (21:9) | `landscape` | The shape this section's media gives the frame. An unknown value falls back to the frame's default size, so two sections in a row with the same shape produce no visible morph |
| `data-rail-line` | `0.15` to `0.85` | `0.5` | Where the handover happens, as a share of the viewport height from the top. A section takes the picture over when its first line crosses this line, so it wants to sit near the middle of the picture: much above `0.3` and the picture changes before the reader reaches the new heading, much below `0.7` and it changes long after |
| `data-rail-morph` | seconds | `0.72` | Length of one shape morph. Below about `0.35` the overshoot stops reading; above about `1.2` the panel visibly lags the copy |
| `data-rail-overshoot` | number | `1.45` | `back.out` strength on the morph. `0` removes the overshoot and the shape change reads mechanical; past about `2.5` the panel wobbles on arrival |
| `data-rail-smear` | `true`, `false` | `true` | Velocity smear on the panel. Always off on coarse pointers, whatever this says |
| `data-rail-smear-max` | degrees | `5` | Cap on the smear. Past about `10` the panel looks broken rather than fast |

Section count is never configured: it is counted from the DOM. Add or remove a `[data-rail-chapter]` and the markers, the announcements and the handover triggers all follow on the next `ScrollTrigger.refresh()`.

A few more knobs live in CSS rather than in data attributes, because they are proportions of the layout rather than settings the script reads:

| Property | Default | Description |
|---|---|---|
| `--stage-h` | `min(70svh, 40rem, calc(var(--content-w) * 0.56))` (`min(36svh, 15rem)` under 860px) | How tall the picture is allowed to be while it holds. Capped against the shell as well as the window, so a very wide window does not turn the picture into the page |
| `--stick-top` | `max(1.5rem, calc((100svh - var(--stage-h) - var(--marks-h)) / 2))` (`0` under 860px) | Where the stuck column parks. Derived from the heights above, so the picture is vertically centred in the window at any size. On a phone it is flush with the top of the window on purpose: a gap above a stuck picture is a letterbox the prose shows through, one clipped line at a time |
| media/copy column split | `0.95fr / 1fr` | How the pair's width is shared. The near-even split gives the long-read text roughly 80px more room in the 1200px product preview |
| `--measure` | `54ch` | The prose measure |

### Accessibility

- **Reduced motion:** the `gsap.matchMedia` branch builds nothing at all, and the section never enters live mode. The page stays what the markup is: the complete article, every section's prose and every photograph, in order, nothing stuck and nothing hidden.
- **No JavaScript:** identical, minus the jump buttons.
- **Keyboard:** the section markers are real `<button>` elements, tab-reachable, activated by Enter and Space. Their `:focus-visible` styling matches `:hover` and adds an outline. Each is a 24x24 target with a 12px mark drawn inside it, which clears the minimum target size a 12px button would fail.
- **Screen readers:** `[data-rail-status]` announces each section change using that section's own heading. Prose stays in normal heading and paragraph markup; the buttons carry the heading text in their labels.
- **Motion safety:** the velocity smear is capped in degrees and always released back to zero by a ticker watchdog, so it can never leave the panel skewed.
- **Read time:** the byline's read time is derived from the article's real word count at a stated words-per-minute and the derivation is written next to it in `index.html`. Recount it when you change the copy.

### Dependencies

**Required:**

- GSAP 3.12+
- ScrollTrigger
- Flip

**Optional:**

- Lenis 1.x for smooth scrolling. If present, the script runs Lenis from GSAP's ticker so both systems share one clock.

## What You Get

- `index.html`: working demo page
- `assets/script.js`: commented, readable source
- `assets/style.css`: effect styles
- `README.md`: full documentation with examples and framework integration notes
- `START-HERE-AI.md`: product-specific copy-paste prompt for AI-assisted setup
- `LICENSE.txt`: standard license terms
- Lifetime updates: re-download anytime from your library

## Get the Code

This is a premium effect. The standard license costs £5 one-time and covers unlimited personal and commercial projects with no attribution required. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Editorial Scrollytelling](https://gsapvault.com/effects/editorial-scrollytelling)
- [Effects & Templates Vault (£39 one-time, best value): every current and future effect and template](https://gsapvault.com/effects)

## Judge the Code Quality First

These related effects are free with complete source published, written to the same production standard (cleanup functions, reduced-motion support, framework-agnostic):

- [Scroll Progress Indicator](https://gsapvault.com/effects/scroll-progress.md): A precise GSAP reading-progress instrument with bar, ring, side rail, percentage, and active chapter feedback.
- [CSS Scroll Reveal](https://gsapvault.com/effects/css-scroll-reveal.md): Native CSS scroll-driven reveals for crisp fade, slide, and scale entrances with accessible static fallbacks and no animation JavaScript.
- [Parallax Hero](https://gsapvault.com/effects/parallax-hero.md): A pinned hero that separates its background photograph, copy, and foreground card into distinct scroll depths from a single scrubbed ScrollTrigger.

---

From [GSAP Vault](https://gsapvault.com): production-ready GSAP animation effects. Full catalog for agents: https://gsapvault.com/llms-full.txt
