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.
A UI mockup cut into depth layers: window, sidebar, cards and a floating button each sit at their own translateZ. Hover or focus pulls them further apart.
Your edited version
rotateX(55deg) rotateZ(-38deg). Its layers are flat siblings inside it, each lifted along the window’s own Z axis.style="--z:40") and shares a single rule: translateZ(calc(var(--z) * var(--k) * var(--u))), where --u is the one base unit every length here is a multiple of, so the window is the same share of a gallery card, the editor and a recording canvas.--k on the wrapper (0.45 → 1). Every layer’s transform changes with it and its own transition animates it; a delay of --z × 2ms lets the top layers rise last.pointer-events: none, so the bobbing never slides out from under the pointer.rotateX + rotateZ isometric tilttranslateZ(calc(var(--z) * var(--k) * var(--u)))one multiplier changes on :hoverstatic hit area, bobbing child<div class="scene">
<div class="browser" tabindex="0" role="img" aria-label="Browser window whose layers lift apart on hover or focus">
<div class="win">
<i class="shadow"></i>
<div class="frame"><span></span><span></span><span></span><em></em></div>
<div class="side" style="--z:14"><i></i><i></i><i></i><i></i></div>
<i class="card hero" style="--z:26"></i>
<i class="card a" style="--z:40"></i>
<i class="card b" style="--z:40"></i>
<b class="fab" style="--z:58"></b>
</div>
</div>
</div>
.scene {
/* one base unit: every length below is a multiple of it, so the window is the same share of a
card, the editor, a full screen and a recording canvas */
--u: 0.47vmin;
perspective: calc(800 * var(--u));
}
/* static hit area: it only changes the multiplier --k */
.browser {
--k: 0.45;
display: grid;
place-items: center;
width: calc(210 * var(--u));
height: calc(184 * var(--u));
border-radius: calc(16 * var(--u));
outline-offset: calc(-4 * var(--u));
cursor: pointer;
transform-style: preserve-3d;
}
.browser:hover,
.browser:focus-visible {
--k: 1;
}
/* 152 × 112, tilted and bobbing */
.win {
position: relative;
width: calc(152 * var(--u));
height: calc(112 * var(--u));
pointer-events: none;
transform-style: preserve-3d;
animation: bob 3.2s ease-in-out infinite alternate;
}
/* every depth layer: one transform from --z × --k, in base units, staggered by depth */
.win > [style] {
position: absolute;
box-sizing: border-box;
transform: translateZ(calc(var(--z) * var(--k) * var(--u)));
transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
transition-delay: calc(var(--z) * 2ms);
}
.shadow {
position: absolute;
inset: calc(4 * var(--u)) calc(-6 * var(--u)) calc(-8 * var(--u)) calc(4 * var(--u));
border-radius: calc(14 * var(--u));
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: translateZ(calc(var(--k) * -24 * var(--u))); /* sinks as the layers rise */
transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
}
/* the window at z = 0: title bar with traffic lights and an address bar */
.frame {
position: absolute;
inset: 0;
display: flex;
flex-wrap: wrap;
align-items: center;
align-content: flex-start;
gap: calc(3 * var(--u));
padding: calc(5 * var(--u)) calc(6 * var(--u));
border: calc(1 * var(--u)) solid rgb(140 150 220 / 0.34);
border-radius: calc(10 * var(--u));
background:
linear-gradient(#2a2d44 0 calc(16 * var(--u)), transparent calc(16 * var(--u))),
#222249;
}
.frame span {
width: calc(6 * var(--u));
height: calc(6 * var(--u));
border-radius: 50%;
background: #ff4d9d;
}
.frame span:nth-child(2) { background: #ffb547; }
.frame span:nth-child(3) { background: #2ee6d6; }
.frame em {
width: calc(62 * var(--u));
height: calc(7 * var(--u));
margin-left: calc(14 * var(--u));
border-radius: calc(4 * var(--u));
background: rgb(236 238 251 / 0.16);
}
.side {
top: calc(23 * var(--u));
left: calc(7 * var(--u));
display: grid;
align-content: start;
gap: calc(6 * var(--u));
width: calc(30 * var(--u));
height: calc(82 * var(--u));
padding: calc(6 * var(--u)) calc(5 * var(--u));
border: calc(1 * var(--u)) solid rgb(139 108 255 / 0.45);
border-radius: calc(6 * var(--u));
background: #2e2a5e;
}
.side i { height: calc(4 * var(--u)); border-radius: calc(2 * var(--u)); background: rgb(236 238 251 / 0.3); }
.side i:first-child { background: #8b6cff; }
.side i:nth-child(3) { width: 70%; }
.card {
border: calc(1 * var(--u)) solid rgb(255 255 255 / 0.35);
border-radius: calc(6 * var(--u));
}
/* hero banner with two lines of "text" */
.hero {
top: calc(23 * var(--u));
left: calc(44 * var(--u));
width: calc(101 * var(--u));
height: calc(32 * var(--u));
background:
linear-gradient(rgb(255 255 255 / 0.9) 0 0) calc(8 * var(--u)) calc(9 * var(--u)) / 42% calc(4 * var(--u)) no-repeat,
linear-gradient(rgb(255 255 255 / 0.55) 0 0) calc(8 * var(--u)) calc(17 * var(--u)) / 28% calc(3 * var(--u)) no-repeat,
linear-gradient(120deg, #8b6cff, #ff4d9d);
}
.a,
.b {
top: calc(62 * var(--u));
width: calc(47 * var(--u));
height: calc(43 * var(--u));
}
/* mini bar chart */
.a {
left: calc(44 * var(--u));
padding: calc(7 * var(--u)) 0 calc(5 * var(--u));
background:
linear-gradient(0deg, #2ee6d6 0 45%, transparent 45%) calc(7 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
linear-gradient(0deg, #2ee6d6 0 70%, transparent 70%) calc(15 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
linear-gradient(0deg, #2ee6d6 0 55%, transparent 55%) calc(23 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
linear-gradient(0deg, #2ee6d6 0 85%, transparent 85%) calc(31 * var(--u)) 100% / calc(5 * var(--u)) 100% no-repeat,
#193d4e;
background-origin: content-box;
}
/* donut: a conic square rounded off by two radial layers in the card colour */
.b {
--paper: #3e3434;
right: calc(7 * var(--u));
background:
radial-gradient(circle, var(--paper) 0 calc(6 * var(--u)), transparent calc(6.5 * var(--u)) calc(11 * var(--u)), var(--paper) calc(11.5 * var(--u))),
conic-gradient(#ffb547 0 70%, #785b3a 0) 50% 50% / calc(24 * var(--u)) calc(24 * var(--u)) no-repeat,
var(--paper);
}
/* the floating button; rotate(38deg) undoes the window's rotateZ so "+" stays a plus */
.win > .fab {
right: calc(12 * var(--u));
bottom: calc(12 * var(--u));
display: grid;
place-items: center;
width: calc(22 * var(--u));
height: calc(22 * var(--u));
border-radius: 50%;
background: linear-gradient(135deg, #ff4d9d, #8b6cff);
box-shadow: 0 calc(2 * var(--u)) calc(6 * var(--u)) rgb(0 0 0 / 0.35);
transform: translateZ(calc(var(--z) * var(--k) * var(--u))) rotate(38deg);
}
/* the "+" is two bars in one grid cell: exactly centred, unlike a text glyph */
.win > .fab::before,
.win > .fab::after {
content: '';
grid-area: 1 / 1;
width: calc(10 * var(--u));
height: calc(2 * var(--u));
border-radius: calc(1 * var(--u));
background: #fff;
}
.win > .fab::after {
rotate: 90deg;
}
/* translateY first: the bob is straight up and down on screen. Measured by what it paints, the
window's solid body already sits a little low in its layout box, and hovering lifts the
layers up the screen (they rise towards the viewer, which is up the screen at this tilt), so
the bob runs from the box to 8 units above it: the paused window, at the bottom of the swing,
is centred, and so is the whole swing, raised layers and all. */
@keyframes bob {
from { transform: translateY(0) rotateX(55deg) rotateZ(-38deg); }
to { transform: translateY(calc(-8 * var(--u))) rotateX(55deg) rotateZ(-38deg); }
}