Blog / CSS & Interactions

Gift Unwrap Animation in CSS/JS: sequencing with transition-delay

A gift that spins on itself, whose lid lifts up and from which a promo code emerges — with sparkles — and that closes back in reverse order. All in vanilla CSS/JS, thanks to sequencing with transition-delay.

Unwrapping a gift is a sequence: the box spins, the lid lifts, the content appears. Reproducing this cleanly on the web raises a genuine question — how to chain multiple animations in the right order, and even reverse them on close, without a jungle of setTimeout calls? This article describes the Gift Unwrap Reveal effect, the free effect from the December 2026 Christmas sprint on Effect.Labs, in CSS + vanilla JavaScript, accessible.

The key idea. We do not control timing in JavaScript. We toggle a single class (.guw-open) and it is transition-delay, element by element, that orchestrates the sequence — including a different order on close.

The structure

Four stacked pieces, plus a wrapper that spins: the content (hidden behind the box), the box body, the lid, the bow.

<button class="guw-gift">
  <span class="guw-inner">            <!-- wrapper that performs the 360° spin -->
    <span class="guw-code">🎄 XMAS30</span>  <!-- z behind the box: hidden -->
    <span class="guw-box"></span>       <!-- body (covers the code) -->
    <span class="guw-lid"></span>       <!-- lid -->
    <span class="guw-bow">🎀</span>
  </span>
</button>

The 360° spin

The .guw-inner wrapper rotates a full turn. Since 360° equals the starting orientation, the box returns to its position: a satisfying twirl that always faces the user.

.guw-inner { transition: transform .75s cubic-bezier(.45,.05,.2,1); }
.guw-open .guw-inner { transform: rotate(360deg); }

Sequencing with transition-delay

Each piece has an increasing delay: the box spins (0), then the lid lifts (.78s), then the code emerges (1s). A single class change, zero setTimeout.

.guw-open .guw-inner { transform: rotate(360deg);                 transition-delay: 0s;   }
.guw-open .guw-lid   { transform: translateY(-86px) rotate(-10deg); transition-delay: .78s; }
.guw-open .guw-code  { transform: translateY(-104px) scale(1);      transition-delay: 1s;   }

The trick: reversed order on close

On close, we want the opposite: the code retracts first, then the lid closes. The key: a transition reads its delay from the TARGET state. We therefore put the opening delays on .guw-open, and the closing delays (reversed) on the base state.

/* base state = what applies on CLOSE */
.guw-code { transition: transform .55s ...; transition-delay: 0s;   } /* code retracts first */
.guw-lid  { transition: transform .5s  ...; transition-delay: .55s; } /* lid after */
.guw-inner{ transition: transform .75s ...; transition-delay: .9s;  } /* return spin last */

Result: open = spin → lid → code; close = code → lid → spin. The full code is available on the Hover catalog pagethis effect is free.

Hide then reveal (z-index)

The promo code is placed behind the box body (lower z-index), in the same stacking context. While it sits within the box area it is occluded; when it rises above the top edge it becomes visible — with no opacity fade whatsoever.

Sparkles & accessibility

On open, a dozen small sparkles burst out (simple animated <span> elements, auto-cleaned). On the accessibility side:

  • A real <button>: keyboard-operable (Enter/Space), announced by screen readers.
  • prefers-reduced-motion: the spin, fly-out and sparkles are skipped — the content is revealed directly.

What ChatGPT and v0 don't do

Ask a code generator for a "gift animation": you often get a fragile stack of setTimeout calls, and never the reversed close order. What is rarely mastered:

  • Sequencing via transition-delay (instead of JS timers) — robust and declarative.
  • Asymmetric delays open/close (reading the delay from the target state): almost never proposed.
  • z-index occlusion within the same stacking context to hide/reveal without a fade.
  • prefers-reduced-motion and particle cleanup: frequently forgotten.

FAQ

Can I use it with React, Vue or Svelte?

Yes: CSS + vanilla JavaScript, no npm dependency. In React, manage the open/closed state with useState and toggle a class; the sequencing stays 100% CSS.

How do I change the revealed content?

The content is a simple element (here a promo code); replace it with a message, a voucher, a visual… The animation is content-agnostic, driven by data-code.

The complete, ready-to-use code

This effect is free this month. Copy-paste the code and adapt the revealed content. Access 800+ other premium effects with the subscription.

Join the founders — €9.90 / month