112

Tear-Off Page Transition

£20 or the whole Vault, £99

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.

advanced
8 more details
page-transitionsoft-navigationdirectional-transitionphysicsinterruptible-transitionsqueued-transitionsfocus-managementprogressive-enhancement

This demo reads better at your own screen size than in the frame below:

Released

About this effect

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.

Read the full effect overview

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.

Tear-Off Page Transition - GSAP animation effect preview

What's included

8 items
  • 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

Perfect for

4 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

How it works

1 section

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.

Difficulty Advanced
Includes HTML + JS + CSS source, documentation, AI setup prompt, lifetime updates

Lighthouse, as measured

Google Lighthouse on this effect's demo (3 pages, lowest score shown), from the latest scan. A measurement of the demo as shipped, not a promise for your page.

Accessibility
100
Best practices
100

No performance score, on purpose. That figure depends on how you deploy: your server's compression and caching, your CDN, the connection and the device doing the test, none of which the code controls. The same page can score very differently on two consecutive runs, so measure it where it will live.

Paid effect

Purchase to unlock the code.

Buying Tear-Off Page Transition opens the HTML, CSS and JavaScript source, the full documentation, an AI setup prompt for your editor, and every update we ship to it. Standard license: unlimited personal and commercial projects. Every paid product is tested for desktop and mobile layouts, reduced-motion handling, animation cleanup and keyboard controls wherever there is something to operate.

£20 Standard license, unlimited projects

Browse free effects

Or the whole library: the Vault, £99, one payment.

Documentation

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 by Anna Shvets on Pexels, under the Pexels licence. 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.

Worked examples, the events and programmatic API, and the class reference ship with the download, alongside the full source.