# Photo Reel Preloader

> A preloader made from the page's own photographs: a small frame flicks through a reel as the images load, a counter climbs to 100, then the last photograph grows out of the frame and becomes the full-bleed hero.

Canonical: https://gsapvault.com/effects/photo-reel-preloader
Live demo: https://gsapvault.com/demos/photo-reel-preloader/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £10 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | load-sequence, shared-element, count-up, clip-path, stagger |
| Uses Lenis | No |

## Lighthouse, as measured

Google Lighthouse on the demo, 26 September 2026. A measurement of the demo as shipped, not a promise for your page.

| Category | Score |
|----------|-------|
| Accessibility | 100 |
| Best practices | 100 |

No performance score is published: it depends on the deployment (server compression, caching, CDN, connection and device) rather than on the code, so measure it where it will live.

## Overview

Most preloaders are a number on a flat overlay followed by a curtain. This one is made from the photographs your page is already loading. A small portrait frame sits on a dark ground and flicks through a reel of images, each one arriving as it actually finishes downloading, while a tabular-figure counter climbs beside it.

At 100 the frame holds on the final photograph for a beat, then that photograph grows out of the frame and lands exactly on your hero image, crop for crop. There is no wipe and no swap: the loader becomes the page. The counter and caption lift away as it grows, and the nav and headline stagger in over the landed image.

It runs on GSAP core alone, is configured with a few data attributes, and leaves a plain, readable hero when JavaScript is off, when GSAP fails to load, or when the visitor prefers reduced motion.

## Features

- Loader built from your own photographs, so the wait previews the work instead of hiding it
- The final photograph grows from the small frame into the full-bleed hero and lands on its exact crop
- Honest progress: the counter follows images that have actually loaded and holds short of 100 until the page has
- Hard time cap so a slow or missing image never traps the visitor, and a minimum duration so a cached load still reads
- Reel skips images that fail to load and falls back to a clean fade if the hero image is unavailable
- Once-per-session mode for repeat visitors, a replay() call, and a photoreel:complete event for chaining
- Scroll locked only while the loader is up; revert() removes every listener, style and generated node
- Works with any hero size: it lands on the hero image's real box, full-bleed or not

## Use Cases

- Photographer and studio portfolios where the first thing a visitor sees should be the work
- Architecture, interiors and travel sites that open on a single strong hero photograph
- Agency case-study pages that want a branded load moment without a curtain
- Image-heavy landing pages where the wait for photographs is real and worth covering
- Product launches that end the load on the hero shot and chain their own animation off the complete event

## 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 loader is an ordinary element on your page: a frame holding a handful of reel images, a counter and a caption. The page's hero image stays where it is in your markup and is the photograph the reel always ends on, so there is one source of truth for what the visitor lands on.

Progress is tied to real loading rather than a timer. The count rises with the images that have settled, stops short of the finish until the browser reports the page loaded, and has both a minimum and a maximum duration so it never flashes past or hangs. The reel only ever shows an image that has arrived.

The handoff is the signature. The last frame and the hero are the same photograph at two sizes, and the growth between them keeps the crop continuous, so the visitor sees one image open up rather than a transition between two. Everything the sequence hides is gated behind a has-js class set in the head, so no JavaScript means no loader and a normal hero.

## Documentation

How this effect works and how it goes into a page. The reference you use once you own the files (worked examples, events, the programmatic API, the class list) ships with the download.

### Quick Start

**1. Add to your HTML `<head>`, before the stylesheet:**

_Code snippet omitted: it ships with the download._

**2. Add the loader as the first thing inside `<body>`:**

_Code snippet omitted: it ships with the download._

**3. Mark your hero image, and the elements that should stagger in once it lands:**

_Code snippet omitted: it ships with the download._

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

_Code snippet omitted: it ships with the download._

### Using It With Your Own Design

**What the effect needs from your markup:** one loader element carrying `data-photo-reel`, with a `data-photo-reel-frame` inside it holding your reel images (`data-photo-reel-image`). The reel always ends on the image marked `data-photo-reel-hero`, which lives in your page as a normal hero image; you do not repeat it in the reel. The counter (`data-photo-reel-count`), the caption (`data-photo-reel-caption`) and anything marked `data-photo-reel-chrome` are optional. The script never reads a class name, so every `reel-loader__*` and `folio-*` class is only there to hang styles on.

**What is only the demo's styling:** the photographer's landing page (nav, headline, scrim, Replay button), the fonts, the caption layout and the counter's size and position. Delete or restyle any of it. The frame can be any size or shape; the script measures it when the photograph starts to grow.

**CSS the effect depends on** (each rule is marked `CONTRACT` in `style.css`):

- `.reel-loader { display: none }` with `.has-js .reel-loader { position: fixed; inset: 0; ... }`: the loader exists only when the script runs, and `.has-js` is removed again if GSAP fails to load.
- `.has-js .reel-loader[data-state="done"] { display: none }`: how the script retires the loader once the hero has landed.
- Reel images are absolutely positioned inside the frame with `object-fit: cover` and start `visibility: hidden`.
- The hero image must cover its box with `object-fit: cover` and a centred `object-position`. That centred crop is what the growing photograph lands on, so a different `object-position` would jump at the end.
- Stacking inside the loader: frame at `z-index: 1`, the script's growing photograph at `2`, the caption and counter at `3`. Do not give the wrapper around them a `z-index`, or the photograph grows behind the frame.
- `html.photo-reel-lock { overflow: hidden }`: the scroll lock while the loader is up.
- `.has-js [data-photo-reel-reveal] { opacity: 0; visibility: hidden; transform: translateY(22px) }`: the reveal start state, restored under `prefers-reduced-motion`.
- `.has-js [data-photo-reel-fade] { opacity: 0 }`: the fade-only start state for overlays, restored under `prefers-reduced-motion`.

Use the same photograph file for the hero that the page would show anyway. The loader copies the hero image's `currentSrc`, so a `srcset` works and the file is only downloaded once.

### Options

All options go on the `data-photo-reel` element.

| Attribute | Values | Default | Description |
|-----------|--------|---------|-------------|
| `data-min-duration` | Seconds | `1` | Shortest the reel may take, even when every image is cached. Keeps a fast load readable |
| `data-max-duration` | Seconds | `6` | Hard cap. The counter completes by this time whatever is still loading; images that never arrived are skipped |
| `data-replay-duration` | Seconds | `1` | Reel length when `replay()` runs, when everything is already cached |
| `data-hold` | Seconds | `0.15` | Pause on the final photograph at 100 before it grows |
| `data-expand-duration` | Seconds | `0.9` | Length of the frame-to-hero growth |
| `data-once` | `true`, `false` | `false` | When `true`, the sequence runs once per browser session; repeat visits go straight to the settled hero |
| `data-once-key` | Any string | Page path | The sessionStorage key suffix for `data-once`. Share one key across pages to show the loader once per site |

### Element Hooks

| Attribute | Where | Description |
|-----------|-------|-------------|
| `data-photo-reel` | The loader | Root of the sequence; options live here. One per page |
| `data-photo-reel-frame` | Inside the loader | The small frame the reel plays in and the photograph grows from |
| `data-photo-reel-image` | Inside the frame | Reel images, shown in DOM order. `data-caption` sets the caption text while it shows |
| `data-photo-reel-count` | Inside the loader | Receives the padded counter text, `000` to `100` |
| `data-photo-reel-caption` | Inside the loader | Receives each photograph's `data-caption` (the hero falls back to its `alt`) |
| `data-photo-reel-chrome` | Inside the loader | Lifts away as the photograph starts to grow |
| `data-photo-reel-hero` | Your hero `<img>` | The photograph the reel lands on and grows into |
| `data-photo-reel-fade` | Anywhere in the page | Fades in, without moving, as soon as the photograph has landed. For a scrim or tint behind your hero text, so it does not snap in with the page |
| `data-photo-reel-reveal` | Anywhere in the page | Staggers in (rise and fade) just after the fades begin |
| `data-photo-reel-replay` | Any `<button>` | Optional: re-runs the sequence without reloading. Disabled while it runs and under reduced motion |

### Accessibility

- **Reduced motion:** under `prefers-reduced-motion: reduce` there is no reel and no growth. The loader never shows, the hero and headline are visible immediately, and the CSS restores the reveal targets even before the script runs. If the preference changes while the page is open, the page settles to the finished hero.
- **No JavaScript:** the loader is hidden unless the `has-js` class is present, so without JavaScript, or when GSAP fails to load, visitors get a plain page with the hero visible.
- **Screen readers:** the reel, counter and caption are `aria-hidden`; a `role="status"` line announces that photographs are loading. The page content is in the document the whole time, so nothing is withheld from assistive technology while the loader plays.
- **Never trapped:** the hard time cap completes the sequence however slow the network is, and scroll is only locked while the loader is on screen.
- **Replay button:** a native `<button>`, disabled while the sequence runs.

### Photography

The demo's photographs are art-directed derivatives of Pexels photographs, used under the [Pexels licence](https://www.pexels.com/license/): heath track in fog (pexels.com/photo/36344918), aerial surf (5851472), ridge over cloud (26953466), pines in fog (15222306), sea cliffs (7112541) and the misty conifer valley hero (10762369). Replace them with your own work: swap the files in `assets/img/`, or edit `assets/img-manifest.json` and rebuild the derivatives.

### Dependencies

**Required:**
- GSAP 3.12+ (core only). The demo is built against GSAP 3.15.0.

No plugins, no build step.

## 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 £10 one-time and covers unlimited personal and commercial projects with no attribution required for our code. Bundled third-party assets retain their own licences and attribution requirements. The only restrictions: no redistribution of the code itself and no competing effect libraries.

- [Buy Photo Reel Preloader](https://gsapvault.com/effects/photo-reel-preloader)
- [The Vault (£99 one-time, best value): every collection in the Vault library, plus future items added to those collections](https://gsapvault.com/effects)

---

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