
The loader that becomes a logo
Transitions, SVG, Text
NewFree
An old valve television in its walnut cabinet, with the channel dial, the speaker and the rabbit-ear aerial. On a click the dial clunks round, the picture squeezes into a line and then into a point of light that goes out, a moment of snow rolls past with its bars and the next channel opens from the middle: weather, news, a cartoon and a quiz show.
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, viewport-fit=cover">
<title>Change the channel</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/index.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/jetbrains-mono@5.3.0/index.css">
<link rel="stylesheet" href="style.css">
</head>
<body>
<main class="room" data-stage>
<div class="tv" data-tv>
<!-- the cabinet: walnut, the bezel, the dial, the volume knob, the speaker and the rabbit ears.
The script draws it, wide on a computer and upright on a phone (the controls go under the screen) -->
<svg class="cabinet" data-cabinet aria-hidden="true"></svg>
<!-- the tube: four made-up channels, each one drawn in a 400 x 300 picture -->
<div class="screen" data-screen>
<div class="tube" data-tube>
<div class="roll" data-roll>
<!-- the weather -->
<div class="channel" data-channel="Weather">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="wx-sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#2463a8"/>
<stop offset="1" stop-color="#123563"/>
</linearGradient>
</defs>
<rect width="400" height="300" fill="url(#wx-sky)"/>
<g class="wx-sea" stroke="#ffffff" stroke-opacity="0.07" stroke-width="1" fill="none">
<path d="M0 60H400M0 100H400M0 140H400M0 180H400M0 220H400M60 34V262M140 34V262M220 34V262M300 34V262M380 34V262"/>
</g>
<path d="M58 132C70 88 140 66 196 92C236 74 300 86 312 128C326 166 292 214 240 222C196 244 128 238 104 206C66 196 46 168 58 132Z" fill="#62b06b" stroke="#3d8a4b" stroke-width="2.5"/>
<path d="M84 140C110 120 150 128 170 112" fill="none" stroke="#4f9a5a" stroke-width="2" stroke-linecap="round"/>
<path d="M318 214C332 196 364 198 372 216C378 234 352 248 332 240C316 234 310 224 318 214Z" fill="#62b06b" stroke="#3d8a4b" stroke-width="2.5"/>
<!-- Port Avery: sun -->
<g data-wx-sun>
<g class="wx-rays" data-spin stroke="#ffd23f" stroke-width="4" stroke-linecap="round">
<path d="M130 92V84M130 136V144M108 114H100M152 114H160M114.4 98.4L108.8 92.8M145.6 129.6L151.2 135.2M114.4 129.6L108.8 135.2M145.6 98.4L151.2 92.8"/>
</g>
<circle cx="130" cy="114" r="16" fill="#ffd23f" stroke="#f29e1f" stroke-width="3"/>
</g>
<rect x="110" y="150" width="40" height="20" rx="6" fill="#0d2342"/>
<text class="wx-temp" x="130" y="165" text-anchor="middle">21°</text>
<text class="wx-town" x="130" y="184" text-anchor="middle">Port Avery</text>
<!-- Kestle: rain -->
<g data-bob>
<g class="wx-rain" stroke="#9fd2ff" stroke-width="2.5" stroke-linecap="round">
<path data-drops d="M228 132L224 142M240 132L236 142M252 132L248 142M228 152L224 162M240 152L236 162M252 152L248 162" stroke-dasharray="6 14"/>
</g>
<path d="M214 128C204 128 200 112 214 110C214 96 234 92 240 104C246 94 266 96 266 112C278 112 278 128 266 128Z" fill="#ffffff"/>
</g>
<rect x="220" y="164" width="40" height="20" rx="6" fill="#0d2342"/>
<text class="wx-temp" x="240" y="179" text-anchor="middle">16°</text>
<text class="wx-town" x="240" y="198" text-anchor="middle">Kestle</text>
<!-- Wynmouth: sun behind a cloud -->
<g data-bob-2>
<circle cx="352" cy="190" r="12" fill="#ffd23f" stroke="#f29e1f" stroke-width="2.5"/>
<path d="M330 206C322 206 320 194 330 193C331 183 345 181 350 189C355 182 368 185 367 196C376 196 376 206 367 206Z" fill="#ffffff"/>
</g>
<rect x="324" y="244" width="40" height="18" rx="6" fill="#0d2342"/>
<text class="wx-temp" x="344" y="258" text-anchor="middle">18°</text>
<rect width="400" height="36" fill="#f2c230"/>
<text class="wx-title" x="16" y="25">WEATHER AT SIX</text>
<text class="wx-day" x="384" y="24" text-anchor="end">TOMORROW</text>
<rect y="264" width="400" height="36" fill="#0a1b33" fill-opacity="0.9"/>
<text class="wx-line" x="16" y="287">SUNNY SPELLS, A SHOWER BY THE COAST</text>
</svg>
</div>
<!-- the news -->
<div class="channel" data-channel="News">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
<defs>
<radialGradient id="nw-bg" cx="0.7" cy="0.3" r="0.9">
<stop offset="0" stop-color="#2f4690"/>
<stop offset="1" stop-color="#0c1430"/>
</radialGradient>
<clipPath id="nw-tick">
<rect x="76" y="270" width="324" height="30"/>
</clipPath>
</defs>
<rect width="400" height="300" fill="url(#nw-bg)"/>
<g fill="none" stroke="#ffffff" stroke-opacity="0.1" stroke-width="1.5">
<circle cx="310" cy="110" r="70"/>
<ellipse cx="310" cy="110" rx="34" ry="70"/>
<ellipse cx="310" cy="110" rx="70" ry="24"/>
<path d="M240 110H380M310 40V180"/>
</g>
<g data-globe-sweep>
<ellipse cx="310" cy="110" rx="10" ry="70" fill="none" stroke="#ffffff" stroke-opacity="0.22" stroke-width="1.5" vector-effect="non-scaling-stroke"/>
</g>
<rect x="14" y="14" width="138" height="26" fill="#c8202f"/>
<text class="nw-logo" x="24" y="32">EVENING BULLETIN</text>
<rect x="158" y="14" width="40" height="26" fill="#f4f1ea"/>
<text class="nw-live" x="178" y="32" text-anchor="middle">LIVE</text>
<!-- the presenter -->
<g data-anchor>
<path d="M118 266C122 214 158 196 200 194C242 196 278 214 282 266Z" fill="#262a35"/>
<path d="M178 196L200 236L222 196Z" fill="#f2efe8"/>
<path d="M196 204H204L208 236L200 246L192 236Z" fill="#c8202f"/>
<path d="M178 196L194 226L186 232L170 202Z M222 196L206 226L214 232L230 202Z" fill="#323746"/>
<rect x="190" y="172" width="20" height="26" fill="#cf966a"/>
<g data-head>
<ellipse cx="200" cy="146" rx="30" ry="36" fill="#e0a97e"/>
<ellipse cx="170" cy="150" rx="5" ry="8" fill="#d59b70"/>
<ellipse cx="230" cy="150" rx="5" ry="8" fill="#d59b70"/>
<path d="M168 140C164 106 190 98 206 102C226 104 238 118 232 142C228 124 214 118 196 122C182 124 172 130 168 140Z" fill="#3a2a22"/>
<path d="M184 134Q190 130 196 134M204 134Q210 130 216 134" stroke="#3a2a22" stroke-width="2.4" fill="none" stroke-linecap="round"/>
<g data-blink>
<ellipse cx="190" cy="146" rx="3" ry="3.4" fill="#1d1612"/>
<ellipse cx="210" cy="146" rx="3" ry="3.4" fill="#1d1612"/>
</g>
<path d="M198 150Q196 160 202 160" stroke="#b97d55" stroke-width="2" fill="none" stroke-linecap="round"/>
<ellipse data-mouth cx="200" cy="168" rx="7" ry="3" fill="#7a3a2c"/>
</g>
</g>
<path d="M30 244H370L392 300H8Z" fill="#17244f"/>
<path d="M30 244H370" stroke="#6f86d6" stroke-width="2"/>
<g class="nw-lower">
<rect x="18" y="206" width="76" height="24" fill="#c8202f"/>
<text class="nw-tag" x="56" y="222" text-anchor="middle">BREAKING</text>
</g>
<g class="nw-lower">
<rect x="94" y="206" width="290" height="24" fill="#f4f1ea"/>
<text class="nw-head" x="104" y="222">TOWN CLOCK RUNS FIVE MINUTES FAST</text>
</g>
<rect y="270" width="400" height="30" fill="#f4f1ea"/>
<g clip-path="url(#nw-tick)">
<g data-ticker>
<text class="nw-tick" x="86" y="290" data-tick>FERRY BACK ON TIME / MARKETS CALM / LIBRARY OPEN LATE ON FRIDAYS /</text>
<text class="nw-tick" x="86" y="290" data-tick>FERRY BACK ON TIME / MARKETS CALM / LIBRARY OPEN LATE ON FRIDAYS /</text>
</g>
</g>
<rect y="270" width="76" height="30" fill="#c8202f"/>
<text class="nw-tag" x="44" y="290" text-anchor="middle">LATEST</text>
</svg>
</div>
<!-- the cartoon -->
<div class="channel" data-channel="Cartoon">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
<defs>
<linearGradient id="tn-sky" x1="0" y1="0" x2="0" y2="1">
<stop offset="0" stop-color="#7fcdf2"/>
<stop offset="1" stop-color="#d4f3ff"/>
</linearGradient>
</defs>
<rect width="400" height="300" fill="url(#tn-sky)"/>
<g transform="translate(62 60)">
<g data-spin-slow stroke="#ffb627" stroke-width="5" stroke-linecap="round">
<path d="M0 -40V-32M0 40V32M-40 0H-32M40 0H32M-28.3 -28.3L-22.6 -22.6M28.3 28.3L22.6 22.6M-28.3 28.3L-22.6 22.6M28.3 -28.3L22.6 -22.6"/>
</g>
<circle r="24" fill="#ffd23f" stroke="#ffb627" stroke-width="3"/>
<path d="M-9 -4Q-6 -8 -3 -4M3 -4Q6 -8 9 -4" stroke="#7a4a12" stroke-width="2.4" fill="none" stroke-linecap="round"/>
<path d="M-8 6Q0 13 8 6" stroke="#7a4a12" stroke-width="2.4" fill="none" stroke-linecap="round"/>
</g>
<g transform="translate(270 4)">
<g data-cloud fill="#ffffff">
<path d="M40 92C30 92 28 78 40 76C40 64 58 60 64 70C70 60 88 64 86 78C98 78 98 92 86 92Z"/>
</g>
</g>
<g data-cloud-2 fill="#ffffff" fill-opacity="0.85">
<path d="M206 64C199 64 197 54 206 53C207 45 219 43 223 50C227 44 239 47 237 56C245 56 245 64 237 64Z"/>
</g>
<path d="M-10 230C60 180 140 190 210 224C260 200 330 180 410 214V300H-10Z" fill="#8bd16a"/>
<path d="M-10 262C80 236 200 246 280 262C330 252 380 248 410 254V300H-10Z" fill="#5fb34a"/>
<g fill="#4a9a3a">
<path d="M70 252l4 -10l4 10Z M300 236l4 -10l4 10Z M340 260l4 -10l4 10Z"/>
</g>
<!-- Pebble -->
<g data-pebble>
<path d="M262 226C258 200 280 186 302 188C326 190 338 206 334 226Z" fill="#a3acb8"/>
<path d="M270 210C276 198 290 194 302 196" stroke="#c9d0d9" stroke-width="4" fill="none" stroke-linecap="round"/>
<g data-pebble-eyes>
<ellipse cx="290" cy="210" rx="5" ry="6" fill="#ffffff"/>
<ellipse cx="308" cy="210" rx="5" ry="6" fill="#ffffff"/>
<circle cx="291.5" cy="211" r="2.6" fill="#1d1612"/>
<circle cx="309.5" cy="211" r="2.6" fill="#1d1612"/>
</g>
<path d="M294 220Q299 224 304 220" stroke="#5d6672" stroke-width="2" fill="none" stroke-linecap="round"/>
</g>
<!-- Pip -->
<ellipse data-pip-shadow cx="160" cy="236" rx="22" ry="4" fill="#3c7a2c" fill-opacity="0.45"/>
<g data-pip>
<g data-pip-body>
<circle cx="160" cy="214" r="21" fill="#ffd23f" stroke="#f0a91c" stroke-width="3"/>
<path d="M144 216Q140 208 148 206" stroke="#f0a91c" stroke-width="3" fill="none" stroke-linecap="round"/>
<circle cx="156" cy="207" r="3" fill="#1d1612"/>
<circle cx="168" cy="207" r="3" fill="#1d1612"/>
<path d="M160 212L172 215L160 219Z" fill="#ff7a2e"/>
<path d="M156 194Q160 186 164 194" stroke="#f0a91c" stroke-width="3" fill="none" stroke-linecap="round"/>
</g>
</g>
<g data-title-wobble>
<text class="tn-title" x="200" y="116" text-anchor="middle">PIP & PEBBLE</text>
</g>
</svg>
</div>
<!-- the quiz -->
<div class="channel" data-channel="Quiz">
<svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
<rect width="400" height="300" fill="#341a66"/>
<g transform="translate(200 46)">
<g data-burst fill="#4d27a0">
<path data-burst-path d=""/>
</g>
</g>
<rect x="104" y="18" width="192" height="44" rx="22" fill="#170b36" stroke="#ffd23f" stroke-width="3"/>
<rect data-chase x="104" y="18" width="192" height="44" rx="22" fill="none" stroke="#ffffff" stroke-width="4" stroke-dasharray="14 14" stroke-linecap="round"/>
<text class="qz-title" x="200" y="47" text-anchor="middle">BRAIN WAVE</text>
<clipPath id="qz-wave">
<rect x="128" y="62" width="144" height="16"/>
</clipPath>
<g clip-path="url(#qz-wave)">
<path data-wave d="M128 70Q138 64 148 70T168 70T188 70T208 70T228 70T248 70T268 70T288 70T308 70T328 70" fill="none" stroke="#ffd23f" stroke-width="2.5" stroke-linecap="round"/>
</g>
<rect x="28" y="84" width="344" height="50" rx="12" fill="#0f0a2a" stroke="#8f6bff" stroke-width="2.5"/>
<text class="qz-q" x="200" y="114" text-anchor="middle">WHICH PLANET HAS THE MOST KNOWN MOONS?</text>
<g class="qz-answers">
<path d="M40 166L52 148H184L196 166L184 184H52Z" fill="#0f0a2a" stroke="#8f6bff" stroke-width="2"/>
<text class="qz-a" x="118" y="171" text-anchor="middle">Mars</text>
<path data-pick d="M204 166L216 148H348L360 166L348 184H216Z" fill="#0f0a2a" stroke="#8f6bff" stroke-width="2"/>
<path data-pick-glow d="M204 166L216 148H348L360 166L348 184H216Z" fill="#ff9d2e" stroke="#ffd23f" stroke-width="2"/>
<text class="qz-a" x="282" y="171" text-anchor="middle">Saturn</text>
<path d="M40 210L52 192H184L196 210L184 228H52Z" fill="#0f0a2a" stroke="#8f6bff" stroke-width="2"/>
<text class="qz-a" x="118" y="215" text-anchor="middle">Venus</text>
<path d="M204 210L216 192H348L360 210L348 228H216Z" fill="#0f0a2a" stroke="#8f6bff" stroke-width="2"/>
<text class="qz-a" x="282" y="215" text-anchor="middle">Mercury</text>
</g>
<rect x="28" y="248" width="96" height="36" rx="8" fill="#170b36"/>
<text class="qz-name" x="76" y="262" text-anchor="middle">ANNA</text>
<text class="qz-score" x="76" y="279" text-anchor="middle">300</text>
<rect x="276" y="248" width="96" height="36" rx="8" fill="#170b36"/>
<text class="qz-name" x="324" y="262" text-anchor="middle">RUBEN</text>
<text class="qz-score" x="324" y="279" text-anchor="middle">250</text>
<circle cx="200" cy="266" r="17" fill="#170b36" stroke="#4a2f8a" stroke-width="5"/>
<circle data-clock cx="200" cy="266" r="17" fill="none" stroke="#ffd23f" stroke-width="5" stroke-linecap="round" transform="rotate(-90 200 266)" pathLength="100" stroke-dasharray="100 100"/>
<path data-clock-hand d="M200 266V255" stroke="#ffd23f" stroke-width="2.5" stroke-linecap="round"/>
</svg>
</div>
</div>
<div class="flash" data-flash></div>
</div>
<canvas class="snow" data-snow width="200" height="150"></canvas>
<div class="spot" data-spot></div>
<p class="osd" data-osd aria-hidden="true"></p>
<div class="scan" data-scan></div>
<div class="glass" data-glass></div>
<button class="screen__hit" type="button" data-hit data-autoclic aria-label="Change the channel"></button>
</div>
<button class="dial" type="button" data-dial data-autoclic aria-label="Turn the channel dial"></button>
</div>
<p class="hint" data-hint>Turn the dial, or tap the screen</p>
<p class="sr-only" aria-live="polite" data-live></p>
</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.
46 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 television transition” from Animorio and fit it into my siteThe television is an SVG that the script writes in two versions: wide, with the controls to the right of the screen, and upright, with the controls underneath, picked with matchMedia('(max-aspect-ratio: 4/5)') and redrawn when the phone turns. The walnut grain is a feTurbulence stretched sideways (baseFrequency 0.004 0.11), turned into dark streaks with feColorMatrix and clipped to the shape of the cabinet. The screen, instead, is an HTML div laid over the drawing with its size in percent (the CSS variables --sx, --sy, --sw, --sh), so the channels stay sharp at any size.
The dial is an SVG group turned with the transform attribute rotate(angle cx cy): an object with rot and hover, animated by GSAP, and a function that rewrites the attribute on every frame. On a click it goes back 7 degrees in 0.09 seconds, then forward 90 with back.out: that is the clunk. The angle always grows (90, 180, 270, 360...), so after the quiz it comes back to the weather turning the same way. The aerial sways with a dying sine: an object with t and w, where w (the amplitude) goes to zero while t runs.
All four channels sit in one box, the tube. To switch off, GSAP squashes it down to scaleY 0.012 with power3.in, while a white veil inside the tube comes up: the line turns to light. Then it narrows it to scaleX 0.015 and hides it; in its place is a round div with a white and green radial gradient, which swells for an instant and then shrinks and goes out in half a second. Once: nothing pulses.
The snow is a 200 by 150 pixel canvas stretched over the screen. On every frame each pixel gets a random grey (written into a Uint32Array over the ImageData, much faster than colour by colour), multiplied by two dark bands made with a sine that moves down with time; a thin strip running up is lighter and shifted sideways, like a tear. The drawing is called by the onUpdate of a GSAP tween on a number from 0 to 1, so the snow has its own length in the timeline and stops by itself.
The new channel opens the other way round: the tube starts as a line, widens in 0.2 seconds and then grows tall with back.out, the white veil dies away in steps with keyframes and the content drops from yPercent -16 with elastic.out, the vertical jump of old sets. Each channel has a six-second timeline that repeats, holding all its little movements (the turns are 45 or 60 degrees, where the drawing comes back the same): it starts from the top when the channel comes on and stops when it goes off. The channel name is typed letter by letter, then fades.
| Name | File | Default | What it changes |
|---|---|---|---|
SPEED | script.js | 1 | The speed of the whole change of channel: 2 is twice as fast, 0.5 half as fast. |
COLLAPSE | script.js | 0.5 | The seconds the picture takes to squeeze into a line and then into a point. |
STATIC | script.js | 0.7 | The seconds of snow between two channels. |
ROLL | script.js | 1 | How much the dark bands roll through the snow and the new picture jumps (0 = still). |
GLOW | script.js | 1 | How bright the line and the last point of light are (0 = they just go dark). |
SNAP | script.js | 1 | How hard the dial clunks: its bounce, the jolt of the cabinet and the tremble of the aerial. |
SCANLINES | script.js | 0.35 | How strong the scan lines on the glass are (0 = none). |
To move from one section of a site to another with a sixties or seventies air: a video maker's portfolio, the site of a film festival or a show, a casting page, a studio's project menu where every project is a channel. It also works as a toy or as a news carousel.
The same animation with the sizes adapted: on a tall screen the cabinet stands up and the controls go under the screen in a row (channel dial, volume, speaker), so the tube stays almost as wide as the phone. A tap on the dial or the screen changes channel. The reflection that follows the pointer is there only with a mouse.
Nothing moves or squashes: the dial is already on the new notch, a still frame of snow fades in over the channel, the channel changes underneath and the snow fades away; the channel name fades in and out. The channels stay still, without their movements, and the set fades in at the start.
GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0, JetBrains Mono (font variabile) 5.3.0.
Compressed with gzip, the three demo files weigh 16 KB; the libraries: GSAP 28 KB.
Tested on 11 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.