Skip to content
animorio
IT
Back to the library

The heading that opens

Text, ScrollCSS only, GSAPMediumCompressed weight: Demo 4.4 KBGSAP + ScrollTrigger 45 KBFreeDownload the zip

Scroll inside the preview

A heading in two halves fills the screen; as you scroll the halves shrink and move apart, and the paragraph opens between them. The headings fit the width with CSS alone.

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 heading that opens</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/big-shoulders@5.3.0/standard.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/newsreader@5.3.0/standard.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <header class="top">
    <p class="brand">Lanterna Philharmonic</p>
    <p class="top__line">Season notes</p>
  </header>

  <main>
    <!-- every split section is three screens tall; its stage stays on screen while the two halves open -->
    <section class="split split--night" data-split>
      <h2 class="visually-hidden">First season</h2>
      <div class="split__stage">
        <div class="split__grid">
          <div class="split__half split__half--top" aria-hidden="true">
            <p class="fit split__title" data-title>
              <span class="fit__box"><span class="fit__display">First</span></span>
              <span class="fit__ref">First</span>
            </p>
          </div>
          <p class="split__text" data-text>Thirty-two concerts, eleven premieres and a new hall by the water. This is the first season of the Lanterna Philharmonic, and it starts with every door open.</p>
          <div class="split__half split__half--bottom" aria-hidden="true">
            <p class="fit split__title" data-title>
              <span class="fit__box"><span class="fit__display">Season</span></span>
              <span class="fit__ref">Season</span>
            </p>
          </div>
        </div>
        <p class="split__hint" data-hint>Scroll</p>
      </div>
    </section>

    <section class="split split--amber" data-split>
      <h2 class="visually-hidden">One hall, every street</h2>
      <div class="split__stage">
        <div class="split__grid">
          <div class="split__half split__half--top" aria-hidden="true">
            <p class="fit split__title" data-title>
              <span class="fit__box"><span class="fit__display">One hall</span></span>
              <span class="fit__ref">One hall</span>
            </p>
          </div>
          <p class="split__text" data-text>On Sundays the orchestra leaves the stage: string quartets in the covered market, brass on the morning ferries, the whole symphony in the old shipyard.</p>
          <div class="split__half split__half--bottom" aria-hidden="true">
            <p class="fit split__title" data-title>
              <span class="fit__box"><span class="fit__display">Every street</span></span>
              <span class="fit__ref">Every street</span>
            </p>
          </div>
        </div>
      </div>
    </section>

    <section class="split split--sky" data-split>
      <h2 class="visually-hidden">The city in tune</h2>
      <div class="split__stage">
        <div class="split__grid">
          <div class="split__half split__half--top" aria-hidden="true">
            <p class="fit split__title" data-title>
              <span class="fit__box"><span class="fit__display">The city</span></span>
              <span class="fit__ref">The city</span>
            </p>
          </div>
          <p class="split__text" data-text>Rehearsals are open to anyone holding a ticket stub, and every school in the district gets a row of seats at the front.</p>
          <div class="split__half split__half--bottom" aria-hidden="true">
            <p class="fit split__title" data-title>
              <span class="fit__box"><span class="fit__display">In tune</span></span>
              <span class="fit__ref">In tune</span>
            </p>
          </div>
        </div>
      </div>
    </section>

    <section class="outro">
      <p class="outro__text">The season opens on the first Friday of October.</p>
    </section>
  </main>

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

32 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 heading that opens” from Animorio and fit it into my site

What it does

The season notes of an invented orchestra, in three chapters, each in its own colour. In each chapter a heading of two huge lines, «First» and «Season», fills the screen, one half above the middle line and one below. As you scroll the page stays put: the two halves shrink and move apart, the top one up and the bottom one down, and the paragraph opens from the middle line, uncovered from the centre outwards. At the end of the chapter the next one arrives.

How it works

The stage

Each chapter is three screens tall and its stage, one screen tall, stays put with position: sticky. The stage is a grid of two equal rows: the top half rests its heading at the bottom, the bottom half at the top, so the two lines touch on the middle line. The paragraph sits in the centre, under the headings.

The width, with CSS alone

Next to the heading there is a hidden copy of it at 16 px, which takes its own width; a box takes the rest of the line. A property registered as a length turns 100cqi into pixels where it is written: in the box it is the whole line, in the heading it is the box, that is the line minus the text. Line divided by text says how much to enlarge, and tan(atan2(a, b)) = a / b is the way CSS has to divide one length by another. The heading never gets taller than its half (100cqh).

How far apart

The script measures the paragraph’s real height, once the fonts are in, and moves each half away by half that height plus 54 px. The halves go down to scale 0.82, or less if needed to stay within a quarter of the screen each; the top one shrinks towards its lower edge (origin at 75%), the bottom one towards the top (25%).

The scroll

One timeline tied to the chapter’s scroll, from the start of the stage to its end, with half a second of lag to smooth the wheel: the halves move from the start, the paragraph opens from a tenth, with clip-path from inset(50% 0 50% 0) to inset(0). If the window’s width changes, everything is measured again after 200 ms of quiet; the height does not count, because on phones it changes with every bar that slides away.

Values to change

NameFileDefaultWhat it changes
SHRINK, HEIGHTscript.js0.82, 0.25The largest scale of the halves once open, and the most of the screen each one may take.
GAPscript.js54The pixels between the paragraph and each half, once open.
TEXT_FROMscript.js0.1When the paragraph starts to open, in the chapter’s scroll.
SCRUBscript.js0.5The seconds the halves take to catch up with the scroll.
--fit-maxstyle.css100cqhThe headings’ limit: the height of their half. Raise it for headings that spill off the stage.

Try them in the Playground

When to use it

To open a chapter with a poster heading and a few lines under it: a season’s page, an annual report, a story in chapters, a product launch.

On phones

The same: the headings fill the screen’s width, the paragraph is centred, at 18 px, and takes the whole line minus 32 px margins.

With reduced motion

Each chapter is one screen tall and already open: small headings above and below, the paragraph in between.

Pitfalls

  • The property must be registered with @property as a length: without it, 100cqi stays a word and is worked out where it is used, that is always on the same box, and the ratio becomes infinite.
  • The hidden 16 px copy must be narrower than the line: if the text at 16 px were wider, the box next to it would shrink to zero and the heading would stay at 16 px.
  • cqh in the heading takes the nearest container that has a height, that is the half (container-type: size): the fit’s boxes are width-only containers and do not count.
  • A short word, enlarged to fill the line, gets taller than its half and spills off the stage: the 100cqh limit stops it.
  • Without smooth scrolling, scrub: true follows the wheel in jumps: here there is half a second of lag.

Libraries

GSAP 3.15.0, ScrollTrigger (GSAP) 3.15.0, Big Shoulders (font variabile) 5.3.0, Newsreader (font variabile) 5.3.0, Mona Sans (font variabile) 5.3.0.

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

Testing

Tested on 29 September 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