Skip to content
animorio
IT
Back to the library

The piano keyboard

Hover, SVGGSAP, CSS onlyMediumCompressed weight: Demo 9.8 KBGSAP 28 KBFreeDownload the zip

A long piano keyboard drawn in 3D with CSS: as the pointer passes, the keys go down in true perspective and spring back up, and a drawn note flies up from each one, coloured by pitch. A glissando makes a wave, a click a chord.

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 piano keyboard</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/index.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/instrument-sans@5.3.0/index.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="sala">
    <header class="insegna">
      <p class="insegna__sopra" data-entra>Piano, strings and voice for every age</p>
      <h1 class="insegna__titolo" data-titolo>Larkspur School of Music</h1>
      <p class="insegna__sotto" data-entra>Open studio every Saturday morning</p>
    </header>

    <!-- la tastiera: tasti bianchi e neri in 3D con il CSS, costruiti dallo script -->
    <div class="pianoforte" data-pianoforte aria-hidden="true">
      <div class="piano" data-piano>
        <div class="piano__parete"><i class="piano__filetto"></i><i class="piano__feltro"></i></div>
        <div class="piano__letto"></div>
        <div class="piano__tasti" data-tasti></div>
        <div class="piano__listello"></div>
      </div>
    </div>

    <p class="invito" data-entra data-invito>Glide across the keys. Click for a chord.</p>
  </main>

  <!-- le note che volano: disegni nostri, usati con use -->
  <div class="cielo" data-cielo aria-hidden="true"></div>
  <svg class="disegni" aria-hidden="true" focusable="false">
    <symbol id="semiminima" viewBox="0 0 40 64">
      <ellipse cx="14" cy="52" rx="9" ry="6.4" transform="rotate(-22 14 52)" />
      <rect x="20.6" y="7" width="3.4" height="45" rx="1.2" />
    </symbol>
    <symbol id="minima" viewBox="0 0 40 64">
      <ellipse cx="14" cy="52" rx="8.2" ry="5.6" transform="rotate(-22 14 52)" fill="none" stroke="currentColor" stroke-width="3.2" />
      <rect x="20.6" y="7" width="3.4" height="43" rx="1.2" />
    </symbol>
    <symbol id="croma" viewBox="0 0 40 64">
      <ellipse cx="14" cy="52" rx="9" ry="6.4" transform="rotate(-22 14 52)" />
      <rect x="20.6" y="7" width="3.4" height="45" rx="1.2" />
      <path d="M21 7 C 23 18 37 22 33 40 C 32 31 28 27 21 25 Z" />
    </symbol>
    <symbol id="semicroma" viewBox="0 0 40 64">
      <ellipse cx="14" cy="52" rx="9" ry="6.4" transform="rotate(-22 14 52)" />
      <rect x="20.6" y="5" width="3.4" height="47" rx="1.2" />
      <path d="M21 5 C 23 13 35 16 33 30 C 31 23 27 21 21 19 Z" />
      <path d="M21 16 C 23 24 37 28 33 44 C 32 35 28 32 21 30 Z" />
    </symbol>
    <symbol id="crome" viewBox="0 0 58 64">
      <ellipse cx="12" cy="52" rx="9" ry="6.4" transform="rotate(-22 12 52)" />
      <ellipse cx="42" cy="46" rx="9" ry="6.4" transform="rotate(-22 42 46)" />
      <rect x="18.6" y="13" width="3.4" height="39" rx="1.2" />
      <rect x="48.6" y="7" width="3.4" height="39" rx="1.2" />
      <path d="M18.6 12 L 52 5 L 52 13 L 18.6 20 Z" />
    </symbol>
    <symbol id="chiave-sol" viewBox="0 0 40 84">
      <path d="M21 52 C 15 52 14 44 20 42 C 28 40 32 48 29 54 C 25 62 9 61 7 51 C 5 40 16 33 22 27 C 28 21 31 12 27 6 C 23 1 17 7 18 16 L 23 72 C 23 80 12 80 12 74" fill="none" stroke="currentColor" stroke-width="3.6" stroke-linecap="round" stroke-linejoin="round" />
      <circle cx="15" cy="73" r="4.2" />
    </symbol>
    <symbol id="chiave-fa" viewBox="0 0 52 64">
      <path d="M11 22 C 11 12 19 8 25 8 C 35 8 39 16 39 24 C 39 40 25 52 8 58" fill="none" stroke="currentColor" stroke-width="4.2" stroke-linecap="round" />
      <circle cx="12" cy="22" r="5.4" />
      <circle cx="46" cy="16" r="3" />
      <circle cx="46" cy="30" r="3" />
    </symbol>
  </svg>

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

20 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 piano keyboard” from Animorio and fit it into my site

What it does

On a paper-coloured background sits the sign of a made-up music school, Larkspur School of Music, and below it a keyboard as long as the screen, seen from above and a little askew: ivory white keys with their thickness at the front, raised black keys with sides and a shadow on the whites, the red felt over the tails and the lacquered wall behind. On opening, the title’s letters jump up one by one and the keyboard plays a rising scale by itself, closed by a chord. As the pointer passes, the key under the hand goes down turning around its tail, darkens and takes the colour of its note; when the hand leaves it springs back with a little bounce. From every key pressed a drawn note flies up (crotchet, minim, quaver, two beamed quavers, semiquaver) which swells, sways, turns a little and fades, blue in the bass, red in the middle, saffron in the treble; meanwhile the title letter above that key jumps, coloured, pulling its neighbours along. The faster the hand runs, the more the keys ahead of it bend early: a fast glissando becomes a wave. A click, or a firm dive onto the keys from above, plays a three-key chord with bigger notes and treble and bass clefs, and the whole title does a Mexican wave. If nobody plays for a few seconds, the keyboard plays a short phrase by itself.

How it works

A 3D keyboard in CSS

The container has the perspective; inside it, the keybed is tilted with rotateX around its front edge and keeps transform-style: preserve-3d. The script builds the keys: for every octave seven whites side by side and five blacks, centred on the border between two whites with a small offset (C sharp a little to the left, D sharp a little to the right), 0.58 of a white wide and 64% as long. A real key is about six and a half times as long as it is wide: that is the length, if the screen allows it.

Thickness, sides, shadow

Every key has its front face, a child turned by −90° at the bottom, hanging into the bed: that is its thickness. The blacks stand up with translateZ, also have two sides (rotateY ±90°) and a shadow lying on the whites, which gets lighter when the black goes down. The red felt is a strip over the tails, the lacquered wall a standing panel, the front rail a hanging panel. The floor between the keys lies lower than the tip of a pressed key, so it never covers it.

The key going down

A key is an object with a value p, from 0 (up) to 1 (down), driven by GSAP: rotateX(−p × DEPTH) around the tail, and a veil that lights up with p, dark at the back and coloured at the tip. Pressing takes seven hundredths; letting go is an elastic.out, springier or stiffer according to BOUNCE, and the upward bounce (p below zero) counts only six tenths.

Who touches what

On every movement elementFromPoint is asked which key lies under the pointer, and also along the segment from the previous point, in steps of a third of a key, so a fast hand skips no key. Speed in keys per second decides the wave: from three upwards the four whites ahead, in the direction of the hand, bend a little, each less than the one before and a moment later. A click, a tap or a dive from above faster than 320 pixels a second plays the triad starting from the key.

The notes and the title

The notes are symbols in a hidden SVG, drawn with ellipses, rectangles and curves, and used with use. Each one starts from its key, picked at random, coloured with gsap.utils.interpolate between the three colours by pitch; a timeline swells it with back.out, carries it up with power2.out and makes it sway and turn with sine.inOut keyframes, then fades it. The title letters are spans: a key picks the one standing above it in proportion, which jumps and takes its colour; a chord makes them all jump, one after the other.

Values to change

NameFileDefaultWhat it changes
KEYSscript.js3How many octaves the keyboard has (two at most on a phone).
NOTESscript.js1How many notes fly up from each key pressed: 0 none, 0.5 one every two, 2 a little flock.
BOUNCEscript.js1How much a key bounces when it comes back up, and the title letters too (0 = no bounce).
FLOATscript.js1How high and how long the notes fly.
WAVEscript.js1How big the wave of a fast glissando is (0 = no wave).
DEPTHscript.js6How far a key goes down, in degrees.
--nota-grave, --nota-media, --nota-acutastyle.css#2E44B4, #D6432E, #DE8C0FThe note colours, from bass to treble: every key takes the one its pitch calls for.
--feltro, --avorio, --ebano, --cassastyle.css#A51F2B, #F8F3E8, #1A1612, #15110EThe red felt, the white keys, the black keys and the lacquer of the case.

Try them in the Playground

When to use it

For the opening of the site of a music school, a piano teacher, an instrument shop, a festival or a composer: a first screen you play with your hand. It also works as a toy on a contact page or at the bottom of a page.

On phones

The same animations with two octaves at most, narrower keys and smaller notes. You play by sliding a finger over the keys (the page does not scroll on the keyboard) and a tap without dragging plays a chord.

With reduced motion

Nothing moves: the keys do not go down but veil and colour themselves, the notes fade in a little above the key and fade away without flying, the title letters change colour without jumping. The entrance is a fade.

Pitfalls

  • A pressed key turns around its tail and its tip goes below the bed: if the floor between the keys lies just under the surface, the tip slips behind it and the key looks cut off. The floor lies lower than the tip goes with the chosen DEPTH.
  • A filter: blur inside a preserve-3d container flattens the element and confuses the plane order: the black keys’ shadow is a colour with a box-shadow, no filters.
  • The keybed is tilted with a transform in the CSS: animating it with GSAP (y, scale) would wipe out the rotateX. The entrance moves the container, which has no transform of its own.
  • With a fast pointer two events can be three keys apart: looking only at the current point leaves the keys in between silent. elementFromPoint is queried along the segment.
  • Letting go of a key while it was still going down killed the press halfway and the key barely moved: the return waits for the press to finish.
  • On a phone the finger scrolls the page instead of playing: the keyboard has touch-action: none, and the key is found with elementFromPoint, because with a finger pointerenter never reaches the other keys.

Libraries

GSAP 3.15.0, Fraunces (font variabile) 5.3.0, Instrument Sans (font variabile) 5.3.0.

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

Testing

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