Skip to content
animorio
IT
All components

The bar that becomes an island

Navigation, Physics, Scroll, SVG, TransitionsGSAPAdvancedCompressed weight: Demo 12 KBGSAP 28 KBFreeDownload the zip

Scroll inside the preview

A wide black navigation bar that, as you scroll, gathers itself into a floating pill like the iPhone’s Dynamic Island, with the name of the section you are reading. Pointed at, or tapped, it swells on a spring into a panel with the links and the upload in progress; while the files go up, a bubble buds off the pill with a ring that fills, stretching a neck of liquid that snaps.

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 bar that becomes an island</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/inter-tight@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <!-- the navigation: a wide black bar at the top of the page that, scrolling, gathers itself into a floating pill
       like the Dynamic Island. Pointed at or tapped, the pill swells into a panel with the links; while the upload
       runs, a bubble buds off it with a ring that fills. Four layers of content take turns inside the one shape -->
  <header class="top">
    <nav class="island" aria-label="Main" data-island>
      <!-- the bubble that buds off the island while the upload runs, and the liquid neck between them -->
      <svg class="island__goo" aria-hidden="true" data-goo>
        <path class="island__neck" data-neck/>
        <circle class="island__bud" r="0" data-bud/>
      </svg>
      <div class="island__shape" data-shape>
        <!-- the wide bar: the upload's chip, the links, the account -->
        <div class="wide" data-layer="wide">
          <span class="chip" aria-hidden="true" data-chip>
            <svg class="ring ring--chip" viewBox="0 0 36 36"><path class="ring__track" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28"/><path class="ring__fill" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28" pathLength="1" data-ring/><path class="ring__check" d="M12.5 18.5l3.6 3.6 7.4-7.6" pathLength="1"/></svg>
            <span class="chip__text" data-chip-text>Uploading</span>
          </span>
          <ul class="wide__links">
            <li><a href="#features" data-go>Features</a></li>
            <li><a href="#pricing" data-go>Pricing</a></li>
            <li><a href="#changelog" data-go>Changelog</a></li>
            <li><a href="#">Docs</a></li>
          </ul>
          <div class="wide__end">
            <a class="wide__signin" href="#">Sign in</a>
            <a class="cta cta--light" href="#">Start free</a>
            <button class="wide__menu" type="button" aria-expanded="false" aria-controls="island-menu" aria-label="Open the menu" data-menu>
              <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 9h14M5 15h14"/></svg>
            </button>
          </div>
        </div>

        <!-- the island: the menu's two lines and the section you are reading -->
        <button class="compact" type="button" aria-expanded="false" aria-controls="island-menu" aria-label="Open the menu" data-layer="compact" data-toggle>
          <svg class="compact__icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 9h14M5 15h14"/></svg>
          <span class="compact__label" aria-hidden="true" data-label-box><span class="compact__ghost" data-ghost></span></span>
        </button>

        <!-- the island saying something for a moment: an upload starting, files synced -->
        <div class="notice" data-layer="notice">
          <span class="notice__icon"><svg class="ring ring--notice" viewBox="0 0 36 36" aria-hidden="true"><path class="ring__track" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28"/><path class="ring__fill" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28" pathLength="1" data-ring/><path class="ring__check" d="M12.5 18.5l3.6 3.6 7.4-7.6" pathLength="1"/></svg></span>
          <span class="notice__text"><b data-notice-title>Uploading 3 files</b><small data-notice-sub>To the team folder</small></span>
        </div>

        <!-- the open island: the live activity, the links, the account -->
        <div class="open" id="island-menu" data-layer="open">
          <div class="act">
            <span class="act__ring">
              <svg class="ring ring--act" viewBox="0 0 36 36" aria-hidden="true"><path class="ring__track" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28"/><path class="ring__fill" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28" pathLength="1" data-ring/><path class="ring__check" d="M12.5 18.5l3.6 3.6 7.4-7.6" pathLength="1"/></svg>
              <span class="act__pct" aria-hidden="true" data-pct>0</span>
            </span>
            <span class="act__text">
              <b data-act-title>Ready to sync</b>
              <small data-act-sub>3 files waiting</small>
            </span>
            <button class="act__btn" type="button" data-act-btn>Upload</button>
          </div>
          <ul class="open__links">
            <li><a href="#features" data-go><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4.5 4.5h6v6h-6zM13.5 4.5h6v6h-6zM4.5 13.5h6v6h-6zM13.5 13.5h6v6h-6z"/></svg>Features</a></li>
            <li><a href="#pricing" data-go><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M3.8 12.6V4.5h8.1l8.3 8.3-8.1 8.1zM8.3 8.6h.01"/></svg>Pricing</a></li>
            <li><a href="#changelog" data-go><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 7.5V12l3 2M20.5 12a8.5 8.5 0 1 1-17 0 8.5 8.5 0 0 1 17 0z"/></svg>Changelog</a></li>
            <li><a href="#"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4.5h9.5L19 9v10.5H5zM14.5 4.5V9H19M8.5 13h7M8.5 16.5h5"/></svg>Docs</a></li>
          </ul>
          <div class="open__end">
            <a class="open__signin" href="#">Sign in</a>
            <a class="cta cta--light" href="#">Start free</a>
          </div>
        </div>
      </div>
      <span class="bud" aria-hidden="true" data-bud-ring>
        <svg class="ring ring--bud" viewBox="0 0 36 36"><path class="ring__track" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28"/><path class="ring__fill" d="M18 4a14 14 0 1 1 0 28a14 14 0 1 1 0-28" pathLength="1" data-ring/><path class="ring__arrow" d="M18 23V13.4M14 17.2l4-4 4 4"/></svg>
      </span>
    </nav>
  </header>

  <main class="read">
    <!-- a plain column to scroll under the bar: each section is a name the island shows while you read it -->
    <section id="overview" data-section="Overview">
      <h2>Overview</h2>
      <p>Files saved on one device are on the others a few seconds later, and in the shared folder as soon as the team needs them.</p>
      <p>There is nothing to set up: it keeps going in the background while you work.</p>
    </section>
    <section id="features" data-section="Features">
      <h2>Features</h2>
      <p>Changes travel in small pieces, so a large file that changed by one line sends only that line.</p>
      <p>Every earlier version is kept for a year, and any of them comes back in one click.</p>
    </section>
    <section id="pricing" data-section="Pricing">
      <h2>Pricing</h2>
      <p>One plan for a single person and one for teams, paid by the seat and not by the gigabyte.</p>
      <p>Either can be cancelled at the end of any month, and the files stay where they are.</p>
    </section>
    <section id="changelog" data-section="Changelog">
      <h2>Changelog</h2>
      <p>Uploads now carry on when you leave the page, and their progress shows in the bar at the top.</p>
      <p>A folder with thousands of small files now arrives in one go instead of one file at a time.</p>
    </section>
  </main>
  <p class="sr" aria-live="polite" data-live></p>

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

57 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 bar that becomes an island” from Animorio and fit it into my site

What it does

On a white background, at the top of the page, a wide black bar with rounded corners: on the left a chip with the upload, in the middle Features, Pricing, Changelog and Docs, on the right Sign in and Start free. Below it there is only a column of headings and short paragraphs, Overview, Features, Pricing and Changelog, to scroll. After a moment the pretend upload starts: the chip goes from 3 to sync to Uploading 15% and its orange ring fills. Scrolling 80 pixels, the bar shrinks on a spring into a black pill 38 pixels tall with the menu's two lines and the name of the section, Overview, which changes with an upward blur at each section. If the upload has just started the pill widens for a couple of seconds and says Uploading 3 files with the ring; then it goes back to a pill and from its right end a black bubble buds off, with the orange ring filling and an up arrow: it stretches a neck of liquid that thins and snaps, and the bubble settles beside it. Pointing at it with the mouse, or tapping it, the island swells on a spring (first in width, then in height, with a little bounce) into a panel: at the top the live activity with the ring, the percentage, the name of the file going up, how many files are still to go, how many MB are left and Cancel to stop it; below, the four links with their icons; at the bottom Sign in and Start free. Moving away it closes. When the upload ends the bubble flows back into the pill, the island widens with a green tick and 3 files synced, and a few seconds later the upload starts again by itself; Again, in the panel, starts it at once. Back at the top the pill opens out into the wide bar.

How it works

One shape, four layers

The island is a single black element: the script writes its width, height, corner radius and place every frame. Inside are four layers of content, each at its own size and centred: the wide bar, the pill, the notice and the open panel. The layer coming in appears with a blur that clears after 0.1 seconds, the one leaving fades in 0.14 and becomes inert, so the Tab key does not go into it.

The springs

Width, height, corners and place are springs on GSAP's ticker, with the stiffness and damping of SPRING and the step capped at 1/30 of a second. Height and corners are less stiff (0.62 times) than the width: the island first widens and then swells, like the iPhone's, instead of scaling all at once.

The bubble and the neck of liquid

The bubble is a circle in an SVG behind the island, starting from the circle of the pill's right end and moving right on a softer spring. Between the two circles the script redraws a neck every frame: two Bézier curves leaving each circle at a spread that narrows as they move apart, closed by an arc on the bubble. First the bubble is flung past the length of the neck (NECK), so the neck snaps, then it settles BUD[0] pixels away; the neck joins again only when the bubble touches the island.

The name of the section

On every scroll the script finds which section has passed 30% of the screen and puts its data-section in the pill: the old word rises and blurs, the new one comes from below. The pill's width is measured on an invisible copy of the new word, so the spring heads for the right size at once. Past SHRINK pixels the bar becomes the island, above half SHRINK it goes back to wide: the gap keeps it from flickering on the edge.

The live activity

The pretend upload is a timeline of six stretches that together last UPLOAD seconds, in fits and starts like a real one: it starts by itself after AUTOPLAY[0] seconds, again AUTOPLAY[1] seconds after it ends, and the panel's button stops it or starts it again. Every frame, if it changed, it updates the rings (stroke-dashoffset on a path that starts at twelve o'clock), the percentage, the file going up (the first one in FILES not yet sent in full) and the MB left, in the bar's chip, in the island's panel and in the bubble. At the start and at the end the island speaks up for NOTICE seconds.

Mouse, finger and keyboard

With the mouse the island opens when pointed at and closes GRACE seconds after the pointer leaves; with a finger, or the keyboard, the island's button opens and closes it. Enter opens and puts the focus inside the panel, Esc closes and puts it back on the island, a tap outside closes, and so does scrolling the page. An aria-live says when the upload starts and ends; the notice does not take the pill's place while someone is working in it with the keys.

Values to change

NameFileDefaultWhat it changes
SPRINGscript.js[240, 0.62]The island's spring: how stiff it is, and how much it is held back (1 = no bounce).
SHRINKscript.js80How far down the page, in pixels, the bar gathers into the island.
PILLscript.js38The height of the island, in pixels.
BUDscript.js[8, 0.92]The bubble of the upload: its gap from the island in pixels, and its size against the island's height.
NECKscript.js20How far the liquid neck stretches, in pixels, before it snaps.
UPLOADscript.js6The seconds the pretend upload takes.
AUTOPLAYscript.js[1.6, 5]The upload starts by itself after these seconds, and again these seconds after it ends (0 = only by hand).
NOTICEscript.js2.2The seconds the island speaks up when an upload starts or ends.
GRACEscript.js0.3The seconds the open island waits, after the pointer leaves, before closing.
FILESscript.js3 fileThe files of the pretend upload: name and size in MB.

Try them in the Playground

When to use it

For the navigation of a product site or a web app with something happening while you read: an upload, an export, an order on its way, a call, a timer. Where there is no live activity, the bar shrinking into an island with the name of the section is enough.

On phones

The wide bar keeps only the upload's chip, Start free and the menu button, which opens the same panel as the island; the island opens and closes with a tap, and a tap on the bubble or on the notice opens it. At 390 pixels the bar and the panel are the screen's width minus 24 pixels and nothing spills over; a file name too long for the panel ends in an ellipsis.

With reduced motion

No springs and no neck of liquid: the island changes size at once, with a quick fade of shape and content; the bubble appears beside the pill already in its place. The name of the section and the words change with a fade, the rings still fill, the links jump to the section without scrolling.

Pitfalls

  • The bubble leaving the pill gently stopped before the neck snapped, and stayed attached by a black bridge: it must be flung past the length of the neck and then brought back to its place, and the neck joins again only when the bubble touches the island (a hysteresis).
  • If the bubble comes out while the island is still turning back into a pill after the notice, the neck stretches behind a moving corner and looks like a tear: it leaves 0.32 seconds later, with the island at rest.
  • The layer coming in was still hidden (visibility: hidden) when the script moved the focus into it with Enter, and the focus was lost: the layer becomes visible at once, and only its opacity rises gently.
  • A ring at zero with a rounded stroke end still shows a dot: the stroke appears with the first percent.
  • An SVG circle starts its stroke at three o'clock: the ring is a path of two arcs starting at twelve, so it fills like the iPhone's.
  • The pill's width taken from the word just put in arrived a frame late and the spring set off towards the old size: an invisible copy of the word is measured before it changes.
  • On the edge between bar and island a single value made the shape flicker with every pixel of scroll: it shrinks past SHRINK and opens again above half of it.
  • With a short page the last section never reached the line at 30% of the screen, and the pill kept the name of the one before even at the end of the page: the last section is at least a screen tall.

Libraries

GSAP 3.15.0, Inter Tight (font variabile) 5.3.0.

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

Testing

Tested on 4 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 components