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.
.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 page — this 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