Skip to content
animorio
IT
All components

The candy wrapper menu

Menus, WebGL, TransitionsGSAP, WebGLAdvancedCompressed weight: Demo 13 KBGSAP 28 KBFreeDownload the zip

A sweet factory's menu wrapped like a candy in ruby foil, with its two ends twisted: pressed, the ends untwist turning opposite ways, the wrapper unrolls and lies flat keeping its crinkles, and the entries appear printed on it in cream. Under the pointer the foil ripples and glints; once an entry is chosen, the candy wraps itself up again with a bounce.

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 candy wrapper menu</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fontsource-variable/fraunces@5.3.0/full-italic.css">
  <link rel="stylesheet" href="style.css">
</head>
<body>
  <main class="stage">
    <!-- the menu of a sweet shop, wrapped like a sweet: at rest it is a candy in ruby foil with its two ends twisted.
         Pressed, the ends untwist the opposite ways, the wrapper unrolls and lies flat with its creases, and the menu
         is printed on it. The foil is a WebGL mesh drawn by the script; the links below are the real menu, laid over
         the flat wrapper -->
    <div class="candy" data-candy>
      <span class="candy__shadow" data-shadow aria-hidden="true"></span>
      <canvas class="candy__gl" data-canvas aria-hidden="true"></canvas>

      <button class="candy__toggle" type="button" aria-expanded="false" aria-controls="sweets-menu" data-toggle data-autoclic>
        <span class="candy__word" data-name>Open the menu</span>
        <svg class="candy__cross" viewBox="0 0 24 24" aria-hidden="true" data-cross><path d="M6.5 6.5l11 11M17.5 6.5l-11 11"/></svg>
      </button>

      <nav class="candy__nav" id="sweets-menu" aria-label="Main" data-nav inert>
        <ul class="candy__list">
          <li><a class="sweet" href="#" data-item><span class="sweet__name">Bonbons</span><span class="sweet__note">Hand-wrapped, sold by the jar</span></a></li>
          <li><a class="sweet" href="#" data-item><span class="sweet__name">Chocolate</span><span class="sweet__note">Bars, pralines and truffles</span></a></li>
          <li><a class="sweet" href="#" data-item><span class="sweet__name">Gift tins</span><span class="sweet__note">Packed and posted for you</span></a></li>
          <li><a class="sweet" href="#" data-item><span class="sweet__name">The factory</span><span class="sweet__note">Tours on Saturday mornings</span></a></li>
          <li><a class="sweet" href="#" data-item><span class="sweet__name">Visit us</span><span class="sweet__note">The shop on Mill Lane</span></a></li>
        </ul>
      </nav>
    </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.

27 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 candy wrapper menu” from Animorio and fit it into my site

What it does

On a white page there is only a candy wrapped in ruby foil, with Menu printed in cream italics on its body and its two ends twisted into fans, full of folds spiralling towards the tight neck. With the mouse over it the ends twist a little tighter, the candy rolls slightly and lifts, and a glint runs across the foil; pressed, it squashes under the finger. Then the ends untwist, the left one one way and the right one the other, the folds gathered at the neck let go and the paper becomes a tube; the tube leans back and unrolls towards the viewer, grows into a sheet, goes a little past flat and back, and lies still with the crinkles left by the wrapping and longer creases where the ends were. A double frame with the shop’s name is printed on the sheet, and the entries appear one after the other like ink settling: Bonbons, Chocolate, Gift tins, The factory and Visit us, each with a line in italics. Under the pointer the foil beneath an entry ripples in little rings spreading from its middle, a glint crosses it diagonally and the name gains weight; the chosen entry has a thin printed oval around it. Pressing the X in the corner, an entry, Esc or outside the sheet, the paper rolls back up and the ends twist again, going a little too far and coming back with a bounce.

How it works

A mesh of points

The paper is a grid of 140 by 88 points the script works out again at every frame, in a WebGL buffer. Each column of the grid is a strip going round the candy: its place along the candy goes from the wrapped one (body and ends) to the flat one, while the strip is bent along a circle.

Unrolling means straightening

Each point has its distance along the paper, s, and lies on the arc of a circle of curvature k: y = sin(ks)/k, z = (cos(ks) − 1)/k. With k = 2π over the length the strip is a closed circle; as k falls to zero it straightens keeping its length, like real paper. With back.out the curvature dips a little below zero and the paper bends the other way before it settles.

The neck, the folds and the twist

Everything else is a push along the arc’s normal: towards the axis where the candy narrows to the neck, in and out with a triangle wave for the gathered folds (PLEATS), a little for the wrinkles of the flat sheet. The twist slides s by an angle times the radius, growing along each end and opposite on the two sides: as the ends untwist the number goes to zero and the ends turn opposite ways.

Foil in WebGL

The normals come from the grid (differences between neighbours). In the fragment shader a cellular noise gives every facet of the foil its own tilt, blended with its neighbours so the crinkles are crisp but not cut glass, and longer on the ends; the reflection of a small photo studio (a big soft light, a strip, ceiling and floor) colours the metal. The inside is silver paper, the closed folds darken, and the cream print is matt.

The print and the real menu

The frame, the shop’s name and the word Menu are on a texture drawn once on a canvas as big as the flat sheet; Menu is drawn squeezed the way the wrapping squeezes it, so on the candy it reads right, and it fades as the paper opens. The entries are real links over the flat sheet: they appear with a GSAP timeline at the end of the opening and, under the pointer, hand the shader the point the ripple starts from and the box of the glint.

Values to change

NameFileDefaultWhat it changes
OPENscript.js1.7The seconds of the whole unwrapping (wrapping up again is a little quicker).
TWISTscript.js2.6How far each end is twisted, in radians.
PLEATSscript.js11How many folds the paper makes, gathered round the ends.
CRINKLEscript.js0.34How strong the crinkles of the foil are, the little facets that glint.
WRINKLEscript.js1.8How high the wrinkles left on the flat sheet are, in pixels.
RIPPLEscript.js2How high the ripple under the entry pointed at is, in pixels.
SHINEscript.js1How bright the glint running over the foil is.
TILTscript.js[24, -12]The degrees the paper leans back and turns while it unrolls.
SETTLEscript.jselastic.out(1, 0.42)How the ends settle when they twist again: the bounce.

Try them in the Playground

When to use it

For the site of a patisserie, a confectioner, a chocolate factory or a gift shop: wherever the menu can be a little sweet to unwrap. With four to six short entries; for a long menu the sheet is not enough.

On phones

The box is at most the screen minus 32 pixels wide and the sheet is worked out again at every change of width. A tap opens and closes; without a pointer the candy does not roll while waiting, and the entry tapped ripples and glints before the paper wraps up.

With reduced motion

No twisting or unrolling: the candy and the flat sheet swap in a fade, the entries appear without moving, the foil does not ripple (the glint, being only light, stays).

Pitfalls

  • Seen head on, a tube unrolling looks like a rectangle growing: the curvature does not show. While opening, the paper leans back and turns a little (TILT), and then the arc reads.
  • Crinkles as sharp facets made the flat sheet look like broken glass and drowned the entries: the tilts of neighbouring facets are blended weighted by distance, and on the flat sheet the small crinkles soften, leaving the big ones.
  • Stretching the cellular noise by a factor changing along the paper squeezed the cells into vertical lines where the body meets the ends: two noises are worked out, a plain one and a stretched one, and blended.
  • Leaning towards the big light, the whole sheet reflected white: a soft shoulder on the brightest colours keeps the glints in the colour of the foil.
  • Wrapping up with the same timeline played backwards, the last gesture (the twist) came with its ease reversed, with no bounce: once wrapped, a separate tween takes the ends a little too far and lets them come back with elastic.

Libraries

GSAP 3.15.0, Fraunces 5.3.0.

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

Testing

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