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) * 1px)).--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) * 1px))one multiplier changes on :hoverstatic hit area, bobbing child<div class="scene">
<div class="browser" tabindex="0">
<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 {
perspective: 800px;
}
/* static hit area: it only changes the multiplier --k */
.browser {
--k: 0.45;
display: grid;
place-items: center;
width: 210px;
height: 184px;
border-radius: 16px;
outline-offset: -4px;
cursor: pointer;
transform-style: preserve-3d;
}
.browser:hover,
.browser:focus-visible {
--k: 1;
}
/* 152 × 112, tilted and bobbing */
.win {
position: relative;
width: 152px;
height: 112px;
pointer-events: none;
transform-style: preserve-3d;
animation: bob 3.2s ease-in-out infinite alternate;
}
/* every depth layer: one transform from --z × --k, staggered by depth */
.win > [style] {
position: absolute;
box-sizing: border-box;
transform: translateZ(calc(var(--z) * var(--k) * 1px));
transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
transition-delay: calc(var(--z) * 2ms);
}
.shadow {
position: absolute;
inset: 4px -6px -8px 4px;
border-radius: 14px;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: translateZ(calc(var(--k) * -24px)); /* 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: 3px;
padding: 5px 6px;
border: 1px solid rgb(140 150 220 / 0.34);
border-radius: 10px;
background:
linear-gradient(#2a2d44 0 16px, transparent 16px),
#222249;
}
.frame span { width: 6px; height: 6px; border-radius: 50%; background: #ff4d9d; }
.frame span:nth-child(2) { background: #ffb547; }
.frame span:nth-child(3) { background: #2ee6d6; }
.frame em {
width: 62px;
height: 7px;
margin-left: 14px;
border-radius: 4px;
background: rgb(236 238 251 / 0.16);
}
.side {
top: 23px;
left: 7px;
display: grid;
align-content: start;
gap: 6px;
width: 30px;
height: 82px;
padding: 6px 5px;
border: 1px solid rgb(139 108 255 / 0.45);
border-radius: 6px;
background: #2e2a5e;
}
.side i { height: 4px; border-radius: 2px; background: rgb(236 238 251 / 0.3); }
.side i:first-child { background: #8b6cff; }
.side i:nth-child(3) { width: 70%; }
.card {
border: 1px solid rgb(255 255 255 / 0.35);
border-radius: 6px;
}
/* hero banner with two lines of "text" */
.hero {
top: 23px;
left: 44px;
width: 101px;
height: 32px;
background:
linear-gradient(rgb(255 255 255 / 0.9) 0 0) 8px 9px / 42% 4px no-repeat,
linear-gradient(rgb(255 255 255 / 0.55) 0 0) 8px 17px / 28% 3px no-repeat,
linear-gradient(120deg, #8b6cff, #ff4d9d);
}
.a,
.b {
top: 62px;
width: 47px;
height: 43px;
}
/* mini bar chart */
.a {
left: 44px;
padding: 7px 0 5px;
background:
linear-gradient(0deg, #2ee6d6 0 45%, transparent 45%) 7px 100% / 5px 100% no-repeat,
linear-gradient(0deg, #2ee6d6 0 70%, transparent 70%) 15px 100% / 5px 100% no-repeat,
linear-gradient(0deg, #2ee6d6 0 55%, transparent 55%) 23px 100% / 5px 100% no-repeat,
linear-gradient(0deg, #2ee6d6 0 85%, transparent 85%) 31px 100% / 5px 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: 7px;
background:
radial-gradient(circle, var(--paper) 0 6px, transparent 6.5px 11px, var(--paper) 11.5px),
conic-gradient(#ffb547 0 70%, #785b3a 0) 50% 50% / 24px 24px no-repeat,
var(--paper);
}
/* the floating button; rotate(38deg) undoes the window's rotateZ so "+" stays a plus */
.win > .fab {
right: 12px;
bottom: 12px;
display: grid;
place-items: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, #ff4d9d, #8b6cff);
box-shadow: 0 2px 6px rgb(0 0 0 / 0.35);
transform: translateZ(calc(var(--z) * var(--k) * 1px)) 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: 10px;
height: 2px;
border-radius: 1px;
background: #fff;
}
.win > .fab::after {
rotate: 90deg;
}
/* translateY first: the bob is straight up and down on screen */
@keyframes bob {
from { transform: translateY(8px) rotateX(55deg) rotateZ(-38deg); }
to { transform: translateY(0) rotateX(55deg) rotateZ(-38deg); }
}
$w: 152px;
$h: 112px;
$bar: 16px; // title bar
// Static hit area. It only changes one number, --k, and every layer multiplies its own depth
// (--z, set in the markup) by it.
.d-browser {
--k: 0.45;
display: grid;
place-items: center;
width: 210px;
height: 184px;
border-radius: 16px;
outline-offset: -4px;
cursor: pointer;
transform-style: preserve-3d;
&:hover,
&:focus-visible {
--k: 1;
}
// The tilted window. It bobs, the layers inside ride along.
&__win {
position: relative;
width: $w;
height: $h;
pointer-events: none;
transform-style: preserve-3d;
animation: d-browser-bob 3.2s ease-in-out infinite alternate;
// every depth layer: one transform, driven by --z × --k, staggered so the top rises last
> [style] {
position: absolute;
transform: translateZ(calc(var(--z) * var(--k) * 1px));
transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
transition-delay: calc(var(--z) * 2ms);
}
}
// shadow on the "desk", sinking further as the layers rise
&__shadow {
position: absolute;
inset: 4px -6px -8px 4px;
border-radius: 14px;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: translateZ(calc(var(--k) * -24px));
transition: transform 0.6s cubic-bezier(0.3, 1.3, 0.5, 1);
}
// the window itself, z = 0: glass panel, title bar with traffic lights and an address bar
&__frame {
position: absolute;
inset: 0;
display: flex;
align-items: center;
align-content: flex-start;
flex-wrap: wrap;
gap: 3px;
padding: 5px 6px;
border: 1px solid var(--border-strong);
border-radius: 10px;
background:
linear-gradient(color-mix(in srgb, var(--text) 10%, var(--surface-solid)) 0 $bar, transparent $bar),
color-mix(in srgb, var(--surface-solid) 88%, var(--accent));
span {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--hot);
&:nth-child(2) {
background: var(--warm);
}
&:nth-child(3) {
background: var(--accent-2);
}
}
em {
width: 62px;
height: 7px;
margin-left: 14px;
border-radius: 4px;
background: color-mix(in srgb, var(--text) 16%, transparent);
}
}
// sidebar with four nav rows, the first one "active"
&__side {
top: $bar + 7px;
left: 7px;
display: grid;
align-content: start;
gap: 6px;
width: 30px;
height: $h - $bar - 14px;
padding: 6px 5px;
border: 1px solid color-mix(in srgb, var(--accent) 45%, transparent);
border-radius: 6px;
background: color-mix(in srgb, var(--accent) 22%, var(--surface-solid));
i {
height: 4px;
border-radius: 2px;
background: color-mix(in srgb, var(--text) 30%, transparent);
&:first-child {
background: var(--accent);
}
&:nth-child(3) {
width: 70%;
}
}
}
&__card {
border: 1px solid rgb(255 255 255 / 0.35);
border-radius: 6px;
// hero banner: gradient with two lines of "text"
&--hero {
top: $bar + 7px;
left: 44px;
width: $w - 51px;
height: 32px;
background:
linear-gradient(rgb(255 255 255 / 0.9) 0 0) 8px 9px / 42% 4px no-repeat,
linear-gradient(rgb(255 255 255 / 0.55) 0 0) 8px 17px / 28% 3px no-repeat,
linear-gradient(120deg, var(--accent), var(--hot));
}
&--a,
&--b {
top: $bar + 46px;
width: ($w - 58px) * 0.5;
height: $h - $bar - 53px;
}
// mini bar chart
&--a {
left: 44px;
background:
linear-gradient(0deg, var(--accent-2) 0 45%, transparent 45%) 7px 100% / 5px 100% no-repeat,
linear-gradient(0deg, var(--accent-2) 0 70%, transparent 70%) 15px 100% / 5px 100% no-repeat,
linear-gradient(0deg, var(--accent-2) 0 55%, transparent 55%) 23px 100% / 5px 100% no-repeat,
linear-gradient(0deg, var(--accent-2) 0 85%, transparent 85%) 31px 100% / 5px 100% no-repeat,
color-mix(in srgb, var(--accent-2) 18%, var(--surface-solid));
background-origin: content-box;
padding: 7px 0 5px;
}
// donut stat: a conic square, rounded off by two radial layers in the card colour
&--b {
--paper: color-mix(in srgb, var(--warm) 18%, var(--surface-solid));
right: 7px;
background:
radial-gradient(circle, var(--paper) 0 6px, transparent 6.5px 11px, var(--paper) 11.5px),
conic-gradient(var(--warm) 0 70%, color-mix(in srgb, var(--warm) 30%, var(--paper)) 0) 50% 50% / 24px 24px no-repeat,
var(--paper);
}
}
// the floating action button, highest layer
&__fab {
right: 12px;
bottom: 12px;
display: grid;
place-items: center;
width: 22px;
height: 22px;
border-radius: 50%;
background: linear-gradient(135deg, var(--hot), var(--accent));
box-shadow: 0 2px 6px rgb(0 0 0 / 0.35);
// the "+" is two drawn bars in the same grid cell: exactly centred (a text glyph sits on the
// font's baseline, so it never is)
&::before,
&::after {
content: '';
grid-area: 1 / 1;
width: 10px;
height: 2px;
border-radius: 1px;
background: #fff;
}
&::after {
rotate: 90deg;
}
// same lift as every layer, then undo the window's rotateZ so the "+" does not read as "×"
// ([style] matches the specificity of the layer rule above, and comes later)
&[style] {
transform: translateZ(calc(var(--z) * var(--k) * 1px)) rotate(38deg);
}
}
}
// translateY comes first, so the bob is straight up and down on screen, not along the tilted axes
@keyframes d-browser-bob {
from {
transform: translateY(8px) rotateX(55deg) rotateZ(-38deg);
}
to {
transform: translateY(0) rotateX(55deg) rotateZ(-38deg);
}
}