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.
Over-ear headphones turning on a stand: the band is a ring of tangent strips, each cup a short cylinder laid on its side. Hover or focus and they turn to face you and light up.
Your edited version
rotateZ, moves out by the radius with translateY(-58px) and folds flat with rotateX(90deg): together they are the top surface of an arch. Two masked rings close its sides.rotateY(i × 30deg) translateZ(r), a disc on each end), then laid on its side with rotateZ(±90deg) so the top disc faces outward.cos() / sin() of its own angle, so the strips facing up are the brightest.@property values animate: the lap angle --spin and a weight --k. The transform uses --spin × --k, and hover eases --k to 0, which glides from wherever the lap is to the front pose.pointer-events: none, so the hover never flickers.arc = strips at rotateZ(a) translateY(−r) rotateX(90deg)cylinder built upright, laid down with rotateZ(90deg)shading from sin() / cos() of the strip angle@property blend: hover eases --k from 1 to 0<div class="scene">
<div class="headphones" tabindex="0">
<div class="rig">
<i class="shadow"></i>
<div class="band">
<b class="side"></b><b class="side"></b>
<i style="--i:0"></i>
<i style="--i:1"></i>
<i style="--i:2"></i>
<i style="--i:3"></i>
<i style="--i:4"></i>
<i style="--i:5"></i>
<i style="--i:6"></i>
<i style="--i:7"></i>
<i style="--i:8"></i>
<i style="--i:9"></i>
<i style="--i:10"></i>
<i style="--i:11"></i>
</div>
<i class="slider slider-l"></i><i class="slider slider-lf"></i>
<i class="yoke yoke-l"></i>
<div class="cup cup-l">
<i style="--i:0"></i>
<i style="--i:1"></i>
<i style="--i:2"></i>
<i style="--i:3"></i>
<i style="--i:4"></i>
<i style="--i:5"></i>
<i style="--i:6"></i>
<i style="--i:7"></i>
<i style="--i:8"></i>
<i style="--i:9"></i>
<i style="--i:10"></i>
<i style="--i:11"></i>
<b class="outer"><em></em></b>
<b class="cushion"></b>
</div>
<i class="slider slider-r"></i><i class="slider slider-rf"></i>
<i class="yoke yoke-r"></i>
<div class="cup cup-r">
<i style="--i:0"></i>
<i style="--i:1"></i>
<i style="--i:2"></i>
<i style="--i:3"></i>
<i style="--i:4"></i>
<i style="--i:5"></i>
<i style="--i:6"></i>
<i style="--i:7"></i>
<i style="--i:8"></i>
<i style="--i:9"></i>
<i style="--i:10"></i>
<i style="--i:11"></i>
<b class="outer"><em></em></b>
<b class="cushion"></b>
</div>
</div>
</div>
</div>
/* registered, so they can be animated and transitioned */
@property --spin {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@property --k {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
.scene {
perspective: 800px;
}
/* the static hit area */
.headphones {
--shell: color-mix(in srgb, #8b6cff 34%, #17182a);
--shell-hi: color-mix(in srgb, #8b6cff 55%, #3a3d5c);
--leather: color-mix(in srgb, #8b6cff 14%, #1b1c29);
--leather-hi: color-mix(in srgb, #8b6cff 22%, #3a3c52);
--stitch: color-mix(in srgb, #8b6cff 80%, #fff);
--pad: #16171f;
--metal: #b8bdd2;
--metal-dark: #5c6280;
--glow: #2ee6d6;
display: grid;
place-items: center;
width: 216px;
height: 184px;
border-radius: 16px;
cursor: pointer;
transform-style: preserve-3d;
}
/* 0 × 0: every part is placed around this point */
.rig {
position: relative;
width: 0;
height: 0;
pointer-events: none;
transform-style: preserve-3d;
/* the front pose (-28deg) plus the lap, weighted by --k */
transform: rotateX(-14deg) rotateY(calc(-28deg + var(--spin) * var(--k)));
animation: turn 16s linear infinite;
transition: --k 1.1s cubic-bezier(0.45, 0, 0.2, 1);
}
.headphones:hover .rig,
.headphones:focus-visible .rig {
--k: 0; /* the lap keeps running, it just stops counting */
}
.shadow {
position: absolute;
left: -80px;
top: 34px;
width: 160px;
height: 80px;
border-radius: 50%;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: rotateX(90deg);
}
/* --- the band: 12 strips over 200deg, radius 58px --- */
.band {
position: absolute;
top: -22px;
transform-style: preserve-3d;
}
.band i {
--a: calc(-100deg + (var(--i) + 0.5) * 16.667deg);
position: absolute;
left: -8.8px;
top: -11px;
width: 17.6px; /* one side of the polygon, a hair wider: no seams */
height: 22px; /* the band's width, front to back */
background:
linear-gradient(rgb(0 0 0 / calc(0.5 - 0.46 * cos(var(--a)))) 0 0), /* shade: none on top */
repeating-linear-gradient(90deg, var(--stitch) 0 2px, transparent 2px 4px) 0 3px / 100% 1px no-repeat,
repeating-linear-gradient(90deg, var(--stitch) 0 2px, transparent 2px 4px) 0 18px / 100% 1px no-repeat,
linear-gradient(var(--leather), var(--leather-hi) 45%, var(--leather));
transform: rotateZ(var(--a)) translateY(-58px) rotateX(90deg);
}
/* the band's side walls: a ring, masked to the same 200deg */
.side {
position: absolute;
left: -58px;
top: -58px;
width: 116px;
height: 116px;
background: radial-gradient(closest-side, transparent calc(100% - 6px), var(--leather) calc(100% - 5.5px), color-mix(in srgb, var(--leather) 70%, #000) 100%, transparent 100%);
mask: conic-gradient(from -100deg, #000 0 200deg, transparent 0);
transform: translateZ(11px);
}
.side + .side {
transform: translateZ(-11px);
}
/* --- metal: sliders (two crossed planes) and yokes (half rings) --- */
.slider {
position: absolute;
top: -18px;
left: -5px;
width: 10px;
height: 20px;
background: linear-gradient(90deg, var(--metal-dark), var(--metal) 45%, #eef0fa 55%, var(--metal-dark));
}
.slider-l { transform: translateX(-55px) rotateY(90deg); }
.slider-r { transform: translateX(55px) rotateY(90deg); }
.slider-lf { left: -1.5px; width: 3px; transform: translateX(-55px); }
.slider-rf { left: -1.5px; width: 3px; transform: translateX(55px); }
.yoke {
position: absolute;
left: -35px;
top: 0;
width: 70px;
height: 70px;
background: radial-gradient(closest-side, transparent calc(100% - 4px), var(--metal) calc(100% - 3.5px), var(--metal-dark) 100%, transparent 100%);
mask: conic-gradient(from -90deg, #000 0 180deg, transparent 0);
}
.yoke-l { transform: translateX(-55px) rotateY(90deg); }
.yoke-r { transform: translateX(55px) rotateY(90deg); }
/* --- cups: built standing (axis Y), then laid down; top disc = outside --- */
.cup {
--dir: 1; /* which way "up" went: flips the light */
position: absolute;
top: 35px;
transform-style: preserve-3d;
transform: translateX(55px) rotateZ(90deg);
}
.cup-l {
--dir: -1;
transform: translateX(-55px) rotateZ(-90deg);
}
/* 12 strips, radius 31px, 26px thick: 17px shell, a seam, 9px cushion */
.cup i {
position: absolute;
left: -8.6px;
top: -13px;
width: 17.2px;
height: 26px;
backface-visibility: hidden;
background:
linear-gradient(rgb(0 0 0 / calc(0.3 + 0.28 * var(--dir) * sin(var(--i) * 30deg))) 0 0),
linear-gradient(var(--shell-hi) 0 2px, var(--shell) 4px 15px, var(--shell-hi) 16.5px, transparent 16.5px),
linear-gradient(transparent 17px, #2a2c3c 18px, var(--pad) 21px, #0b0c12);
transform: rotateY(calc(var(--i) * 30deg)) translateZ(31px);
}
/* the seam light: its own layer, so hover only changes opacity */
.cup i::after {
content: '';
position: absolute;
inset: 16px 0 auto;
height: 1.5px;
background: var(--glow);
opacity: 0.25;
transition: opacity 0.5s;
}
.outer,
.cushion {
position: absolute;
left: -31.5px;
top: -31.5px;
width: 63px;
height: 63px;
border-radius: 50%;
backface-visibility: hidden;
}
.outer {
background:
radial-gradient(circle, var(--metal) 0 2.5px, transparent 3px),
radial-gradient(circle, transparent 0 7px, var(--metal) 7.5px 8.5px, transparent 9px),
radial-gradient(circle, transparent 0 83%, var(--metal-dark) 85%, #eef0fa 90%, var(--metal) 94%, var(--metal-dark)),
radial-gradient(circle at 35% 30%, var(--shell-hi), var(--shell) 60%, color-mix(in srgb, var(--shell) 60%, #000));
transform: rotateX(90deg) translateZ(13px);
}
.outer em {
position: absolute;
inset: 9px;
border: 2px solid var(--glow);
border-radius: 50%;
box-shadow: 0 0 8px var(--glow), inset 0 0 8px var(--glow);
opacity: 0.2;
transition: opacity 0.5s;
}
.cushion {
background: radial-gradient(circle, #06070b 0 52%, #2a2c3c 60%, var(--pad) 72%, #2a2c3c 86%, #0b0c12);
transform: rotateX(-90deg) translateZ(13px);
}
.headphones:hover .outer em,
.headphones:focus-visible .outer em,
.headphones:hover .cup i::after,
.headphones:focus-visible .cup i::after {
opacity: 1;
}
/* -180 → 180: blending back to the pose never takes more than half a turn */
@keyframes turn {
from { --spin: -180deg; }
to { --spin: 180deg; }
}
@use 'sass:math';
// Over-ear headphones: an arched band of flat strips, two sliders, two yokes and two cups. Each
// cup is a short cylinder (12 strips + 2 discs) lying on its side, so it has real thickness.
$rb: 58px; // band radius (outer surface)
$bt: 6px; // band thickness (radial)
$bw: 22px; // band width (front to back)
$arc: 200deg; // how much of a circle the band covers
$nb: 12; // band strips
$step: math.div($arc, $nb);
$seg: 2 * $rb * math.tan(math.div($step, 2)); // one strip = one side of the polygon
$cy: -22px; // arc centre (rig coordinates: 0 0 is the middle of the whole object)
$end: $cy - $rb * math.cos(math.div($arc, 2)); // height where the band ends
$cx: 55px; // cup centre, left / right
$rc: 31px; // cup radius
$ry: $rc + 4px; // yoke radius
$ccy: $end + 12px + $ry; // cup centre height: slider, then yoke, then the cup
$t: 26px; // cup thickness: 17px shell + 9px cushion
$nc: 12; // strips per cup
$cw: 2 * $rc * math.tan(math.div(180deg, $nc));
$turn: 16s;
// Two registered properties, so they can animate: the lap angle, and how much of it is applied.
// Hover takes --d-headphones-k to 0, which blends ANY point of the lap smoothly back to the pose.
@property --d-headphones-spin {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@property --d-headphones-k {
syntax: '<number>';
inherits: false;
initial-value: 1;
}
// Static hit area: hovering it never moves it.
.d-headphones {
--shell: color-mix(in srgb, var(--accent) 34%, #17182a);
--shell-hi: color-mix(in srgb, var(--accent) 55%, #3a3d5c);
--pad: #16171f;
--leather: color-mix(in srgb, var(--accent) 14%, #1b1c29);
--leather-hi: color-mix(in srgb, var(--accent) 22%, #3a3c52);
--metal: #b8bdd2;
--metal-dark: #5c6280;
--glow: var(--accent-2);
display: grid;
place-items: center;
width: 216px;
height: 184px;
border-radius: 16px;
outline-offset: -4px;
cursor: pointer;
transform-style: preserve-3d;
&__rig {
position: relative;
width: 0;
height: 0;
pointer-events: none;
transform-style: preserve-3d;
// The product shot (-28deg) plus the lap, scaled by k. Only one angle feeds the transform.
transform: rotateX(-14deg) rotateY(calc(-28deg + var(--d-headphones-spin) * var(--d-headphones-k)));
animation: d-headphones-turn $turn linear infinite;
transition: --d-headphones-k 1.1s cubic-bezier(0.45, 0, 0.2, 1);
}
&__shadow {
position: absolute;
left: -80px;
top: $ccy + $rc + 8px - 40px;
width: 160px;
height: 80px;
border-radius: 50%;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: rotateX(90deg);
}
// --- the band ---------------------------------------------------------------------------
// Strips stand at the arc centre, turn by their angle, move out by the radius and fold flat
// (rotateX 90deg): each one becomes a tangent slice of the band's top surface.
&__band {
position: absolute;
left: 0;
top: $cy;
transform-style: preserve-3d;
> i {
--a: calc(#{-0.5 * $arc} + (var(--i) + 0.5) * #{$step});
position: absolute;
left: -0.5 * $seg - 0.3px;
top: -0.5 * $bw;
width: $seg + 0.6px; // a hair wider: no hairline seams
height: $bw;
// light from above: the strip facing up (cos = 1) gets no shade
background:
linear-gradient(rgb(0 0 0 / calc(0.5 - 0.46 * cos(var(--a)))) 0 0),
repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 80%, #fff) 0 2px, transparent 2px 4px) 0 3px / 100% 1px no-repeat,
repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 80%, #fff) 0 2px, transparent 2px 4px) 0 #{$bw - 4px} / 100% 1px no-repeat,
linear-gradient(var(--leather), var(--leather-hi) 45%, var(--leather) 100%);
transform: rotateZ(var(--a)) translateY(-$rb) rotateX(90deg);
}
}
// The band's two side walls: one ring, masked to the band's arc.
&__side {
position: absolute;
left: -$rb;
top: -$rb;
width: 2 * $rb;
height: 2 * $rb;
background: radial-gradient(closest-side, transparent calc(100% - #{$bt}), var(--leather) calc(100% - #{$bt} + 0.5px), color-mix(in srgb, var(--leather) 70%, #000) 100%, transparent 100%);
mask: conic-gradient(from #{-0.5 * $arc}, #000 0 $arc, transparent 0);
transform: translateZ(0.5 * $bw);
& + & {
transform: translateZ(-0.5 * $bw);
}
}
// --- sliders and yokes (metal) ------------------------------------------------------------
// Two crossed planes make a bar that has width from every angle.
&__slider {
position: absolute;
top: $end - 6px;
height: 20px;
background: linear-gradient(90deg, var(--metal-dark), var(--metal) 45%, #eef0fa 55%, var(--metal-dark));
&--l,
&--r {
left: -5px;
width: 10px;
}
&--l {
transform: translateX(-$cx) rotateY(90deg);
}
&--r {
transform: translateX($cx) rotateY(90deg);
}
&--lf,
&--rf {
left: -1.5px;
width: 3px;
}
&--lf {
transform: translateX(-$cx);
}
&--rf {
transform: translateX($cx);
}
}
// The fork around each cup: a half ring standing in the plane through the cup's axis.
&__yoke {
position: absolute;
left: -$ry;
top: $ccy - $ry;
width: 2 * $ry;
height: 2 * $ry;
background: radial-gradient(closest-side, transparent calc(100% - 4px), var(--metal) calc(100% - 3.5px), var(--metal-dark) 100%, transparent 100%);
mask: conic-gradient(from -90deg, #000 0 180deg, transparent 0);
&--l {
transform: translateX(-$cx) rotateY(90deg);
}
&--r {
transform: translateX($cx) rotateY(90deg);
}
}
// --- the cups -----------------------------------------------------------------------------
// Built as a can standing up (axis = Y), then laid on its side with rotateZ(±90deg) so the
// top disc ends up facing outward on both sides.
&__cup {
--dir: 1; // which way "up" went: flips the light on the strips
position: absolute;
left: 0;
top: $ccy;
transform-style: preserve-3d;
transform: translateX($cx) rotateZ(90deg);
&--l {
--dir: -1;
transform: translateX(-$cx) rotateZ(-90deg);
}
// shell (outer 17px), a thin glow seam, cushion (inner 9px)
> i {
position: absolute;
left: -0.5 * $cw - 0.3px;
top: -0.5 * $t;
width: $cw + 0.6px;
height: $t;
backface-visibility: hidden;
background:
linear-gradient(rgb(0 0 0 / calc(0.3 + 0.28 * var(--dir) * sin(var(--i) * #{math.div(360deg, $nc)}))) 0 0),
linear-gradient(var(--shell-hi) 0 2px, var(--shell) 4px 15px, var(--shell-hi) 16.5px, transparent 16.5px),
linear-gradient(transparent 17px, #2a2c3c 18px, var(--pad) 21px, #0b0c12);
transform: rotateY(calc(var(--i) * #{math.div(360deg, $nc)})) translateZ($rc);
// the seam light: its own layer so only opacity changes on hover
&::after {
content: '';
position: absolute;
inset: 16px 0 auto;
height: 1.5px;
background: var(--glow);
opacity: 0.25;
transition: opacity 0.5s;
}
}
}
&__outer,
&__cushion {
position: absolute;
left: -$rc - 0.5px;
top: -$rc - 0.5px;
width: 2 * $rc + 1px;
height: 2 * $rc + 1px;
border-radius: 50%;
backface-visibility: hidden;
}
// the outside of the cup: brushed metal plate, logo, light ring
&__outer {
background:
radial-gradient(circle, var(--metal) 0 2.5px, transparent 3px),
radial-gradient(circle, transparent 0 7px, var(--metal) 7.5px 8.5px, transparent 9px),
radial-gradient(circle, transparent 0 83%, var(--metal-dark) 85%, #eef0fa 90%, var(--metal) 94%, var(--metal-dark) 100%),
repeating-conic-gradient(rgb(255 255 255 / 0.04) 0 2deg, rgb(0 0 0 / 0.06) 2deg 4deg),
radial-gradient(circle at 35% 30%, var(--shell-hi), var(--shell) 60%, color-mix(in srgb, var(--shell) 60%, #000));
transform: rotateX(90deg) translateZ(0.5 * $t);
em {
position: absolute;
inset: 9px;
border: 2px solid var(--glow);
border-radius: 50%;
box-shadow:
0 0 8px var(--glow),
inset 0 0 8px var(--glow);
opacity: 0.2;
transition: opacity 0.5s;
}
}
// the inside: a soft cushion ring around the speaker cloth
&__cushion {
background:
radial-gradient(circle, rgb(255 255 255 / 0.1) 0 1px, transparent 1.5px) 0 0 / 4px 4px,
radial-gradient(circle, #06070b 0 52%, #2a2c3c 60%, var(--pad) 72%, #2a2c3c 86%, #0b0c12 100%);
transform: rotateX(-90deg) translateZ(0.5 * $t);
}
// --- hover / focus: face the viewer, light up -------------------------------------------
&:hover &__rig,
&:focus-visible &__rig {
--d-headphones-k: 0; // the lap keeps running underneath, it just stops counting
}
&:hover &__outer em,
&:focus-visible &__outer em,
&:hover &__cup > i::after,
&:focus-visible &__cup > i::after {
opacity: 1;
}
}
// -180 → 180, not 0 → 360: the blend back to the pose is never more than half a turn
@keyframes d-headphones-turn {
from {
--d-headphones-spin: -180deg;
}
to {
--d-headphones-spin: 180deg;
}
}