Pure CSS Products & branding #hover #product #box #packaging

Unboxing in pure CSS

Hover or focus: the lid swings open on its back edge and the product card rises out. One hinge (transform-origin) and two staggered transitions.

  • 136 lines of CSS
  • 13 lines of HTML
  • No JavaScript
  • No dependencies
  • MIT licensed

How it works

  1. An open-top box: four walls around the centre and a base laid flat with rotateX(-90deg), pushed down by half the wall height.
  2. The lid stands above the back wall and is hinged on its bottom edge with transform-origin: 50% 100%. rotateX(-90deg) lays it over the opening; rotateX(24deg) swings it open past vertical.
  3. Two transitions, swapped delays: opening, the lid goes first and the card rises 0.3s later; closing, the card drops first and the lid waits 0.3s. Each state carries the delay for the move into it.
  4. The hovered element is a static wrapper; the box inside has pointer-events: none. A hovered element that moves away from the pointer would flicker.

Key techniques

  • lid hinged with transform-origin: bottom
  • open-top box from 4 walls + base
  • transition-delay swaps between open and close
  • static hit area

Copy-paste code

HTML 13 lines

<div class="scene">
  <div class="package" tabindex="0">
    <div class="box">
      <i class="wall back"></i>
      <i class="wall left"></i>
      <i class="wall right"></i>
      <i class="base"></i>
      <div class="card"><b></b><span>NEW</span></div>
      <i class="wall front"><em>CUBE · 01</em></i>
      <i class="lid"></i>
    </div>
  </div>
</div>

CSS 136 lines

.scene {
  perspective: 800px;
}

/* the static hit area */
.package {
  --card: #4a3e8d;
  --card-dark: #2e2a5e;
  display: grid;
  place-items: center;
  width: 200px;
  height: 186px;
  border-radius: 16px;
  outline-offset: -4px;
  cursor: pointer;
  transform-style: preserve-3d;
}

/* 88 wide, 88 deep, 60 tall */
.box {
  position: relative;
  width: 88px;
  height: 60px;
  pointer-events: none;
  transform-style: preserve-3d;
  animation: sway 5s ease-in-out infinite alternate;
}

.wall {
  position: absolute;
  inset: 0;
  border: 1px solid #a893fe;
  /* a ribbon down the middle */
  background:
    linear-gradient(90deg, transparent 42%, #ff4d9d 42% 58%, transparent 58%),
    linear-gradient(var(--card), var(--card-dark));
}

.front {
  display: grid;
  place-items: end start;
  padding: 5px 6px;
  backface-visibility: hidden; /* from behind you look INTO the box */
  transform: translateZ(44px);
}

.back { transform: rotateY(180deg) translateZ(44px); }
.left { transform: rotateY(-90deg) translateZ(44px); }
.right { transform: rotateY(90deg) translateZ(44px); }

.left,
.right {
  background: linear-gradient(var(--card-dark), #201d42);
}

.front em {
  color: #fff;
  font: normal 800 7px system-ui;
  letter-spacing: 1px;
}

/* an 88 × 88 square centred on the box, laid flat at the bottom edge */
.base {
  position: absolute;
  top: calc(50% - 44px);
  left: 0;
  width: 88px;
  height: 88px;
  background: #1c1938;
  transform: rotateX(-90deg) translateZ(30px);
}

/* stands above the back wall, hinged on its bottom edge = the top-back edge of the box */
.lid {
  position: absolute;
  top: -88px;
  left: 0;
  box-sizing: border-box;
  width: 88px;
  height: 88px;
  border: 1px solid #a48cff;
  background:
    linear-gradient(90deg, transparent 42%, #ff4d9d 42% 58%, transparent 58%),
    linear-gradient(#6e65a4, var(--card));
  transform-origin: 50% 100%;
  transform: translateZ(-44px) rotateX(-90deg);
  transition: transform 0.55s ease-in-out 0.3s; /* closing: wait for the card */
}

.card {
  position: absolute;
  top: 5px;
  left: calc(50% - 31px);
  display: grid;
  place-items: center;
  align-content: center;
  gap: 5px;
  box-sizing: border-box;
  width: 62px;
  height: 52px;
  border: 1px solid rgb(255 255 255 / 0.5);
  border-radius: 8px;
  background: linear-gradient(140deg, #2ee6d6, #8b6cff 60%, #ff4d9d);
  box-shadow: inset 0 0 14px rgb(255 255 255 / 0.25);
  color: #fff;
  font: 900 9px system-ui;
  letter-spacing: 1.5px;
  transition: transform 0.5s ease-in-out;
}

/* the "product": a little gem */
.card b {
  width: 16px;
  height: 16px;
  border-radius: 4px;
  background: linear-gradient(135deg, #fff, rgb(255 255 255 / 0.35));
  transform: rotate(45deg);
}

.package:hover .lid,
.package:focus-visible .lid {
  transform: translateZ(-44px) rotateX(24deg); /* past vertical, leaning back */
  transition: transform 0.7s cubic-bezier(0.3, 1.35, 0.5, 1);
}

/* opening: the card waits until the lid is out of the way */
.package:hover .card,
.package:focus-visible .card {
  transform: translateY(-64px);
  transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1) 0.3s;
}

@keyframes sway {
  from { transform: translateY(40px) rotateX(-24deg) rotateY(-38deg); }
  to   { transform: translateY(40px) rotateX(-24deg) rotateY(-22deg); }
}

Sass source 151 lines

$s: 88px; // width and depth
$h: 60px; // wall height

// The wrapper is the hit area and never moves (a hovered element that moves away from the
// pointer flickers). Everything that moves is inside and ignores the pointer.
.d-package {
  --card: color-mix(in srgb, var(--accent) 45%, var(--surface-solid));
  --card-dark: color-mix(in srgb, var(--accent) 22%, var(--surface-solid));
  display: grid;
  place-items: center;
  width: 200px;
  height: 186px;
  border-radius: 16px;
  outline-offset: -4px;
  cursor: pointer;
  transform-style: preserve-3d;

  &__box {
    position: relative;
    width: $s;
    height: $h;
    pointer-events: none;
    transform-style: preserve-3d;
    animation: d-package-sway 5s ease-in-out infinite alternate;
  }

  &__wall {
    position: absolute;
    inset: 0;
    border: 1px solid color-mix(in srgb, var(--accent) 70%, var(--text));
    // a ribbon runs down the middle of the front and back walls and across the lid
    background:
      linear-gradient(90deg, transparent 42%, var(--hot) 42% 58%, transparent 58%),
      linear-gradient(var(--card), var(--card-dark));

    &--front {
      display: grid;
      place-items: end start;
      padding: 5px 6px;
      backface-visibility: hidden; // from behind you look INTO the box, not at mirrored text
      transform: translateZ($s * 0.5);
    }

    &--back {
      transform: rotateY(180deg) translateZ($s * 0.5);
    }

    &--left,
    &--right {
      background: linear-gradient(var(--card-dark), color-mix(in srgb, var(--card-dark) 70%, #000));
    }

    &--left {
      transform: rotateY(-90deg) translateZ($s * 0.5);
    }

    &--right {
      transform: rotateY(90deg) translateZ($s * 0.5);
    }

    em {
      color: #fff;
      font-size: 7px;
      font-style: normal;
      font-weight: 800;
      letter-spacing: 1px;
    }
  }

  // $s × $s square, centred on the box, laid flat and pushed down to the bottom edge
  &__base {
    position: absolute;
    top: calc(50% - #{$s * 0.5});
    left: 0;
    width: $s;
    height: $s;
    background: color-mix(in srgb, var(--card-dark) 60%, #000);
    transform: rotateX(-90deg) translateZ($h * 0.5);
  }

  // The lid stands ABOVE the back wall (top: -$s) and is hinged on its bottom edge, which is
  // the top-back edge of the box. rotateX(-90deg) lays it forward over the opening.
  &__lid {
    position: absolute;
    top: -$s;
    left: 0;
    width: $s;
    height: $s;
    border: 1px solid color-mix(in srgb, var(--accent) 70%, var(--text));
    background:
      linear-gradient(90deg, transparent 42%, var(--hot) 42% 58%, transparent 58%),
      linear-gradient(color-mix(in srgb, var(--card) 80%, #fff), var(--card));
    transform-origin: 50% 100%;
    transform: translateZ($s * -0.5) rotateX(-90deg);
    // closing: wait for the card to drop back in first
    transition: transform 0.55s ease-in-out 0.3s;
  }

  &__card {
    position: absolute;
    top: 5px;
    left: calc(50% - 31px);
    display: grid;
    place-items: center;
    align-content: center;
    gap: 5px;
    width: 62px;
    height: 52px;
    border: 1px solid rgb(255 255 255 / 0.5);
    border-radius: 8px;
    background: linear-gradient(140deg, var(--accent-2), var(--accent) 60%, var(--hot));
    box-shadow: inset 0 0 14px rgb(255 255 255 / 0.25);
    color: #fff;
    font-size: 9px;
    font-weight: 900;
    letter-spacing: 1.5px;
    transition: transform 0.5s ease-in-out;

    // a little gem as the "product"
    b {
      width: 16px;
      height: 16px;
      border-radius: 4px;
      background: linear-gradient(135deg, #fff, rgb(255 255 255 / 0.35));
      transform: rotate(45deg);
    }
  }

  &:hover &__lid,
  &:focus-visible &__lid {
    transform: translateZ($s * -0.5) rotateX(24deg); // past vertical, leaning back
    transition: transform 0.7s cubic-bezier(0.3, 1.35, 0.5, 1);
  }

  // opening: the card waits until the lid is out of the way
  &:hover &__card,
  &:focus-visible &__card {
    transform: translateY(-64px);
    transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1) 0.3s;
  }
}

@keyframes d-package-sway {
  from {
    transform: translateY(40px) rotateX(-24deg) rotateY(-38deg);
  }

  to {
    transform: translateY(40px) rotateX(-24deg) rotateY(-22deg);
  }
}
Standalone snippet · plain CSS, no build step, no dependencies.