Skip to content
animorio
IT
Back to the library

The mask that opens

Transitions, Scroll, SVGGSAP, CSS onlyMediumCompressed weight: Demo 6.2 KBGSAP + ScrollTrigger 45 KBFreeDownload the zip

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.

The code

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>

Playground

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

Or ask Claude Code for it

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 site

What it does

The home page of an invented dive club, Lowtide Dive Club. It opens on the sea at night: a dark blue sky with the moon and its glow, the horizon and three waves drifting slowly. In the middle there is a diving mask with a dark rubber frame, and its glass is a window: inside there is turquoise water, with swaying rays of light and passing fish. Below, “Put the mask on.” and a hint. At the first scroll the words rise and vanish into their masks; then the surface grows from the middle of the mask and blurs, the glass widens until it fills the screen and in the end the surface fades away. Under water the headline “Below the surface, everything slows down.” and the button “Book a first dive” arrive, while the water comes a little closer. Scrolling back up, you surface again.

How it works

Two worlds, one over the other

A 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 window

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.

The dive

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 words

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.

Water that lives

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.

Values to change

NameFileDefaultWhat it changes
GROWscript.js19How much the surface grows beyond its size: at the end it is 1 + GROW times bigger, twenty with 19.
BLURscript.js10How much the surface blurs by the end, in pixels.
LENGTHscript.js2.2How long the dive lasts, in window heights of scrolling.
SCRUBscript.js0.6The seconds the dive takes to catch up with the scroll.
FADEscript.js[0.82, 1]Between these two points of the dive the surface fades away.
SWIMscript.js[14, 26]The slowest and the fastest fish, in seconds to cross the screen.
WAVEscript.js9The seconds a wave takes to drift by one length.

Try them in the Playground

When to use it

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.

On phones

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.

With reduced motion

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.

Pitfalls

  • The original’s straight-line growth, 1 + 19t, works with a symbol as small as theirs. With a big window the screen is full after a quarter of the scroll and the rest of the dive shows nothing: with 20 to the power of t the window always opens at the same pace.
  • The window must sit exactly in the middle of the drawing: the layer grows from the middle of the screen, and with preserveAspectRatio slice the middle of the drawing is always the middle of the screen. Off centre, the window runs sideways as it grows.
  • The wobbling fish must not move y: its crossing changes it every lap, and two tweens on the same property cancel each other out. The wobble is a small rotation.
  • The bar at the top cannot use mix-blend-mode: difference: over turquoise the white turns orange. It changes colour with a class once you are under water.
  • The waves must be drawn longer than the screen by at least one wavelength on each side, or the end of the line shows while they drift.

Libraries

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.

Testing

Tested on 2 October 2026

  • Desktop: Done
  • Phone: Done
  • Reduced motion: Done
  • Safari on iPhone: Done

License

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.

All animations