Phone mockup
Pure CSSA phone with real thickness: screen, back, four thin walls and a stack of rounded slabs that fills the corners. It turns to show both sides.
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.
Your edited version
rotateX(-90deg), pushed down by half the wall height.transform-origin: 50% 100%. rotateX(-90deg) lays it over the opening; rotateX(24deg) swings it open past vertical.pointer-events: none. A hovered element that moves away from the pointer would flicker.lid hinged with transform-origin: bottomopen-top box from 4 walls + basetransition-delay swaps between open and closestatic hit area<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>
.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); }
}
$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);
}
}