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 paper cup that is wider at the top: twenty trapezoid strips lean out from the base circle. The sleeve and its logo wrap around without a seam, the lid is a stack of discs and the steam keeps rising.
Your edited version
rotateY(i × 18deg) translateZ(27px)), hinged on their bottom edge, and lean out by atan((36 − 27) / 100) ≈ 5.14° so the rim is wider than the base.clip-path: 11.4px wide at the rim, 8.6px at the base (both are 2 · r · tan(9°)). Its height is the slant length, √(100² + 9²).background-position). The lap is measured at the sleeve's middle height, where the strips are 9.86px wide, so the logo wraps round without a seam.animation-delay.frustum: strips hinged at the base, rotateX(−lean)trapezoids with clip-pathone lap-long label, background-position by indexsteam from the sip slot, counter-turned to face you<div class="scene">
<div class="coffee">
<div class="spin">
<i class="shadow"></i>
<i class="strip" style="--i:0"></i>
<i class="strip" style="--i:1"></i>
<i class="strip" style="--i:2"></i>
<i class="strip" style="--i:3"></i>
<i class="strip" style="--i:4"></i>
<i class="strip" style="--i:5"></i>
<i class="strip" style="--i:6"></i>
<i class="strip" style="--i:7"></i>
<i class="strip" style="--i:8"></i>
<i class="strip" style="--i:9"></i>
<i class="strip" style="--i:10"></i>
<i class="strip" style="--i:11"></i>
<i class="strip" style="--i:12"></i>
<i class="strip" style="--i:13"></i>
<i class="strip" style="--i:14"></i>
<i class="strip" style="--i:15"></i>
<i class="strip" style="--i:16"></i>
<i class="strip" style="--i:17"></i>
<i class="strip" style="--i:18"></i>
<i class="strip" style="--i:19"></i>
<i class="lid" style="--y:-1.5"></i><i class="lid" style="--y:0"></i><i class="lid" style="--y:1.5"></i><i class="lid" style="--y:3"></i>
<i class="lid lid-top" style="--y:4.5"></i>
<i class="lid spout" style="--y:6.5"></i>
<i class="vent"><i class="face"><i class="steam" style="--x:-2;--d:0"></i><i class="steam" style="--x:3;--d:1"></i></i></i>
</div>
</div>
</div>
.scene {
perspective: 800px;
}
/* a 0-wide column, 100px tall: the cup's axis */
.coffee {
--paper: #f4f1ea;
--paper-dark: #d9d3c6;
--kraft: #c1834a;
--violet: #8b6cff;
--lid: #22232e;
position: relative;
width: 0;
height: 100px;
transform-style: preserve-3d;
transform: rotateX(-20deg);
}
.spin {
position: absolute;
inset: 0;
transform-style: preserve-3d;
animation: spin 12s linear infinite;
}
.shadow {
position: absolute;
left: -60px;
top: 70px;
width: 120px;
height: 60px;
border-radius: 50%;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: rotateX(90deg);
}
/* 20 trapezoids hinged on the base circle (r 27px), leaning out towards the rim (r 36px) */
.strip {
position: absolute;
left: -6.3px;
bottom: 0;
width: 12.6px; /* 11.4px at the rim, plus a little overlap */
height: 100.4px; /* the slant length */
transform-origin: 50% 100%;
transform: rotateY(calc(var(--i) * 18deg)) translateZ(27px) rotateX(-5.14deg);
clip-path: polygon(0 0, 100% 0, calc(50% + 4.9px) 100%, calc(50% - 4.9px) 100%);
backface-visibility: hidden;
/* one 197px canvas = one lap round the sleeve's middle; each strip shows its own slice */
background-image:
radial-gradient(ellipse 1.2px 5px at 49.3px 53px, #f0d9b8 0 60%, transparent 90%),
radial-gradient(ellipse 4.5px 6.5px at 49.3px 53px, #fff 0 20%, #6b3e22 30% 92%, transparent),
radial-gradient(circle 11px at 49.3px 53px, var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
radial-gradient(ellipse 1.2px 5px at 148px 53px, #f0d9b8 0 60%, transparent 90%),
radial-gradient(ellipse 4.5px 6.5px at 148px 53px, #fff 0 20%, #6b3e22 30% 92%, transparent),
radial-gradient(circle 11px at 148px 53px, var(--violet) 0 86%, #fff 87% 93%, var(--violet) 94% 100%, transparent 100%),
linear-gradient(transparent 34px, var(--kraft) 34px 36px, #d8b08c 36px 37px, var(--kraft) 37px 70px, #d8b08c 70px 71px, var(--kraft) 71px 73px, transparent 73px),
linear-gradient(var(--paper-dark) 0 1px, var(--paper) 3px 12px, var(--violet) 12px 14px, var(--paper) 14px calc(100% - 3px), var(--paper-dark));
background-size: 197.3px 100%;
background-position: calc(1.07px - var(--i) * 9.864px) 0;
}
/* lighting that stays put: one lap of shade, each strip started i/20 of the way round */
.strip::after {
content: '';
position: absolute;
inset: 0;
background: #06070f;
animation: shade 12s linear infinite;
animation-delay: calc(var(--i) * -0.6s);
}
/* discs centred on the rim, lifted by --y, laid flat */
.lid {
--r: 38.4px;
position: absolute;
left: calc(var(--r) * -1);
top: calc(var(--r) * -1);
width: calc(var(--r) * 2);
height: calc(var(--r) * 2);
border-radius: 50%;
background: radial-gradient(circle, var(--lid) 0 80%, #646570 94%, var(--lid));
transform: translateY(calc(var(--y) * -1px)) rotateX(90deg);
}
.lid-top {
--r: 35.4px;
background:
radial-gradient(circle, transparent 0 70%, rgb(255 255 255 / 0.12) 72% 74%, transparent 76%),
radial-gradient(circle at 35% 30%, #4e4f58, var(--lid) 60%);
}
.spout {
--r: 13px;
background:
radial-gradient(ellipse 7px 2.5px at 50% 70%, #05060a 0 90%, transparent), /* the sip slot */
radial-gradient(circle at 40% 30%, #6a6b74, var(--lid) 70%);
transform: translateY(calc(var(--y) * -1px)) translateZ(19px) rotateX(90deg);
}
/* steam rises from the sip slot: placed there inside .spin, so it goes round with the cup... */
.vent {
position: absolute;
top: -7px;
left: 0;
transform-style: preserve-3d;
transform: translateZ(24px);
}
/* ...and turned back the other way, so the wisps always face you */
.face {
position: absolute;
transform-style: preserve-3d;
animation: spin 12s linear infinite reverse;
}
/* two arcs make an S */
.steam {
position: absolute;
top: -30px;
left: -5px;
width: 10px;
height: 30px;
opacity: 0;
animation: steam 3.6s linear infinite;
animation-delay: calc(var(--d) * -1.8s);
}
.steam::before,
.steam::after {
content: '';
position: absolute;
left: 0;
box-sizing: border-box;
width: 100%;
height: 52%;
border: 2.5px solid transparent;
border-radius: 50%;
}
.steam::before { top: 0; border-left-color: rgb(236 238 251 / 0.5); }
.steam::after { bottom: 0; left: -70%; border-right-color: rgb(236 238 251 / 0.5); }
@keyframes spin {
to { transform: rotateY(360deg); }
}
/* 0% = facing you; between 25% and 75% the strip faces away */
@keyframes shade {
0% { opacity: 0.06; }
12.5% { opacity: 0.3; }
25%, 50% { opacity: 0.5; }
75% { opacity: 0.3; }
88% { opacity: 0; }
100% { opacity: 0.06; }
}
@keyframes steam {
0% { opacity: 0; transform: translate(calc(var(--x) * 1px), 10px) scale(0.6); }
30% { opacity: 0.9; }
60% { transform: translate(calc(var(--x) * 1px + 4px), -12px) scale(0.9, 1.1); }
100% { opacity: 0; transform: translate(calc(var(--x) * 1px - 2px), -34px) scale(1.1, 1.3); }
}
@use 'sass:math';
// A takeaway cup: a frustum (a cone with its tip cut off) of 20 leaning trapezoid strips, a kraft
// sleeve whose logo wraps around it, a lid of stacked discs, and steam that rises from the sip slot.
$n: 20;
$rt: 36px; // radius at the rim (to the middle of a strip's top edge)
$rb: 27px; // radius at the base
$h: 100px; // height
$turn: 12s;
$slant: math.hypot($h, $rt - $rb); // a strip's real length
$lean: math.atan(math.div($rt - $rb, $h)); // leaning OUT, so the top is wider
$half: math.tan(math.div(180deg, $n));
$wt: 2 * $rt * $half; // strip width at the rim
$wb: 2 * $rb * $half; // ... and at the base
// The sleeve art is laid out for the sleeve's middle height: there a strip is exactly $wm wide,
// so a strip's slice of the art lines up with its neighbours' (the logo has no seams).
$wm: 2 * ($rt - ($rt - $rb) * 0.54) * $half;
$lap: $wm * $n;
$rim: math.div($rt, math.cos(math.div(180deg, $n))) + 2px; // lid radius, just past the corners
.d-coffeecup {
--paper: #f4f1ea;
--paper-dark: #d9d3c6;
--kraft: color-mix(in srgb, var(--warm) 45%, #8a5a33);
--lid: #22232e;
--steam: color-mix(in srgb, var(--text) 50%, transparent);
position: relative;
width: 0;
height: $h;
transform-style: preserve-3d;
transform: rotateX(-20deg);
&__spin {
position: absolute;
inset: 0;
transform-style: preserve-3d;
animation: d-coffeecup-spin $turn linear infinite;
}
&__shadow {
position: absolute;
left: -60px;
top: $h - 30px;
width: 120px;
height: 60px;
border-radius: 50%;
background: radial-gradient(closest-side, rgb(0 0 0 / 0.45), transparent);
transform: rotateX(90deg);
}
// --- the body ------------------------------------------------------------------------------
// Each strip stands on the base circle, hinged on its bottom edge, and leans out by
// atan((rt - rb) / h). It is a trapezoid: $wt wide at the top, $wb at the bottom.
&__strip {
position: absolute;
left: -0.5 * $wt - 0.6px;
bottom: 0;
width: $wt + 1.2px; // overlap the neighbours a little: no seams
height: $slant;
transform-origin: 50% 100%;
transform: rotateY(calc(var(--i) * #{math.div(360deg, $n)})) translateZ($rb) rotateX(-$lean);
clip-path: polygon(0 0, 100% 0, calc(50% + #{$wb * 0.5 + 0.6px}) 100%, calc(50% - #{$wb * 0.5 + 0.6px}) 100%);
backface-visibility: hidden;
// All layers are painted on ONE lap-long canvas ($lap × 100%) and every strip slides it by
// its index, so strip i shows slice i: the canvas wraps around the cup.
background-image:
// logo: a violet seal with a white coffee bean, twice round the cup
radial-gradient(ellipse 1.2px 5px at #{$lap * 0.25} 53px, #f0d9b8 0 60%, transparent 90%),
radial-gradient(ellipse 4.5px 6.5px at #{$lap * 0.25} 53px, #fff 0 20%, #6b3e22 30% 92%, transparent 100%),
radial-gradient(circle 11px at #{$lap * 0.25} 53px, var(--accent) 0 86%, #fff 87% 93%, var(--accent) 94% 100%, transparent 100%),
radial-gradient(ellipse 1.2px 5px at #{$lap * 0.75} 53px, #f0d9b8 0 60%, transparent 90%),
radial-gradient(ellipse 4.5px 6.5px at #{$lap * 0.75} 53px, #fff 0 20%, #6b3e22 30% 92%, transparent 100%),
radial-gradient(circle 11px at #{$lap * 0.75} 53px, var(--accent) 0 86%, #fff 87% 93%, var(--accent) 94% 100%, transparent 100%),
// the sleeve: kraft band with a pinstripe near each edge
linear-gradient(transparent 34px, var(--kraft) 34px 36px, color-mix(in srgb, var(--kraft) 70%, #fff) 36px 37px, var(--kraft) 37px 70px, color-mix(in srgb, var(--kraft) 70%, #fff) 70px 71px, var(--kraft) 71px 73px, transparent 73px),
// paper, with a violet stripe under the rim
linear-gradient(var(--paper-dark) 0 1px, var(--paper) 3px 12px, var(--accent) 12px 14px, var(--paper) 14px calc(100% - 3px), var(--paper-dark));
background-size: $lap 100%;
background-position: calc(#{($wt - $wm) * 0.5 + 0.3px} - var(--i) * #{$wm}) 0;
// lighting that stays put while the cup turns (same idea as the drinks can)
&::after {
content: '';
position: absolute;
inset: 0;
background: #06070f;
animation: d-coffeecup-shade $turn linear infinite;
animation-delay: calc(var(--i) * #{math.div($turn, -$n)});
}
}
// --- the lid: discs laid flat, stacked for thickness -------------------------------------------
&__lid {
--r: #{$rim};
position: absolute;
left: calc(var(--r) * -1);
top: calc(var(--r) * -1);
width: calc(var(--r) * 2);
height: calc(var(--r) * 2);
border-radius: 50%;
background: radial-gradient(circle, var(--lid) 0 80%, color-mix(in srgb, var(--lid) 70%, #fff) 94%, var(--lid));
transform: translateY(calc(var(--y) * -1px)) rotateX(90deg);
// the recessed top, a little smaller
&--top {
--r: #{$rim - 3px};
background:
radial-gradient(circle, transparent 0 70%, rgb(255 255 255 / 0.12) 72% 74%, transparent 76%),
radial-gradient(circle at 35% 30%, color-mix(in srgb, var(--lid) 80%, #fff), var(--lid) 60%);
}
// the raised sip spout, toward the front, with its slot
&--spout {
--r: 13px;
background:
radial-gradient(ellipse 7px 2.5px at 50% 70%, #05060a 0 90%, transparent 100%),
radial-gradient(circle at 40% 30%, color-mix(in srgb, var(--lid) 65%, #fff), var(--lid) 70%);
transform: translateY(calc(var(--y) * -1px)) translateZ(19px) rotateX(90deg);
}
}
// --- steam: rises from the sip slot, so it turns round with the cup; a counter-turn on the
// inside keeps each wisp facing you (a flat wisp seen edge-on would vanish) -------------------------
&__vent {
position: absolute;
top: -7px; // the top of the spout
left: 0;
transform-style: preserve-3d;
transform: translateZ(24px); // over the slot, near the spout's front
}
&__face {
position: absolute;
transform-style: preserve-3d;
animation: d-coffeecup-spin $turn linear infinite reverse;
}
&__steam {
position: absolute;
top: -30px;
left: -5px;
width: 10px;
height: 30px;
opacity: 0;
animation: d-coffeecup-steam 3.6s linear infinite;
animation-delay: calc(var(--d) * -1.8s);
// two arcs make an S
&::before,
&::after {
content: '';
position: absolute;
left: 0;
width: 100%;
height: 52%;
border: 2.5px solid transparent;
border-radius: 50%;
}
&::before {
top: 0;
border-left-color: var(--steam);
}
// shifted left so its top meets the bottom of the upper arc
&::after {
bottom: 0;
left: -70%;
border-right-color: var(--steam);
}
}
}
@keyframes d-coffeecup-spin {
to {
transform: rotateY(360deg);
}
}
// 0% = strip facing the viewer; the lamp is front-left. Between 25% and 75% it faces away.
@keyframes d-coffeecup-shade {
0% {
opacity: 0.06;
}
12.5% {
opacity: 0.3;
}
25%,
50% {
opacity: 0.5;
}
75% {
opacity: 0.3;
}
88% {
opacity: 0;
}
100% {
opacity: 0.06;
}
}
// Rise, sway, grow and fade. Starts and ends invisible, so the jump back is never seen.
@keyframes d-coffeecup-steam {
0% {
opacity: 0;
transform: translate(calc(var(--x) * 1px), 10px) scale(0.6);
}
30% {
opacity: 0.9;
}
60% {
transform: translate(calc(var(--x) * 1px + 4px), -12px) scale(0.9, 1.1);
}
100% {
opacity: 0;
transform: translate(calc(var(--x) * 1px - 2px), -34px) scale(1.1, 1.3);
}
}