# Tear-Off Page Transition

> Soft navigation for multi-page static sites with a physical page tear: the page rips along a perforated top edge, swings from its last corner and falls away to reveal the next page. Back re-attaches it.

Canonical: https://gsapvault.com/effects/tear-off-page-transition
Live demo: https://gsapvault.com/demos/tear-off-page-transition/index.html

| Property | Value |
|----------|-------|
| Type | effect |
| Tier | paid |
| Price | £20 |
| Difficulty | advanced |
| Plugins | Core GSAP only |
| Techniques | page-transition, soft-navigation, directional-transition, physics, interruptible-transitions, queued-transitions, focus-management, progressive-enhancement |
| Uses Lenis | No |

## Lighthouse, as measured

Google Lighthouse on the demo (3 pages, lowest score shown), 5 October 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

Each page is a sheet on a tear-off pad. Click a link and the sheet rips along the perforation at its top edge, starting from the corner nearest the click, swings from the last corner still attached, then falls away with weight to reveal the next page already lying underneath. A thin torn stub stays on the pad for a beat, then lifts away. Back runs it in reverse: the torn sheet rises from below, swings into place and re-attaches.

Underneath is a drop-in soft navigation engine for ordinary multi-page sites. It fetches the next page, swaps one container, and handles history with direction, scroll restoration, focus, announcements, prefetching and queued clicks. Lifecycle hooks let your own page scripts tear down and start again, and a transition option replaces the tear with your own GSAP timelines.

## Features

- A physical tear along a crisp perforation: the rip starts at the corner nearest the click, then the sheet swings from its last attachment point and falls away with its momentum
- Back and Forward run in reverse, and the returning sheet re-attaches along the perforation with a small settle
- On a slow connection the sheet hangs by a thread with a slight sway until the next page is ready, then completes
- Fetch-and-swap navigation with prefetch on hover, focus and touch, a small page cache and a timeout that falls back to a normal page load
- History with direction, scroll restoration per entry, hash targets on arrival, focus moved to the new heading and the new title announced
- Clicks during a transition queue the latest destination; modified clicks, new tabs, downloads, other sites and same-page anchors stay native
- onEnter and onLeave hooks with matching document events for per-page scripts, ScrollTriggers and analytics page views
- Bring your own leave and enter timelines through one option, and destroy() restores ordinary navigation

## Use Cases

- Portfolios and studio sites built as plain HTML pages that want app-like navigation without a framework
- Print, stationery, zine and paper brands where a page that tears off says something about the work
- Event and campaign microsites with a handful of pages and a memorable way to move between them
- Existing static sites that need soft navigation with proper history, focus and analytics before adding their own transition

## 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

Same-origin links to other pages are intercepted. The page is fetched (often already prefetched on hover), its container is parsed and swapped in, and the title, description and language are updated; the rest of the head is shared. The current page is pinned in place as a sheet, cut along its perforation with a mask, and moved only with transforms: GSAP's ticker drives a small rigid-body model in which the tear runs along the top edge, the sheet swings as a pendulum from its last corner and then falls freely. A soft curl and a shadow on the page beneath are cheap overlay layers. History entries carry an order key so Back and Forward know their direction, and record where each sheet was torn so it re-attaches on the same side. Under reduced motion the pages swap instantly with focus and announcements intact; without JavaScript, GSAP or a web server, every link is an ordinary link.

## 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

Every page of your site gets the same three things. Serve the site from a web server (http or https): the next page is fetched, so opened straight from disk every link stays an ordinary link.

**1. In each page's `<head>`, before your stylesheet:**

_Code snippet omitted: it ships with the download._

`has-js` is a hook for your own styles; the script removes it if GSAP fails to load.

**2. Wrap the part of each page that changes in one container:**

_Code snippet omitted: it ships with the download._

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

_Code snippet omitted: it ships with the download._

That is all. The script starts itself: links between your pages now tear, Back re-attaches, and everything else (history, scroll, focus, announcements, prefetching) is handled. To pass options, add `data-tear-off="manual"` to `<html>` and start it yourself (see Options and API).

### Using It With Your Own Design

**What the engine needs from your pages.** One container per page, matched by the same selector (`[data-transition-page]` by default). Only that container is swapped; the document title, the meta description and `<html lang>` are updated from the fetched page, and the rest of `<head>` is kept. So **every page must share the same stylesheets and scripts**; a page with its own CSS or script tags will not get them on a soft navigation. Scripts inside the container are not run either: start per-page behaviour from `onEnter` instead. Classes on `<body>` are not swapped, so put page-specific classes on the container. Anything outside the container (a fixed header, a cookie banner) stays in place across navigations and does not tear.

**What the tear needs.**

- The container is the sheet, so give it an **opaque background**: that colour is the paper. A transparent container borrows the nearest ancestor's background.
- The top `--tear-offset` pixels of the container (16px in the demo) become the **stub** that stays on the pad. Keep content clear of that strip.
- During a transition the leaving page is pinned in place with `position: fixed`. Do not put the container inside an element with a `transform`, `filter`, `perspective` or `contain` value, which would capture the fixed positioning. Fixed elements *inside* the container travel with the sheet.
- The sheet, stub, shadow and curl are layered from `zIndex` (default 60) upwards. Raise it if your own fixed header should be torn over, lower it if the header should stay on top.

**What is only the demo's design.** The Offcut Press copy and photographs, Schibsted Grotesk, the three paper stocks, the header, the sheet counter and the pink "Next sheet" tab are all replaceable. The tab is an ordinary link to the next page; delete it or style it any way you like. The resting row of punched holes is the `.sheet::before` rule in `style.css`: keep it to show the perforation at rest, or remove it and the tear still draws its own torn edge.

**Tear styling, set on the container:**

| Property | Default | What it controls |
|---|---|---|
| `--tear-offset` | `16px` | Distance from the top of the sheet to the perforation (the stub height) |
| `--tear-hole-size` | `4px` | Diameter of the perforation holes |
| `--tear-pitch` | `10px` | Spacing of the holes |
| `--tear-hole` | dark or light tint | Colour of the intact holes on the stub |
| `--tear-shadow` | `rgba(24, 20, 16, 0.3)` | Shadow cast on the page beneath |

The demo's resting perforation reads the same properties, so the two always line up.

**Multiple folders.** Pages in other folders work: their relative URLs are rewritten as they arrive. Links and images *outside* the container keep their first page's URLs, so use root-relative or absolute URLs there if your pages live in different folders.

### Options and API

The script starts itself with the defaults below unless `<html data-tear-off="manual">` is present. To use options, either add that attribute and call `TearOff.init()` yourself, or call `TearOff.init()` at any time: it replaces the running instance.

_Code snippet omitted: it ships with the download._

| Option | Default | Description |
|---|---|---|
| `container` | `'[data-transition-page]'` | Selector for the element swapped between pages |
| `prefetch` | `true` | Fetch pages on hover, focus and touch so the click is instant. Skipped when the visitor has Save-Data on |
| `cacheSize` | `8` | Fetched pages kept in memory |
| `timeout` | `8000` | Milliseconds before a slow page is abandoned for a normal page load |
| `focus` | `true` | Move focus to the new page's `h1` (or the hash target, or `main`) |
| `announce` | `true` | Read the new title in a polite live region |
| `speed` | `1` | Speed multiplier for the tear (`1.3` is brisker, `0.8` heavier) |
| `zIndex` | `60` | Base stacking order of the transition layers |
| `transition` | the tear | Your own `leave` and `enter` timelines (see Custom Transitions) |
| `onEnter` | none | Called with each arriving container (see Lifecycle Hooks) |
| `onLeave` | none | Called with each departing container |

**Instance methods:** `router.navigate(url)` runs a navigation from code; `router.prefetch(url)` warms the cache; `router.isRunning()` is true during a transition; `router.destroy()` (alias `revert()`) restores ordinary navigation. `TearOff.instance` is the running instance.

**Attributes:**

- `data-no-transition` on a link, on any element containing links, on a page's container or on `<html>` keeps those links ordinary.
- `data-tear-off-preload` on a `loading="lazy"` image makes the transition wait for it, like the eager images it already waits for.
- `<html data-transitioning="loading">` while a page is on its way, `"animating"` while it moves: useful for a progress cursor (the demo sets one).

**What stays native without any attribute:** clicks with a modifier key or the middle button, links with a `target` other than `_self`, `download` links, `rel="external"`, other origins, links to files that are not pages (anything with an extension other than `.html`/`.htm`), and links to a fragment of the current page. A page that fails to load, answers with something other than HTML, or has no matching container is opened with a normal page load.

**History and timing.** Each history entry carries an order number, so Back and Forward know their direction, and remembers where its sheet was torn so it re-attaches on the same side. Scroll positions are restored per entry, a new page starts at the top or at its `#hash` target. A click during the rip (before the next page has arrived) sends the tear to the new destination; a click once the new page is underneath is queued and the running transition finishes quickly; a second click on the same link is ignored. Back or Forward mid-transition always ends on the entry the address bar shows. With the next page prefetched a tear takes about 1.1 seconds, and visible movement starts on the first frame after the click. On a slow connection the sheet hangs by a thread from its last corner, swaying slightly, until the page is ready.

### Lifecycle Hooks

Pages arrive by fetch, so scripts that set up a page on load need to run again for each arriving page, and anything they start needs cleaning up when the page leaves. Use the hooks or the matching events on `document`:

- **`onEnter(container, info)`** and the `tear-off:enter` event fire as soon as a new page is in the document, before it is revealed. The address bar, `document.title` and `<html lang>` are already updated. They also fire once for the first page when the script starts, with `info.initial` set to `true`; listeners added after that miss the first call, so prefer the option for set-up code.
- **`onLeave(container, info)`** and `tear-off:leave` fire once the old page has left the screen, just before it is removed. With the tear the new page arrives while the old one is still falling, so **enter fires before leave**; with a sequential custom transition, leave fires first.

`info` (and `event.detail`) holds `container`, `url`, `direction` (`'forward'` or `'back'`), `trigger` (the clicked link, `'popstate'`, `'api'` or `'init'`) and `initial`. A navigation that falls back to a normal page load dispatches `tear-off:error` first, with the URL and the error in `event.detail`.

**Per-page components and ScrollTriggers:**

_Code snippet omitted: it ships with the download._

**Analytics page views.** Umami's default script and Google Analytics 4 (with the default "page changes based on browser history events" enhanced measurement) already record soft navigations, because they watch `history.pushState`; the title is updated before the address changes, so the page view carries the right title. If you have turned automatic tracking off, send the page view from `onEnter`:

_Code snippet omitted: it ships with the download._

### Custom Transitions

The tear is the default and the only transition the effect ships, but the engine is reusable: pass `transition` with your own timelines and keep everything else.

_Code snippet omitted: it ships with the download._

`leave` runs at the click and `enter` once the next page is in place. Each may return a GSAP animation, a promise or nothing; the engine waits for it, speeds GSAP animations up when the visitor clicks again, and gives up waiting after the animation's duration plus two seconds, so a killed tween cannot stall navigation.

By default the change is sequential: the old page leaves, then it is swapped for the new one, then `enter` runs. Set **`overlap: true`** to keep both on screen: the old page is pinned exactly where it was in a fixed layer above the new one, which sits in the normal flow beneath it, scrolled to its place. `enter` then animates either or both, and the old page is removed when `enter` finishes.

_Code snippet omitted: it ships with the download._

`data` holds `current`, `next` (set once the new page is in the document), `url`, `direction`, `trigger`, `point` (where the visitor clicked, or for Back and Forward where they clicked when they first left that page), `speed` (1, or 3 when hurried) and `memo`, an object stored with the history entry of the page that is leaving and handed back when the visitor returns to it. `data.lift(el)` pins another page in a fixed layer above the others, as the tear does with the returning sheet on Back. Two optional methods complete the contract: `arrive(data)` runs the moment the new page is inserted, before it is painted, and `cancel(data)` runs if the navigation is abandoned before the new page arrives (it may return a promise to finish settling).

Under reduced motion the engine skips the transition entirely and swaps the page instantly, custom or not.

### Accessibility

- Links stay ordinary links: keyboard Enter, screen reader link lists, middle-click and open-in-new-tab all work. A keyboard activation tears from the link's own position.
- After each navigation, focus moves to the new page's `h1` (given `tabindex="-1"`), the heading inside a `#hash` target, or `main`, without scrolling. The new title is read out through a polite live region.
- The tear's own layers (stub, shadow, curl) are `aria-hidden` and ignore the pointer. Scrolling is held for the second or so the sheet is moving, so the new page does not slide under it.
- Under `prefers-reduced-motion: reduce` pages swap instantly, with focus, announcement, history and scroll restoration unchanged. A preference change mid-transition finishes it quickly.
- Without JavaScript, when GSAP fails to load, or opened from `file://`, every page is complete and the links load pages normally.
- Tested in the demo: keyboard-only navigation with visible focus, Back and Forward, reduced motion, no JavaScript, a blocked GSAP CDN and a 390px phone layout, with no axe violations on any page.

### Dependencies

- **GSAP 3.12+**, core only (the demo pins 3.15.0). No plugins, no smooth-scroll library.
- A browser with `fetch`, `DOMParser`, `AbortController`, `history.pushState` and CSS masks: every current browser. Anything older keeps ordinary links.
- A web server: soft navigation needs pages served over http or https.

### Teardown

`router.destroy()`, `router.revert()` or `window.gsapContext.revert()` (always the running instance) stops soft navigation and puts everything back: a transition in progress is cut short and its layers removed, a page caught mid-swap is left as the one the address bar shows, in-flight fetches are aborted, every listener is removed, the live region is deleted and `history.scrollRestoration` returns to its previous value. Links load pages normally from then on. Your own hooks are not called by `destroy()`, so clean up per-page components yourself if you are tearing the whole site down.

### Images and Credits

The images ship as baked WebP files in `assets/img/`. `assets/img-manifest.json` records each source, crop and size; to swap one, put a new JPEG in `assets/img-src/` (or a source URL in the manifest) and rebuild it with the repository's `build-template-assets.ts` script, or simply replace the WebP file at the same size.

- `studio.webp`: [Woman walking against art background](https://www.pexels.com/photo/woman-walking-against-art-background-4483218/) by Anna Shvets on Pexels, under the [Pexels licence](https://www.pexels.com/license/). Cropped and resized.
- `press.webp`: an AI-generated photograph of an unbranded risograph duplicator with its pink ink drum pulled out. Not a real machine or studio.
- `print-lido.webp`, `print-allotment.webp`, `print-gulls.webp`, `print-moor.webp`: AI-generated photographs of fictional two-colour risograph prints. The artworks, editions and prices are fiction.

Offcut Press, its address and its email are fictional. Schibsted Grotesk is loaded from Google Fonts under the SIL Open Font License.

## 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 £20 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 Tear-Off Page Transition](https://gsapvault.com/effects/tear-off-page-transition)
- [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
