
The frame that becomes the page
Transitions, Scroll, SVG
NewFree
Scroll inside the preview
The sea at night fills the screen, and in the middle there is a window shaped like a diving mask: inside you see the water, with its fish and its light. As you scroll the whole surface grows from the middle of the mask up to twenty times its size and blurs, until the window is the screen and you are under water.
A folder that runs on its own: index.html, style.css, script.js and the libraries from a CDN. The React and Vue tabs hold the same demo as a ready component, which switches its animations off when it leaves the page.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The mask that opens</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/space-grotesk@5.3.0/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource/instrument-serif@5.3.0/400-italic.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="bar">
<a class="brand" href="#">Lowtide Dive Club</a>
<nav class="bar__nav" aria-label="Sections">
<a href="#">Courses</a>
<a href="#">Sites</a>
<a href="#">Gear</a>
</nav>
</header>
<!-- one pinned screen with two worlds: under the water at the back, the night surface in front of it -->
<section class="dive" data-dive>
<!-- under the water: seen through the mask at the start, the whole screen at the end -->
<div class="deep" data-deep>
<svg class="deep__art" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="water" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#5fe0d0"/>
<stop offset="0.45" stop-color="#1fa6b8"/>
<stop offset="1" stop-color="#0b4f74"/>
</linearGradient>
<linearGradient id="ray" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#ffffff" stop-opacity="0.55"/>
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
</linearGradient>
</defs>
<rect width="1440" height="900" fill="url(#water)"/>
<g class="rays" data-rays>
<path d="M470 0h90l-150 900h-170z" fill="url(#ray)" opacity="0.6"/>
<path d="M700 0h60l40 900h-190z" fill="url(#ray)" opacity="0.45"/>
<path d="M930 0h110l230 900h-200z" fill="url(#ray)" opacity="0.5"/>
</g>
<!-- fish: each one swims across on its own line -->
<g data-fish fill="#f6b43c">
<path d="M0 0c22-16 52-16 72 0-20 16-50 16-72 0zm72 0 22-14v28z"/>
</g>
<g data-fish fill="#ff7a59">
<path d="M0 0c16-11 38-11 52 0-14 11-36 11-52 0zm52 0 16-10v20z"/>
</g>
<g data-fish fill="#fff1c9">
<path d="M0 0c14-9 32-9 44 0-12 9-30 9-44 0zm44 0 13-8v16z"/>
</g>
<g data-fish fill="#f6b43c">
<path d="M0 0c19-13 44-13 61 0-17 13-42 13-61 0zm61 0 19-12v24z"/>
</g>
<!-- the sea floor: two hills and a few weeds that sway -->
<path d="M0 790c180-60 330-40 470 0s320 40 520-10 330-40 450 10v110H0z" fill="#083a57"/>
<g class="weeds" fill="none" stroke="#0e6b62" stroke-width="10" stroke-linecap="round">
<path data-weed d="M180 860c-10-60 20-110 0-170"/>
<path data-weed d="M230 870c10-50-20-90 0-140"/>
<path data-weed d="M1150 860c-12-70 22-120 0-190"/>
<path data-weed d="M1210 870c8-50-16-80 0-120"/>
</g>
</svg>
<div class="deep__words">
<h1 class="deep__title" data-title>Below the surface, <em>everything slows down.</em></h1>
<a class="deep__link" href="#" data-cta>Book a first dive</a>
</div>
</div>
<!-- the surface at night, with a window in the shape of a diving mask; scrolling makes it grow until you are in -->
<div class="surface" data-surface>
<svg class="surface__art" viewBox="0 0 1440 900" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<defs>
<linearGradient id="night" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#0a1430"/>
<stop offset="0.55" stop-color="#162a52"/>
<stop offset="1" stop-color="#0b1834"/>
</linearGradient>
<radialGradient id="glow" cx="0.5" cy="0.5" r="0.5">
<stop offset="0" stop-color="#fff6dc" stop-opacity="0.5"/>
<stop offset="1" stop-color="#fff6dc" stop-opacity="0"/>
</radialGradient>
<!-- the window: everything is drawn except the glass of the mask -->
<mask id="window" maskUnits="userSpaceOnUse" x="0" y="0" width="1440" height="900">
<rect width="1440" height="900" fill="#fff"/>
<use href="#lens" fill="#000"/>
</mask>
<path id="lens" data-lens d="M-96-34c0-20 16-26 36-26h120c20 0 36 6 36 26v48c0 26-18 42-44 42-16 0-26-8-34-20-6-10-12-14-18-14s-12 4-18 14c-8 12-18 20-34 20-26 0-44-16-44-42z" transform="translate(720 450) scale(1.15)"/>
</defs>
<g mask="url(#window)">
<rect width="1440" height="900" fill="url(#night)"/>
<circle cx="1080" cy="190" r="120" fill="url(#glow)"/>
<circle cx="1080" cy="190" r="34" fill="#fff4d8"/>
<path d="M0 560h1440v340H0z" fill="#0d2347"/>
<g class="waves" fill="none" stroke-linecap="round">
<!-- the script draws the waves: a line that goes up and down every 360 units, long enough to drift -->
<path data-wave data-y="610" data-amp="16" stroke="#24467a" stroke-width="5"/>
<path data-wave data-y="690" data-amp="20" stroke="#1d3c6c" stroke-width="7"/>
<path data-wave data-y="790" data-amp="24" stroke="#173462" stroke-width="9"/>
</g>
<!-- the rubber frame of the mask, around the glass -->
<use href="#lens" fill="none" stroke="#1b2333" stroke-width="22"/>
<use href="#lens" fill="none" stroke="#3a4a66" stroke-width="3"/>
</g>
</svg>
<div class="surface__words">
<p class="surface__lead" data-lead>Put the mask on.</p>
<p class="surface__hint">Scroll to go under.</p>
</div>
</div>
</section>
<section class="after">
<p class="after__text">Small groups, slow dives, and a warm drink on the boat on the way back.</p>
</section>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
<script src="script.js"></script>
</body>
</html>Change values, colours and text with the controls and watch the animation respond live. Or write in the code yourself. Then download the folder with your changes.
22 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The mask that opens” from Animorio and fit it into my siteA pinned screen with two layers: the water at the back, the surface in front. They are two SVGs with the same viewBox, 1440 by 900, and preserveAspectRatio slice: they cover the screen the same way and the centre of the drawing is always the centre of the screen.
The surface is drawn under an SVG mask: a white rectangle, which shows everything, and the shape of the mask’s glass in black, which makes the hole. The same shape, drawn again with a thick dark stroke, is the rubber frame. The glass sits in the middle of the drawing.
A ScrollTrigger pins the screen for LENGTH heights and with scrub carries a value from 0 to 1. The surface grows from the middle and blurs with filter: blur(BLUR · value px), the original’s blur. The growth does not follow the original: there it is a straight line, scale 1 + 19t, but with a window as big as this one the screen would be full in the first quarter of the dive. Here the scale is (1 + GROW) to the power of the value, so it grows by the same factor at every step and the window opens at the same pace all the way, twenty times at the end. It is a single layer, which the browser moves on the graphics card. Between the two points of FADE the surface fades out; the water goes from 1.12 to 1.
The surface line is split into words, each inside a span that cuts off what sticks out: at the first touch of the scroll they rise (yPercent −110, power3.out, 0.04 seconds apart), like the original’s SplitText words. At 0.72 of the dive the water’s headline and button arrive; going back, everything returns.
The script draws the waves: a curve going up and down every 360 units, long enough to drift by 720 and start again. The fish cross each at their own pace, between the seconds of SWIM, with a small rotation as their wobble; the rays sway with skewX and the weeds bend around their foot.
| Name | File | Default | What it changes |
|---|---|---|---|
GROW | script.js | 19 | How much the surface grows beyond its size: at the end it is 1 + GROW times bigger, twenty with 19. |
BLUR | script.js | 10 | How much the surface blurs by the end, in pixels. |
LENGTH | script.js | 2.2 | How long the dive lasts, in window heights of scrolling. |
SCRUB | script.js | 0.6 | The seconds the dive takes to catch up with the scroll. |
FADE | script.js | [0.82, 1] | Between these two points of the dive the surface fades away. |
SWIM | script.js | [14, 26] | The slowest and the fastest fish, in seconds to cross the screen. |
WAVE | script.js | 9 | The seconds a wave takes to drift by one length. |
For stepping into a world: a dive, a trip, a museum, a game, a product you look inside. It needs a shape you recognise even when small, like a mask, a porthole, a keyhole or a solid logo, and a second scene worth seeing in full.
The drawing covers the narrow screen by cutting the sides, so the mask stays big, about half the width. A finger makes the dive like the wheel, with the same blur.
The surface does not grow or blur: as you scroll it fades into the water between a quarter and three fifths of the dive. Waves, fish, rays and weeds hold still, with the fish in a row in the water.
GSAP 3.15.0, Space Grotesk (font variabile) 5.3.0, Instrument Serif 5.3.0.
Compressed with gzip, the three demo files weigh 6.2 KB; the libraries: GSAP + ScrollTrigger 45 KB.
Tested on 2 October 2026
Use it in your own projects and in your clients’ projects. Do not resell it or redistribute it as is. GSAP stays under its own Standard license. Full license.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.