
The curling sidebar
Navigation, SVG, Text, Physics, Transitions
NewFree
The group chat of three flatmates, where the field you write in is a grey skin made of points on springs. Sending, the skin lets go of the field and closes round the words like a vacuum pack, turns yellow and carries them up into the conversation, stretching on the way and wobbling as it lands; a new one comes out of the button. The answer is written in a ghost bubble in the colour of whoever is writing: instead of words, bars as long as the words grow in bursts, now and then taken back, and when it is sent the bubble fills and the bars develop into the words.
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 vacuum-sealed chat</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/rubik@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="stage">
<!-- the group chat of a shared flat: the field's grey skin closes round the words like a vacuum pack and carries
them up into the conversation, and a flatmate's answer is written in a ghost bubble that shows the shape of
its words before the words themselves -->
<section class="chat" aria-label="Flat 4B, group chat" data-chat data-autoclic>
<header class="chat__head">
<div class="chat__faces" aria-hidden="true">
<span class="face" style="--who: var(--who-1)">I</span>
<span class="face" style="--who: var(--who-2)">T</span>
</div>
<div class="chat__title">
<h2 class="chat__name">Flat 4B</h2>
<p class="chat__status" aria-hidden="true" data-status><span class="status__line">Iris, Theo and you</span></p>
</div>
</header>
<div class="chat__thread" data-thread>
<div class="chat__log" role="log" aria-live="polite" aria-label="Messages" data-log></div>
</div>
<form class="chat__composer" data-form>
<div class="field" data-field>
<label class="sr" for="chat-input">Message Flat 4B</label>
<textarea class="field__input" id="chat-input" rows="1" placeholder="Message Flat 4B" maxlength="280" enterkeyhint="send" aria-describedby="chat-keys" data-input></textarea>
<span class="sr" id="chat-keys">Enter sends, Shift and Enter starts a new line.</span>
<span class="field__caret" aria-hidden="true" data-caret></span>
<button class="send" type="submit" aria-label="Send" disabled data-send>
<span class="send__bg" aria-hidden="true"></span>
<svg class="send__arrow" viewBox="0 0 24 24" aria-hidden="true" data-arrow><path d="M12 18.6V5.8M6.6 11.2 12 5.8l5.4 5.4"/></svg>
</button>
</div>
</form>
<!-- the skins that move between the field and the conversation, and the words they carry -->
<svg class="chat__skins" aria-hidden="true" data-skins></svg>
<div class="chat__fly" aria-hidden="true" data-fly></div>
</section>
</main>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="script.js"></script>
</body>
</html>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.
48 values to change
With the Animorio connector, Claude Code takes it from here, guide included, and fits it into your project. Paste this request:
Take “The vacuum-sealed chat” from Animorio and fit it into my siteEvery shape that moves is a ring of 62 points always laid in the same order on a rounded box (seven on every corner, twelve along the long sides, five along the short ones): so a corner of the field always becomes a corner of the bubble. Every point is pulled to its place by a damped spring, x'' = −k(x − goal) − c·x', in steps of 1/240 of a second with the frame’s step limited to 1/30; its two neighbours drag it, but on how far each is from its place and not on where it is, so the skin rests exactly on the shape and only its waves are smoothed out. Moving, it is drawn as a closed Catmull-Rom curve; still, with exact arcs, and it hands over to the real bubble, an element with a border-radius. When nothing moves the loop stops.
On sending, a hidden copy of the textarea, with the same measures and a span for every word, says where every word is; the words move into a layer above everything, in the same spot, in the same frame in which the textarea empties. The field’s skin takes as its goal the box of the words plus the bubble’s padding, on springs 1.9 times stiffer with a damping of 0.72, while its colour goes from grey to yellow: in WRAP seconds the field has closed round the message.
The real message is already in its place in the conversation, see-through. After WRAP the skin’s goal is a box between the one in the field and the real bubble: x and width follow power3.inOut, y and height power3.out, so it rises first and slides across after, in LIFT seconds. The points at the front, in the direction of travel, have stiffer springs than the ones behind (up to 35% more or less, from the product of the point’s normal and the direction): the skin stretches as it goes and squashes as it stops. A new skin comes out of the button, from a 4 pixel dot, and spreads into the field.
The words have no path of their own: each keeps a fixed distance from the middle of the shape, going from the one in the field to the one in the bubble, and they all move by as much as the skin’s centroid trails behind its goal. So they never leave the skin, they wobble with it and they are exactly in place when it is still. The bubble is at most as wide as the field’s lines (max-width: calc(100% − 50px)), so the line breaks stay the same and the words travel as one block.
Before every change each message’s place is read; after it, each starts again from its old place on a spring, the lower ones a little stiffer, so the push travels up the conversation. When the bubble lands, the messages above get an upward push of BUMP pixels per second, 30% less for each one further up. The oldest fade out under the header (a mask at the top) and past fourteen they go.
The draft is a real bubble with the words written so far in a see-through ink: they are measured with getClientRects and never seen. On every word a bar as long as the word, a caret after the last one, and round it a skin outlined in the writer’s colour that follows the bubble as it grows; a word that wraps slides its bar down to the next line. The way of writing is made up once per answer by a generator seeded from the text: bursts at TYPING letters per second, pauses after commas and full stops, a word written and deleted. Once the answer is sent, the real message takes the draft’s place and its skin; the colour goes to solid grey mixed with premultiplied alpha, a push from the middle swells it for a moment, the bars fade and the words come in from blur(5px). The conversation is a role log with aria-live polite, every message starts with who speaks (hidden from sight) and the draft is aria-hidden; a click from a script, the library’s preview, writes a message letter by letter without taking the focus and sends it.
| Name | File | Default | What it changes |
|---|---|---|---|
WRAP | script.js | 0.16 | The seconds the field’s skin takes to close round the words before it lifts. |
LIFT | script.js | 0.5 | The seconds of the message’s way from the field to its place in the conversation. |
SPRING | script.js | [420, 0.5] | The skin’s springs: how stiff, and how damped, from 0 (wobbles for ever) to 1 (no wobble). |
TENSION | script.js | 0.5 | How much each point of the skin drags its neighbours along, from 0 to 1: higher, smoother. |
TYPING | script.js | 22 | The letters per second of a flatmate writing an answer. |
WAIT | script.js | 0.6 | The seconds between the message landing and the answer starting. |
BUMP | script.js | 130 | How hard a message that lands pushes the ones above it, in pixels per second. |
PEOPLE | script.js | ['Iris', 'Theo'] | The flatmates, in the colours --who-1 and --who-2. |
REPLIES | script.js | [['milk', 'Theo', 'Guilty. Two cartons on my way home, promise.'], …] | A word to look for in my message, who answers it, and the answer. |
OTHER | script.js | [['Iris', 'Ha, fair. Let’s talk about it tonight.'], …] | The answers to anything else, one after the other. |
DEMO | script.js | ['Who finished the oat milk?', …] | What the library’s preview writes and sends, one message per click. |
For the chat of a group or flatmates app, a shop’s support, an assistant that answers, live comments, the demo of a messaging app: wherever the field you write in and the moment of sending should have a character of their own.
The chat takes the width of the screen less the margins (358 pixels at 390) and the bubble stays as wide as the field’s lines; the textarea is 16 pixels, so Safari on iPhone does not zoom the page, the keyboard shows its send key (enterkeyhint send) and the button is a 44 pixel target that goes down under the finger.
No skin closing or travelling: the message appears in its place fading in and the field stays as it is; the messages do not slide, the draft grows word by word without the bars moving and the answer appears fading in.
GSAP 3.15.0, Rubik (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 17 KB; the libraries: GSAP 28 KB.
Tested on 6 October 2026
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.
With your account’s email and password.
or with your email
You’re signed in as .
With your account, you can download the zip of every animation and every pack, for free.