Skip to content
animorio
IT
All components

The punch card

Cards, Physics, SVGGSAP, CSS onlyAdvancedCompressed weight: Demo 9.5 KBGSAP 28 KBFreeDownload the zip

The loyalty card of a small coffee roaster, cream card stock with ten printed coffee beans: with every coffee the punch takes the next bean away. The card dips under the press towards that bean and springs back a pixel past rest; the little piece of paper comes out of the back, you see it fall through its own hole, in the card’s shadow, and tumble out from under the edge, turning over. Every hole shows the page with the card’s real shadow in it. Undo brings it back up into its hole; full, the card gets the Redeemed stamp and the next tap deals a new one.

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 punch card</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/bricolage-grotesque@5.3.0/standard.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the loyalty card of a small coffee roaster, on paper. Every cup bought the card is punched: the punch
         bites a coffee bean out of it, the card dips under the press and springs back, and the little paper bean
         falls out of the back of the card, seen through its hole, and tumbles out from under it. Through every hole
         you see the page, with the card's own shadow in it. Full, the card is redeemed with a stamp, and a fresh one
         takes its place. The paper, the beans and the stamp are drawn by the script and the CSS -->
    <div class="loyalty" data-loyalty>
      <div class="chads" aria-hidden="true" data-chads></div>
      <div class="deal" data-deal>
      <div class="tilt" data-tilt>
        <button class="card" type="button" data-card data-autoclic aria-describedby="loyalty-status">
          <svg class="paper" viewBox="0 0 320 420" aria-hidden="true" data-paper>
            <defs>
              <mask id="loyalty-holes" maskUnits="userSpaceOnUse" x="-10" y="-10" width="340" height="440">
                <rect x="-10" y="-10" width="340" height="440" fill="#fff" />
                <g data-holes></g>
              </mask>
            </defs>
            <g mask="url(#loyalty-holes)">
              <rect class="paper__sheet" width="320" height="420" rx="16" />
              <rect class="paper__frame" x="10.5" y="10.5" width="299" height="399" rx="9" />
              <path class="paper__rule" d="M24 238.5H296" />
              <g data-beans></g>
              <g class="stamp" data-stamp>
                <rect class="stamp__box" x="-88" y="-27" width="176" height="54" rx="6" />
                <rect class="stamp__box stamp__box--in" x="-82" y="-21" width="164" height="42" rx="3" />
                <text class="stamp__word" y="-1">Redeemed</text>
                <text class="stamp__date" y="13" data-stamp-date>19 Oct 2026</text>
              </g>
            </g>
          </svg>
          <span class="card__words">
            <span class="card__brand">Halde</span>
            <span class="card__kind">Coffee roasters &middot; Loyalty card</span>
            <span class="card__headline" data-headline>Ten cups, the eleventh is on us.</span>
            <span class="card__status" id="loyalty-status"><span class="card__num" data-num>Four</span><span data-rest> cups to go</span></span>
            <span class="card__small">Valid at both Halde counters</span>
          </span>
          <span class="sr" data-label>Punch a cup</span>
        </button>
        <button class="undo" type="button" data-undo hidden>
          <svg viewBox="0 0 16 16" aria-hidden="true"><path d="M5.5 4.5 2.5 7.5l3 3M3 7.5h6.5a3.5 3.5 0 0 1 0 7H8" /></svg>
          <span>Undo</span>
        </button>
      </div>
      </div>
      <p class="hint" aria-hidden="true" data-hint>Press the card to punch a cup</p>
      <p class="sr" aria-live="polite" data-live></p>
    </div>
  </main>

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

26 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 punch card” from Animorio and fit it into my site

What it does

On a white background a cream card-stock card 320 pixels wide, with round corners, a printed hairline all round and a soft shadow on the page. At the top the roaster’s name, Halde, in a narrow, very black Bricolage Grotesque, under it Coffee roasters · Loyalty card and the line Ten cups, the eleventh is on us. Then how many coffees are left, with the number spelt out in orange (Four cups to go), a thin rule and two rows of five printed coffee beans, slanted, with a dashed outline and the S-shaped crease; the last one, the gift, is printed in orange. Six beans are already punched: in their place there is a bean-shaped hole, and through it the white page with a crescent of shadow at the top. With the pointer over it the card lifts a few pixels and the shadow widens, inside the holes too; the bean that will be punched turns solid orange and a little bigger. Held down, the card dips in 3D towards that bean; let go, the bean is gone, the card comes back up a little past rest and wobbles, and the cut piece of paper, in shadow, is seen sinking in the hole, moving away and falling, turning over, until it comes out from under the bottom edge and fades. The number leaves upwards and the new one comes in from below with a small bounce and more weight. Undo appears at the top right: the bean comes back up from under the card, turning over, and fits into its hole, which closes. At the tenth hole the line says This one is on us; pressing it, the orange Redeemed stamp with today’s date comes down on the holes with a thud and the card jolts; pressing again the old card slides down and a new one comes in from above, ready to be punched.

How it works

Card stock cut by a mask

The card is a 320 × 420 SVG: the stock, the hairline, the printed beans and the stamp sit in a group with a mask, and every hole is a black oval in the mask. Punching means switching that oval on: the cut is sharp at any size, and the printed bean under the cut is hidden, because the piece takes it away.

The shadow that falls into the holes

The shadow is not a box-shadow but two drop-shadows on the already cut stock: a tight one that draws the edge of every hole and a soft one that tells how high the card is. As they are worked out on the cut shape, the card’s crescent of shadow falls into every hole by itself, and when the card lifts the crescent grows.

Two springs for the card

The press and the lift are two springs integrated on the GSAP ticker in four sub-steps with the step capped at 1/30 of a second (SPRING). The press turns the card in 3D towards the bean that is next (rotateX and rotateY in proportion to where it is, PRESS degrees) and pushes it back; released it comes up past rest and wobbles. A key or a script click make the press and the release in one go, with a kick to the spring.

The piece that falls out of the back

Each cut piece is a little SVG in a layer under the card: it starts exactly behind its hole, so it is seen only through the hole, in shadow, as it shrinks a little moving away. It has gravity (GRAVITY), a sideways push, a spin and a flip made with scaleY = cosine of the angle: when the cosine is negative it shows the back of the stock. Below the edge it leaves the shadow and fades in thirty pixels.

Undo, the stamp and the new card

Undo takes the piece from where it is (or from under the edge, if it has already fallen) and a timeline brings it back to the hole, turning it face up; when it arrives the hole closes and the card gets a little knock. The stamp comes down with power4.in, the card jolts at the impact and the stamp settles with an elastic; the new card comes in with back.out.

Keyboard and touch

The card is a button: Enter and Space punch, redeem or ask for a new card, Undo is a button of its own and Ctrl or Cmd+Z do the same. Keyboard focus lifts the card and shows the next bean like the mouse passing over; on a phone the finger dips it where it will punch. An aria-live says how many coffees are left.

Values to change

NameFileDefaultWhat it changes
CUPSscript.js10How many coffees fill the card, in two rows (from 6 to 12). The line at the top writes itself.
PUNCHEDscript.js6How many coffees are already punched when the page opens.
PRESSscript.js5How far the card dips under the punch, in degrees.
SPRINGscript.js[260, 0.36]The card’s spring: how stiff it is and how much it is held back (1 = no bounce).
GRAVITYscript.js1600How fast the punched piece falls, in pixels per second squared.
LIFTscript.js5How far the card lifts under the pointer, in pixels.
SLANTscript.js-14The slant of the printed beans, in degrees.

Try them in the Playground

When to use it

For the loyalty card of a cafe, a coffee roaster, an ice-cream shop, a bakery or a hairdresser, in the app or on the site where customers see their points; also for a stamp collection, a pass of entries or a ten-class card.

On phones

The card is 320 pixels wide, or the screen minus 32, and all the words are in container units, so they keep their place at every size. Under a finger the card dips where it will punch and punches on release; Undo has a 36 pixel touch area. Springs, fall and timings are the same as on a computer.

With reduced motion

No dip, no lift and no falling pieces: the hole fades in, Undo closes it the same way, the number changes with a fade, the stamp fades in and the new card arrives with a fade.

Pitfalls

  • A single soft shadow made the holes grey and blurry, like dents: a second one is needed, tight and darker, that draws the cut. Both are drop-shadows on the group already cut by the mask (a box-shadow would ignore the holes).
  • With reduced motion the printed bean stayed visible: its dashed outline sits right on the cut and half the stroke, outside the hole, made a ring. A punched bean is always hidden.
  • A CSS scale on the next bean would wipe out the transform attribute (translate and rotate) and send it to the corner: each bean has two groups, the outer one for its place and the inner one for the CSS spring.
  • The preview’s click and Enter come without a pointerdown: the card did not dip and the hole appeared on a still card. A click with no press kicks the spring and punches 70 milliseconds later, when the card is down.

Libraries

GSAP 3.15.0, Bricolage Grotesque (font variabile) 5.3.0.

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

Testing

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