Episode 7 of 8

CSS Christmas Present

Build an animated CSS Christmas present that opens its lid on hover and closes when the mouse leaves — using transforms and transitions.

Build a CSS Christmas Present that Opens & Closes

In this episode you will build a CSS-only Christmas present that opens its lid on hover to reveal a surprise inside, and closes smoothly when the mouse leaves. We will use CSS transforms, transitions, and transform-origin to animate the lid.

The HTML Structure

<div class="present">
    <div class="present-lid">
        <div class="present-bow"></div>
    </div>
    <div class="present-box">
        <div class="present-ribbon"></div>
        <div class="present-content">
            🎁 Surprise!
        </div>
    </div>
</div>

The present is split into two parts — the lid (which will animate) and the box (which stays in place). The bow sits on top of the lid, and the ribbon runs down the center of the box. The content is hidden inside the box.

The Present Box

.present {
    position: relative;
    width: 200px;
    margin: 100px auto;
    cursor: pointer;
}

.present-box {
    width: 200px;
    height: 160px;
    background: #e74c3c;
    border-radius: 0 0 8px 8px;
    position: relative;
    overflow: hidden;
    z-index: 1;
}

The box is a simple red rectangle with rounded bottom corners. overflow: hidden hides the surprise content inside until the box opens.

The Ribbon

.present-ribbon {
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 30px;
    height: 100%;
    background: #f1c40f;
}

/* Horizontal ribbon band */
.present-box::before {
    content: "";
    position: absolute;
    top: 50%;
    left: 0;
    width: 100%;
    height: 30px;
    background: #f1c40f;
    transform: translateY(-50%);
    z-index: 1;
}

A vertical yellow stripe runs down the center and a horizontal stripe crosses the middle, forming the classic cross-ribbon pattern on a gift box.

The Lid

.present-lid {
    width: 220px;
    height: 40px;
    background: #c0392b;
    position: relative;
    left: -10px;
    border-radius: 4px;
    z-index: 2;
    transform-origin: right bottom;
    transition: transform 0.6s ease;
}

/* Ribbon on the lid */
.present-lid::after {
    content: "";
    position: absolute;
    top: 0;
    left: 50%;
    transform: translateX(-50%);
    width: 30px;
    height: 100%;
    background: #f1c40f;
    border-radius: 4px 4px 0 0;
}

The lid is slightly wider than the box (220px vs 200px) and offset with left: -10px to overhang equally on both sides. The critical property is transform-origin: right bottom — this makes the lid pivot from its right-bottom corner, like a hinge on the right side of the box.

The Bow

.present-bow {
    position: absolute;
    top: -25px;
    left: 50%;
    transform: translateX(-50%);
    width: 40px;
    height: 20px;
}

.present-bow::before,
.present-bow::after {
    content: "";
    position: absolute;
    width: 30px;
    height: 30px;
    border: 5px solid #f1c40f;
    border-radius: 50% 50% 0;
    top: -5px;
}

.present-bow::before {
    left: -5px;
    transform: rotate(-45deg);
}

.present-bow::after {
    right: -5px;
    transform: rotate(45deg);
}

The bow is built entirely from two pseudo-elements. Each one is a rounded square (using border-radius: 50% 50% 0) rotated 45 degrees in opposite directions. Together they form the two loops of a ribbon bow on top of the lid.

The Hidden Content

.present-content {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-size: 32px;
    opacity: 0;
    transition: opacity 0.4s ease 0.3s;
}

The surprise text is centered inside the box and starts invisible. The 0.3s transition delay ensures it fades in only after the lid has started opening.

Opening on Hover

.present:hover .present-lid {
    transform: rotate(-120deg);
}

.present:hover .present-content {
    opacity: 1;
}

On hover, the lid rotates -120 degrees from its right-bottom hinge point, swinging open to the left. The content fades in with a slight delay, creating the effect of peering inside the gift box.

The Closing Animation

Since we defined the transition on the base state (not on :hover), the lid smoothly closes when the mouse leaves. The same transition that opened the lid now reverses, and the content fades back out.

Adding a Shake Effect Before Opening

@keyframes shake {
    0%, 100% { transform: rotate(0); }
    20%  { transform: rotate(-3deg); }
    40%  { transform: rotate(3deg); }
    60%  { transform: rotate(-2deg); }
    80%  { transform: rotate(2deg); }
}

.present:hover {
    animation: shake 0.5s ease;
}

.present:hover .present-lid {
    transform: rotate(-120deg);
    transition-delay: 0.4s;
}

A quick shake animation plays when the user hovers, and the lid opening is delayed by 0.4 seconds, creating the effect of the present wriggling before bursting open.

Key Takeaways

  • transform-origin controls the hinge point — setting it to right bottom makes the lid swing open from the right side
  • Transitions defined on the base state (not :hover) animate both opening and closing automatically
  • Using transition-delay on the content creates a sequenced effect — lid opens first, then content appears
  • Pseudo-elements (::before and ::after) with rotated borders create the bow loops without extra HTML
  • Chaining a shake @keyframes animation with a delayed hover transition creates a playful two-step interaction
  • The lid is wider than the box for a realistic gift-box overhang effect