Skip to content
animorio
IT
Back to the library

The aperture transition

Transitions, SVG, TextGSAP, CSS onlyMediumCompressed weight: Demo 7.3 KBGSAP 28 KBFreeDownload the zip

A minimal page change: the diaphragm of a lens, a few dark blades with thin edges, turns shut over the old page and snaps onto a vermilion pinhole while the f-number climbs to f/22. Then it turns open the other way on the new page and the title widens out of the middle of the hole, letter after letter.

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 aperture transition</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/roboto-flex@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage" data-stage>
    <!-- the first page: everything is centred on the point where the lens closes -->
    <section class="page page--a is-on" data-page="a" aria-label="Still light">
      <header class="bar">
        <span class="brand">Oriel</span>
        <nav class="nav" aria-label="Pages">
          <a href="#still-light" aria-current="page">Still light</a>
          <a href="#portraits" data-go="b">Portraits</a>
        </nav>
      </header>
      <div class="hero">
        <h1 class="title" data-title>Still light</h1>
        <div class="below">
          <p class="lede" data-rise>Photographs made by one north window, slowly, with the shutter open a little longer than it should be.</p>
          <a class="go" href="#portraits" data-go="b" data-autoclic data-rise>
            <span class="go__text">See the portraits</span>
            <svg class="go__iris" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8.5"/><path class="go__hex" d="M10 4.6 14.68 7.3v5.4L10 15.4 5.32 12.7V7.3Z"/></svg>
          </a>
        </div>
      </div>
      <footer class="foot">
        <span>A photography studio</span>
        <span>Film and paper</span>
      </footer>
    </section>

    <!-- the second page: its title widens out of the hole the lens leaves -->
    <section class="page page--b" data-page="b" aria-label="Portraits" aria-hidden="true">
      <header class="bar">
        <span class="brand">Oriel</span>
        <nav class="nav" aria-label="Pages">
          <a href="#still-light" data-go="a">Still light</a>
          <a href="#portraits" aria-current="page">Portraits</a>
        </nav>
      </header>
      <div class="hero">
        <h2 class="title" data-title>Portraits</h2>
        <div class="below">
          <p class="lede" data-rise>Forty faces, the same chair and the same hour of the afternoon, one exposure each.</p>
          <a class="go" href="#still-light" data-go="a" data-autoclic data-rise>
            <span class="go__text">Back to still light</span>
            <svg class="go__iris" viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8.5"/><path class="go__hex" d="M10 4.6 14.68 7.3v5.4L10 15.4 5.32 12.7V7.3Z"/></svg>
          </a>
        </div>
      </div>
      <footer class="foot">
        <span>A photography studio</span>
        <span>Sittings by letter</span>
      </footer>
    </section>

    <!-- the accent seen through the smallest hole, the blades of the lens and the f-number (all moved by the script) -->
    <div class="spot" data-spot aria-hidden="true"></div>
    <svg class="iris" data-iris aria-hidden="true">
      <path class="iris__blades" data-blades fill-rule="evenodd"/>
      <path class="iris__edges" data-edges/>
    </svg>
    <span class="stop" data-stop aria-hidden="true">f/22</span>
  </main>

  <script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.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.

33 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 aperture transition” from Animorio and fit it into my site

What it does

The site of a made-up photography studio, Oriel, in two pages and almost nothing else: a paper-coloured background, one huge centred headline, a line of text, a link. On load the screen is the inside of a shut lens: black, six thin lines turning like a pinwheel around the centre and in the middle a small hexagonal pinhole of vermilion, with f/22 written under it. The diaphragm turns open, the hole grows and shows the paper, and the headline Still light widens right out of the middle of the hole: the middle letters first, very narrow and thin, widening and pushing their neighbours away, going a breath past their width and settling as they take weight. Then the text and the link rise. A click on See the portraits snaps the small lens next to the link, and the real diaphragm closes on the site: the blades come in from the corners, turning, faster and faster, the old headline narrows towards the centre, the number under the hole counts the stops from f/1.4 to f/22 and the blades snap one pixel past the pinhole and spring back with a little bounce. For a breath there is only the spot of colour. Then the diaphragm turns open the other way on the Portraits page, stone-coloured, and the new title widens out of the hole the same way. The Back to still light link does it all in reverse. With the pointer on a link its words widen on a spring and the small lens closes as it turns.

How it works

One hole, not six blades

The diaphragm is a single SVG as big as the stage with two paths. The first is a rectangle bigger than the screen with a regular polygon inside it, filled with fill-rule evenodd: the polygon becomes a hole. The second is the blades' edges: each side of the hole carries on past its corner out of the screen, so the lines turn like a pinwheel, as in a real lens. The polygon has one side per blade, at distance a from the centre, and turns by rot degrees.

Closing on a scale

The size of the hole is not animated in pixels but on a scale: a = PINHOLE times (R / PINHOLE) to the power k, with R half the screen's diagonal. With k going from 1 to 0 the hole halves in the same time over and over, like a steady zoom, and the last stretch, where the hole is small, does not go by in a flash. Closing uses power2.in, so it speeds up into the snap; the same timeline turns rot from 0 to TURN and redraws both paths on every update.

The snap and the pinhole

At the pinhole the blades go SNAP pixels further in 0.07 seconds and come back with elastic.out(1, 0.4), turning a degree and a half more and then back; the number under the hole feels the knock and bounces 3 pixels. Behind the blades there is a vermilion circle as big as the pinhole out to its corners: it grows in the last third of the closing, so in the hole the page gives way to colour. The number counts the stops from f/1.4 to f/22 following k and only shows while the hole is small.

The title widening out of the hole

The title is split into centred inline-block letters, with font-stretch and font-weight tied to the --w and --g variables (Roboto Flex runs from 25 to 151 in width). The letters start at --w 25 and --g 200 and widen with back.out(1.7), staggered from the centre: the middle ones grow first and push the others away, and the overshoot takes each letter a breath past its width before it settles. The title is measured with the letters at 110 width, so even the top of the bounce stays inside the screen. The middle of the capitals sits right on the centre of the stage, where the hole closes.

Direction, phone and reduced motion

Each change is one timeline with the labels close, shut, open and title, and timeScale for SPEED; the pages swap behind the shut blades. A click that comes while the page is changing waits in a queue. The diaphragm is already shut on the first frame, before the fonts arrive. With reduced motion the diaphragm never shows: the old page fades out, the new one fades in with its title already in place, then the text.

Values to change

NameFileDefaultWhat it changes
BLADESscript.js6How many blades the diaphragm has: 5 makes a pentagon of a hole, 9 is nearly round.
TURNscript.js60How many degrees the blades turn while they close (and back while they open): 0 = the hole shrinks without turning.
CLOSEscript.js0.9The seconds the diaphragm takes to close on the old page.
HOLDscript.js0.55The seconds it stays shut on the pinhole of colour.
OPENscript.js1.25The seconds the diaphragm takes to open on the new page.
PINHOLEscript.js6How big the pinhole is, in pixels from the centre to a side.
SNAPscript.js2How many pixels the blades go past the pinhole when they snap shut: 0 = they just stop.
SPEEDscript.js1The speed of everything: 2 is twice as fast, 0.5 half as fast.

Try them in the Playground

When to use it

To move from one page to another on a site about pictures: a photographer or a photo studio, a print lab, a film festival, a gallery, a cinematographer's portfolio. It also works as the site's opening, with the diaphragm opening on the first page.

On phones

The same transition with the sizes adapted: the diaphragm closes on the centre of the tall screen, the titles shrink to fit the width even at the widest point of their bounce and the text wraps under the title. A tap on the link starts it and brings it back.

With reduced motion

Nothing moves or turns: the diaphragm never shows, the old page fades out as the new one fades in with its title already in place, then the text and the link fade in.

Pitfalls

  • Six blades drawn one by one cannot be stacked: in a real diaphragm each blade lies over the next and under the one before, all the way round, and with z-index the last one would always cover the first. The answer is one fill with the hole in it (fill-rule evenodd) and the edges drawn apart, each carried on past its corner.
  • With the size of the hole animated in pixels, the stretch where the hole is small (the best one) would go by in a frame or two: the size is animated on a scale, like a zoom, with a = PINHOLE times (R / PINHOLE) to the power k.
  • The letters are inline-blocks and as font-stretch changes they push their neighbours, so during the bounce the title is wider than at rest: it is measured with the letters at 110 width, above the top of the bounce, so on a phone the outer letters never leave the screen, not even for a moment.
  • The spot of colour sits above the page: if it draws back too late it covers the middle letters of the widening title. It draws back with back.in as soon as the hole is bigger than it, before the letters really show.
  • Before the fonts arrive the page would show open for a moment and then close: the diaphragm is measured and drawn shut at once, from the stage's size alone, and the title is set later.
  • The links' hover was wired only when the entrance started, after the fonts: a pointer already over the link did nothing. The listeners are now wired at once.
  • A click that comes halfway through the change (the previews' autopilot clicks as soon as the entrance ends) would start on top of a timeline still running: it waits in a queue and goes as soon as the timeline ends.

Libraries

GSAP 3.15.0, Roboto Flex (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 7.3 KB; the libraries: GSAP 28 KB.

Testing

Tested on 16 October 2026

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

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