Skip to content
animorio
IT
All components

The coupon button

Buttons, Physics, SVGGSAP, CSS onlyAdvancedCompressed weight: Demo 14 KBGSAP 28 KBFreeDownload the zip

The Apply coupon line of a cart, with an orange paper coupon in the pill: the code AUTUMN15, a stub with a leaf and a row of holes between them. At a click the coupon tears along the holes: the ragged edge is worked out anew every time, the white core of the paper shows, and the torn half turns about the point where it still holds, comes free and falls spinning like a card, then fades away under the pill. The coupon can also be pulled by hand. After Checking… a pen strikes €120.00 through, the old price shrinks away upwards and the new one rolls down with its digits spinning on their wheels to €102.00, and a −15% badge pops in. Another click takes the discount off: the stroke is wiped, the digits roll back up and a fresh coupon rises in its place.

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 coupon button</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/mona-sans@5.3.0/wdth.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="stage">
    <!-- the coupon button: the Apply coupon line of a cart, with a small paper coupon in the pill, perforated down its
         stub. A click (or a pull on the coupon) tears it along the holes: the torn edge is worked out anew every time,
         the torn half falls away spinning, and the price is struck through with a pen and rolls down to the new one.
         Another click takes the discount off and a fresh coupon rises in its place -->
    <div class="deal" data-deal>
      <button class="deal__btn" type="button" data-btn data-autoclic>
        <span class="deal__fill" aria-hidden="true"></span>
        <svg class="deal__coupon" viewBox="0 0 104 42" aria-hidden="true" data-coupon>
          <defs>
            <linearGradient id="cp-paper" x1="0" y1="0" x2="0.35" y2="1">
              <stop offset="0" class="cp-a"/>
              <stop offset="1" class="cp-b"/>
            </linearGradient>
            <pattern id="cp-print" width="3.2" height="3.2" patternUnits="userSpaceOnUse" patternTransform="rotate(32)">
              <path class="cp-print" d="M0 1.6h3.2"/>
            </pattern>
            <clipPath id="cp-cut-stub"><path data-cut="stub"/></clipPath>
            <clipPath id="cp-cut-voucher"><path data-cut="voucher"/></clipPath>
            <g id="cp-art">
              <!-- the paper: a ticket with a notch top and bottom where the perforation runs -->
              <path class="cp-paper" d="M3.5 0H18.6A3.4 3.4 0 0 0 25.4 0H100.5A3.5 3.5 0 0 1 104 3.5V38.5A3.5 3.5 0 0 1 100.5 42H25.4A3.4 3.4 0 0 0 18.6 42H3.5A3.5 3.5 0 0 1 0 38.5V3.5A3.5 3.5 0 0 1 3.5 0Z"/>
              <path class="cp-printed" d="M3.5 0H18.6A3.4 3.4 0 0 0 25.4 0H100.5A3.5 3.5 0 0 1 104 3.5V38.5A3.5 3.5 0 0 1 100.5 42H25.4A3.4 3.4 0 0 0 18.6 42H3.5A3.5 3.5 0 0 1 0 38.5V3.5A3.5 3.5 0 0 1 3.5 0Z"/>
              <rect class="cp-dash" x="28.5" y="4.5" width="71" height="33" rx="2.4"/>
              <!-- a fallen leaf on the stub -->
              <g class="cp-leaf" transform="translate(11 21) rotate(-28)">
                <path d="M0 -8.2C3.6 -5.6 4.6 -1.6 3.4 2.2C2.6 4.6 1.2 6.2 0 7C-1.2 6.2 -2.6 4.6 -3.4 2.2C-4.6 -1.6 -3.6 -5.6 0 -8.2Z"/>
                <path class="cp-vein" d="M0 -6.6V9.6M0 -2.6L2.4 -4.4M0 -2.6L-2.4 -4.4M0 0.9L2.8 -0.9M0 0.9L-2.8 -0.9M0 4.1L2.2 2.6M0 4.1L-2.2 2.6"/>
              </g>
              <text class="cp-off" x="34" y="16.2" data-off-text>15% OFF</text>
              <text class="cp-code" x="33.6" y="30.4" data-code-text>AUTUMN15</text>
            </g>
          </defs>
          <g class="cp-piece" data-piece="stub">
            <g clip-path="url(#cp-cut-stub)"><use href="#cp-art"/></g>
            <path class="cp-fiber" data-fiber="stub"/>
          </g>
          <g class="cp-piece" data-piece="voucher">
            <g clip-path="url(#cp-cut-voucher)"><use href="#cp-art"/></g>
            <path class="cp-fiber" data-fiber="voucher"/>
          </g>
        </svg>
        <span class="deal__faces" data-faces>
          <span class="deal__face" data-face="rest">
            <span class="deal__word" data-wide="Apply coupon" data-rest-text>Apply coupon</span>
          </span>
          <span class="deal__face deal__face--busy" data-face="busy" aria-hidden="true">
            <span class="deal__shine" data-busy-text>Checking…</span>
          </span>
          <span class="deal__face deal__face--done" data-face="done" aria-hidden="true">
            <span class="deal__lines">
              <span class="deal__applied" data-applied-text>Coupon applied</span>
              <span class="deal__sub"><span class="deal__code" data-code-small>AUTUMN15</span> · <span data-remove-text>Remove</span></span>
            </span>
          </span>
        </span>
        <span class="deal__chip" aria-hidden="true" data-chip>−15%</span>
        <span class="deal__price" data-price>
          <span class="deal__was" aria-hidden="true" data-was>
            <span data-was-text>€120.00</span>
            <svg class="deal__strike" aria-hidden="true" data-strike-svg><path data-strike/></svg>
          </span>
          <span class="deal__now" data-now></span>
        </span>
      </button>
      <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 coupon button” from Animorio and fit it into my site

What it does

On a black background a warm charcoal pill 60 pixels tall, with a thread of light along the top. On the left, lying in the pill with its shadow, an orange paper coupon: the stub with an ivory leaf and its veins, a row of small holes with a round notch top and bottom, and the voucher with a dashed frame, 15% OFF in small type and AUTUMN15 in typewriter letters, on a fine print of slanted lines. Then, in ivory Mona Sans, Apply coupon, and on the right the total €120.00. On arrival the coupon rises from below with a bounce. Over the pill the words widen, the fill swells with a bounce and the coupon gives a small tug at its perforation, as if a finger tried it. At a click the words become Checking…, with a sheen running through them, and the tear runs down the holes: the voucher turns about the point where it still holds, and along the two ragged edges, different every time, the white of the paper shows. At the bottom the voucher comes free and falls spinning and sliding sideways like a card, then fades away under the pill; the stub shakes. The pill closes up on the stub and the words follow. Then an orange pen strikes the price through with a quick stroke and a hook; the struck price shrinks away upwards, dimmed, and under it the new price rolls down with its digits spinning on their wheels to €102.00. An orange −15% badge pops in with a bounce, and the words say Coupon applied with AUTUMN15 · Remove under them. Another click takes the discount off: the badge goes, the pen stroke is wiped, the old price comes back down and the digits roll up to €120.00, the stub slides out and a fresh coupon, with a new tear waiting in its holes, rises in its place. The coupon can also be pulled right or down: the tear follows the hand, let go past halfway it rips through and falls with the hand's push, before that it settles back. With the keyboard the focus shows an ivory ring and the coupon gives its tug; Enter or Space apply it.

How it works

An SVG coupon cut in two

The coupon is an SVG group in defs (the paper with its gradient, a fine print of slanted lines, the dashed frame, the leaf on the stub, 15% OFF and the code in JetBrains Mono) used twice with use: one copy is the stub, the other the voucher, and each has its own clipPath. The two clipPaths meet along the tear line, so at rest the coupon looks whole; the perforation's holes are not drawn, they are the gaps left outside both cuts, and the pill shows through them.

The tear, worked out

For every new coupon a random generator with a new seed builds the tear: between one hole and the next (every 3.1 units, radius 0.72) the little bridge of paper gives way along three or four points pushed sideways at random, most in the middle of the bridge (a sine), as much as RAGGED says; at every hole the stub keeps its left half-circle and the voucher its right one. So the two edges fit each other, and the edge is never the same twice. Along the part already torn an ivory line is drawn, the white core of the paper under the print.

The tear running down, and the fall

While the tear runs down (tear.t from 0 to 1 in TEAR seconds, or following the hand) the voucher turns about the point where it still holds, up to 11 degrees. At the bottom it becomes a free card: its centre and angle come from the turn about the last point, its speed from a small push (plus the hand's, if it was pulled), and from there gravity (GRAVITY), air drag stronger across than down, a sideways push following sin 2a like a card dropped flat, and a spin that slows (SPIN). Steps of 1/240 of a second, the frame capped at 1/30; after twenty pixels of fall the voucher fades in eighty. The stub gets a knock on a spring and shakes.

The pen and the wheels of the digits

The pen stroke is a new path for every discount (a little uphill, bowed, with a hook where the pen lifts), drawn with stroke-dashoffset. The price is made of wheels: every digit that changes is a strip with a blank and three rounds of figures behind a window one figure high, with a mask fading it at the top and bottom; the full price uses the third round and the cut price the second, so going down every wheel turns downwards and coming back it turns up. The old price is set in the same equal-width cells, so it sits exactly over the wheels when the pen strikes it, and then shrinks away upwards.

Pulling the coupon

The coupon can also be pulled: pointerdown on the voucher (not on the stub) captures the pointer, and moving right or down becomes the tear, (dx · 0.7 + dy · 0.7) / 70. Let go past halfway it rips the rest of the way and the hand's speed goes into the fall; before that it springs back into place. After a drag the click that follows is ignored. The coupon has touch-action: none, so on a phone the finger pulls instead of scrolling the page; a plain tap stays a click.

The states

The button is a real button with an aria-label that says it all (Apply coupon AUTUMN15. Total €120.00; once applied the code, the discount, the new total and the old one, and Remove coupon), an aria-live region (Applying AUTUMN15…, AUTUMN15 applied: 15% off. New total €102.00., Coupon removed.) and aria-disabled while it works. Checking the code is simulated. Applying takes about three seconds, more than the 2.8 between two clicks of the preview: the second click falls in the wait and the discount stays in view for a whole round.

Values to change

NameFileDefaultWhat it changes
LABELscript.jsApply couponThe words at rest. If they are too long for the room, the type gets smaller.
CODEscript.jsAUTUMN15The code printed on the coupon, repeated under Coupon applied and in the aria-live region.
PERCENTscript.js15The discount in percent: it changes the coupon (15% OFF), the badge and the new price.
PRICEscript.js120The cart total before the discount; the cut one is worked out.
CURRENCYscript.js€The symbol written before the price.
BUSYscript.jsChecking…The words while the code is checked (simulated).
APPLIEDscript.jsCoupon appliedThe words once the discount is on.
REMOVEscript.jsRemoveThe small word that says another click takes the discount off.
TEARscript.js0.34The seconds the tear takes to run down the perforation.
RAGGEDscript.js1How rough the torn edge is: 0 clean from hole to hole, 2 very rough.
GRAVITYscript.js1500How hard the torn half falls, in pixels per second squared.
SPINscript.js1How much the torn half spins as it falls: 0 it drops straight.
ROLLscript.js0.9The seconds the digits spin to the new price.

Try them in the Playground

When to use it

For the discount code of a cart or a checkout: a seasonal voucher, a welcome code, an influencer's code, a gift card. It suits fashion, home and cosmetics shops and any seasonal promotion. Checking the code is simulated: in a real shop the click sends the code to the server, and the tear can start at once while the pen waits for the answer.

On phones

The pill at rest is about 345 pixels wide and the discounted one about 320: at 390 pixels they fit, and if the words are longer than the room the type gets smaller. A tap tears the coupon; a finger can also pull it, and over the coupon the page does not scroll. The falling voucher leaves under the pill and fades before it reaches the bottom of the screen.

With reduced motion

No tear running down, no fall and no spinning wheels: at a click the voucher fades away leaving the stub with its torn edge, the pill takes its new width while the words are gone, and the struck price, the new price and the badge fade in. The coupon does not tug and the pill does not swell (under the mouse the fill only lightens a little). Another click fades it all away and a fresh coupon fades in.

Pitfalls

  • The price vanished for a second and a half right after the click: the fromTo that later brings the new price down from opacity 0 applied its starting state at once, when the timeline was built. It has immediateRender: false, like the badge's.
  • The struck price vanished while it shrank: GSAP cannot interpolate color from var(--ink) to var(--muted) and halfway wrote an invalid colour. The two colours are read from :root and interpolated as real values.
  • With font-variant-numeric: tabular-nums Mona Sans uses a slashed zero, and €120.00 looked like a code. No tabular figures: every digit sits in a cell 0.64 em wide, in the wheels and in the old price, so the two line up.
  • The torn half set off to the right at 70 pixels a second and passed over Checking…; and the pill closed up while it was still there, bringing the words towards it. Now the sideways push is 22 and the pill closes only once the voucher has dropped below.
  • The fresh coupon rose while Apply coupon was still sliding right, and for a moment the words passed under it. The words come back first, the coupon rises after.

Libraries

GSAP 3.15.0, Mona Sans (font variabile) 5.3.0, JetBrains Mono (font variabile) 5.3.0.

Compressed with gzip, the three demo files weigh 14 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 components