Skip to content
animorio
IT
Back to the library

The headline that opens the photo

Scroll, Text, ImagesGSAP, CSS only, CanvasMediumCompressed weight: Demo 12 KBGSAP + ScrollTrigger 45 KBFreeDownload the zip

Scroll inside the preview

The opening of a long feature: on light paper a huge serif headline, and the photo shows only inside its letters. As you scroll the headline grows into one letter until the photo comes out and fills the screen, then it pulls into a frame with its caption and the story begins below, with a drop cap.

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 headline that opens the photo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bodoni-moda@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/opsz.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/opsz-italic.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/schibsted-grotesk@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- the opening of the feature: a tall section with a pinned screen inside; scrolling through it opens the photo -->
  <section class="opener" data-opener>
    <div class="stage" data-stage>
      <header class="mast" data-fade="up">
        <div class="mast__row in">
          <a class="mast__name" href="#">Halyard</a>
          <span class="mast__tag">A quarterly of the sea</span>
          <span class="mast__date">Autumn 2026</span>
        </div>
        <span class="mast__rule" data-rule></span>
      </header>

      <div class="cover">
        <p class="kicker" data-fade="up">
          <span class="in"><span class="kicker__section">The Long Read</span><span class="kicker__sep" aria-hidden="true"></span><span class="kicker__place">North Atlantic</span></span>
        </p>
        <h1 class="cover__title" data-headline>Salt</h1>
        <!-- an empty box as tall as the headline: the script draws the letters over it -->
        <div class="cover__slot" data-slot aria-hidden="true"></div>
        <p class="standfirst" data-fade="down">
          <span class="in">On the last island where the weather still decides when the boats go out, a village of forty-one people has learned the slow art of waiting.</span>
        </p>
        <p class="byline" data-fade="down">
          <span class="in"><span>Words by <b>Ines Harlow</b></span><span>Photographs by <b>Tomas Vey</b></span><span class="byline__time">18 min read</span></span>
        </p>
      </div>

      <p class="hint" data-hint>
        <span class="in">Scroll to read
          <svg class="hint__arrow" data-arrow viewBox="0 0 12 18" aria-hidden="true"><path d="M6 1v15M1 11l5 5 5-5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>
        </span>
      </p>

      <!-- the photo: at first it shows only through the letters of the headline, at the end it is the whole frame -->
      <svg class="art" data-art aria-hidden="true">
        <defs>
          <clipPath id="headline-clip" clipPathUnits="userSpaceOnUse">
            <text class="art__word" data-word></text>
          </clipPath>
        </defs>
        <g data-art-in>
          <!-- the clip sits on a group: on the image itself it would grow with the image -->
          <g clip-path="url(#headline-clip)"><image data-photo-in preserveAspectRatio="xMidYMid slice"/></g>
          <image data-photo-full preserveAspectRatio="xMidYMid slice" opacity="0"/>
        </g>
      </svg>

      <p class="caption" data-caption>
        <span>The harbour at Skarvik at ten to six, before the first boat leaves.</span>
        <span class="caption__credit">Tomas Vey for Halyard</span>
      </p>
    </div>
  </section>

  <article class="story">
    <p class="story__lead" data-reveal><span class="drop" aria-hidden="true"><span data-drop>E</span></span><span class="sr">E</span>very morning at a quarter past five, Aksel Dam walks to the end of the stone pier, takes off one glove and holds his hand up to the wind. He does not check his phone. The forecast, he says, is written by people who have never had to row home. If the air smells of iron, the boats stay in. If it smells of kelp and nothing else, the boats go out. On the morning I met him it smelled, he told me, of both, so we stood there for a long time and said nothing.</p>
    <p data-reveal>Skarvik has forty-one residents, one shop that opens when its owner remembers, and a harbour older than the church. For three centuries the village lived on cod, salted and dried on the flat rocks behind the houses until the whole bay smelled like a kitchen. The racks are still there, grey and silver, though nobody has hung a fish on them since the plant across the sound closed in 1998.</p>
    <p data-reveal>What the village kept is the salt. In a shed with a turf roof, Marit Joensen boils seawater in three iron pans over a driftwood fire, sixteen hours at a time, until what is left at the bottom looks like snow that has been told a secret. She sells it in paper bags to restaurants in cities she has never visited. The waiting list is two years long. She finds this funny.</p>
    <blockquote class="quote" data-quote>
      <span class="quote__rule" data-quote-rule></span>
      <p data-quote-text>The sea gives you the salt. It just wants you to stay up all night for it.</p>
      <span class="quote__rule" data-quote-rule></span>
    </blockquote>
    <p data-reveal>It would be easy to call Skarvik a place that time forgot. The people who live here would disagree. Time, they will tell you, is the one thing they have paid close attention to: the hour of the tide, the week the eider ducks come back, the single afternoon in March when the light returns over the ridge and every kitchen door in the village is opened at once.</p>
  </article>

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

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 headline that opens the photo” from Animorio and fit it into my site

What it does

The first page of a feature in Halyard, an invented sea magazine. At the top the masthead with a thin rule; in the middle the kicker “The Long Read”, the word “Salt” in deep black Bodoni nearly as wide as the page, the standfirst in italics and the byline. Inside the letters there is a photo: a harbour at dawn, with the dark headland, the low sun over the sea, a boat and a lighthouse. On arrival the photo settles into the letters and the words rise one after the other. As you scroll, kicker and masthead rise and fade, standfirst and byline sink and fade, while the headline grows into the “l” up to forty times: the other letters leave the screen and the photo fills everything. Then the photo pulls into a frame, the caption with the photographer appears and the paragraphs start below, the first one with a drop cap rising from its line, and a quote between two rules that draw themselves.

How it works

A pinned stage

A section (1 + LENGTH) screens tall holding a sticky stage 100lvh high. A single ScrollTrigger with scrub carries a timeline from 0 to 1, and the timeline moves four numbers: how much the headline has grown, how big the photo is, how much of the paper is covered and how closed the frame is. One function draws them.

The headline as a window

Over the page there is an SVG as big as the screen, with the photo in an <image> and a clipPath made of text: the word WORD in Bodoni Moda 900. The clipPath sits on a group around the image, not on the image: there it would grow with the photo. The script measures the letters with measureText and picks the size that fills the width; in the page an empty box as tall as the headline keeps its place.

The letter to go into

The letter LETTER is drawn alone in a canvas and a two-pass distance map finds its thickest point, the one farthest from the edge. The headline grows around that point with a scale of GROW to the power of the progress, so it always comes closer at the same pace, while the point slides to the middle of the screen: the letter covers everything as soon as possible.

The photo comes out and is framed

Near the end of the growth a second copy of the photo, without the clip, goes from transparent to full: if the letter were not enough, the paper still disappears. Then clip-path: inset() pulls the photo in by FRAME from the sides and from top and bottom, while the photo goes from 1.06 to 1 and settles. The caption rises under the frame.

The photo and the story

The photo is a layered SVG drawing (sky, sun, haze, hills, the sea with its glints, a rocky headland, lighthouse, boat, grain and vignette) baked once into a JPEG on a canvas: moving a picture costs nothing. Below, each paragraph rises with its own ScrollTrigger, the drop cap comes out of a box that cuts it, and the quote’s rules draw from the middle.

Values to change

NameFileDefaultWhat it changes
WORDscript.js'Salt'The headline, one or two words: the photo shows only inside its letters. On a phone two words go on two lines.
GROWscript.js40How many times the headline grows by the end of the zoom.
LETTERscript.js2The letter the zoom goes into, counting from 0 and without spaces: with “Salt”, 2 is the “l”.
LENGTHscript.js2.6How long the opening lasts, in window heights of scrolling.
SCRUBscript.js0.9The seconds the opening takes to catch up with the scroll: higher is softer.
FRAMEscript.js0.07How far the photo pulls in from the sides at the end, as a share of the width.

Try them in the Playground

When to use it

To open a long article, a feature, a case study or a project page: a short, strong headline and a photo worth the whole screen. It works best with short words and thick letters, in a black serif or a heavy sans.

On phones

The headline fills the width of the narrow screen (two words go on two lines) and the zoom goes into the same letter until it covers everything. The final frame is upright, with more room below for the two-line caption.

With reduced motion

Nothing grows and nothing moves: as you scroll the words fade, the letters with the photo dissolve and the photo already in its frame fades in their place, with the caption. Paragraphs and quote appear with a fade only.

Pitfalls

  • A clip-path set straight on the <image> uses the image’s space, transform included: zooming the photo grows the letters too. It goes on a group around it.
  • Scaling the text from its middle is not enough: at forty times the middle of a letter can be a hole (the bowl of the “a”) and the screen stays paper. The zoom goes into the thickest point of the letter, found with a distance map.
  • With straight-line growth the letter covers the screen in the first quarter and the rest of the scroll shows nothing: a scale of GROW to the power of the progress grows by the same factor at every step.
  • A photo drawn with blurs and grain, moved at every step of the scroll, is redrawn from scratch and stutters: bake it once into a picture.
  • Measure only once the font is loaded (document.fonts.load with the word): with the fallback font the headline does not fill the line and the centre letter is in the wrong place.
  • On phones the browser bar changes the height while scrolling: measuring again at every resize makes the headline jump. Measure again only when the width changes or the height changes a lot.

Libraries

GSAP 3.15.0, Bodoni Moda (font variabile) 5.3.0, Newsreader (font variabile) 5.3.0, Schibsted Grotesk (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 12 KB; the libraries: GSAP + ScrollTrigger 45 KB.

Testing

Tested on 12 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