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 watch with thickness, strap stubs and the real time. Once a second JS writes three angles and two integers; CSS turns the hands and prints the digits with counters.
Your edited version
--h, --m, --s as angles. Each hand stands on the centre (bottom: 50%), turns about its foot and reads its own variable.counter-reset: hh var(--hh) mm var(--mm) turns them into counters and content: counter(hh, decimal-leading-zero) prints 09:05.±5px. The straps are hinged on the case edge with transform-origin and tilted away with rotateX.setInterval → --h / --m / --s anglesdigits via counter-reset: var(--hh)straps hinged on the case edgerounded slabs for thickness<div class="scene">
<div class="watch">
<div class="body">
<i class="strap top"></i><i class="strap bottom"></i>
<i class="slab" style="--i:0"></i><i class="slab" style="--i:1"></i><i class="slab" style="--i:2"></i>
<i class="slab mid" style="--i:3"></i>
<i class="slab" style="--i:4"></i><i class="slab" style="--i:5"></i><i class="slab" style="--i:6"></i>
<i class="crown"></i>
<div class="back"></div>
<div class="face">
<div class="dial"><i class="hand h"></i><i class="hand m"></i><i class="hand s"></i></div>
<div class="time"></div>
<small>live time</small>
</div>
</div>
</div>
</div>
.scene {
perspective: 800px;
}
.watch {
--metal: #5a607f;
--band: #6550ba;
transform-style: preserve-3d;
animation: sway 7s ease-in-out infinite alternate;
}
/* 88 × 104, 10px thick, 24px corners */
.body {
position: relative;
width: 88px;
height: 104px;
transform-style: preserve-3d;
}
/* hinged on the back edge of the case, then tilted away round an invisible wrist */
.strap {
position: absolute;
left: 16px;
width: 56px;
height: 44px;
background:
linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
var(--band);
}
.strap.top {
bottom: 100%;
border-radius: 10px 10px 0 0;
transform-origin: 50% 100%;
transform: translateZ(-4px) rotateX(38deg);
}
.strap.bottom {
top: 100%;
border-radius: 0 0 10px 10px;
background:
radial-gradient(circle, rgb(0 0 0 / 0.45) 0 2px, transparent 2.5px) 50% 8px / 100% 10px repeat-y,
linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
var(--band);
transform-origin: 50% 0;
transform: translateZ(-4px) rotateX(-38deg);
}
/* rounded slabs fill the case, corners included */
.slab {
position: absolute;
inset: 0.5px;
border-radius: 24px;
background: var(--metal);
transform: translateZ(calc((var(--i) - 3) * 1.5px));
}
.slab.mid {
background: color-mix(in srgb, #eceefb 45%, var(--metal)); /* a polished band */
}
/* ribbed crown plate, plus its end face turned side-on */
.crown {
position: absolute;
top: 30px;
left: calc(100% - 3px);
width: 7px;
height: 20px;
border-radius: 2px;
background: repeating-linear-gradient(var(--metal) 0 1.5px, #a3a7bd 1.5px 3px);
transform-style: preserve-3d;
transform: translateZ(0.75px);
}
.crown::before {
content: '';
position: absolute;
top: 0;
left: calc(100% - 4px);
width: 8px;
height: 100%;
border-radius: 2px;
background: inherit;
transform: rotateY(90deg);
}
.back,
.face {
position: absolute;
inset: 0;
box-sizing: border-box;
border-radius: 24px;
backface-visibility: hidden;
}
.back {
background:
radial-gradient(circle, #05060c 0 10px, #1e8c85 11px 13px, transparent 14px),
radial-gradient(circle, #868ba4 0 30px, var(--metal) 31px);
transform: rotateY(180deg) translateZ(5px);
}
/* black glass, the border is the bezel */
.face {
display: flex;
flex-direction: column;
align-items: center;
padding: 9px 6px 6px;
overflow: hidden;
border: 5px solid #05060c;
background: radial-gradient(circle at 50% 30%, #231c42, #05060c 70%);
color: #fff;
font-family: system-ui, sans-serif;
transform: translateZ(5px);
}
.face::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(125deg, rgb(255 255 255 / 0.16), transparent 38%);
}
.face small {
margin-top: 1px;
color: #2ee6d6;
font-size: 6px;
font-weight: 700;
letter-spacing: 1.2px;
text-transform: uppercase;
}
/* 12 ticks from a repeating conic gradient, a dark disc on top, an activity arc behind */
.dial {
position: relative;
flex: none;
width: 50px;
height: 50px;
border-radius: 50%;
background:
radial-gradient(circle, #0b0d18 0 19px, transparent 19.5px),
repeating-conic-gradient(from -1.5deg, rgb(255 255 255 / 0.85) 0 3deg, transparent 3deg 30deg),
conic-gradient(#ff4d9d 0 68%, rgb(255 77 157 / 0.25) 0);
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
}
.dial::after {
content: '';
position: absolute;
top: calc(50% - 2.5px);
left: calc(50% - 2.5px);
width: 5px;
height: 5px;
border-radius: 50%;
background: #2ee6d6;
}
/* stands on the centre, turns about its foot */
.hand {
position: absolute;
bottom: 50%;
border-radius: 2px;
background: #fff;
transform-origin: 50% 100%;
}
.hand.h { left: calc(50% - 1.5px); width: 3px; height: 12px; transform: rotate(var(--h, 300deg)); }
.hand.m { left: calc(50% - 1px); width: 2px; height: 18px; transform: rotate(var(--m, 54deg)); }
.hand.s { left: calc(50% - 0.5px); width: 1px; height: 21px; background: #2ee6d6; transform: rotate(var(--s, 180deg)); }
/* two integers from JS become counters; decimal-leading-zero pads them */
.time {
margin-top: 5px;
counter-reset: hh var(--hh, 10) mm var(--mm, 9);
font: 700 15px/1 ui-monospace, Consolas, monospace;
letter-spacing: -0.5px;
}
.time::before {
content: counter(hh, decimal-leading-zero) ':' counter(mm, decimal-leading-zero);
}
@keyframes sway {
from { transform: rotateX(10deg) rotateY(-30deg); }
to { transform: rotateX(6deg) rotateY(30deg); }
}
const watch = document.querySelector('.watch');
function tick() {
const d = new Date();
// seconds since midnight: the angles only grow, so no hand sweeps backwards at 59 → 0
const t = d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
watch.style.setProperty('--s', t * 6 + 'deg');
watch.style.setProperty('--m', (t / 10).toFixed(1) + 'deg');
watch.style.setProperty('--h', (t / 120).toFixed(2) + 'deg');
// integers for the digital readout (CSS counters)
watch.style.setProperty('--hh', d.getHours());
watch.style.setProperty('--mm', d.getMinutes());
}
tick();
setInterval(tick, 1000);
$w: 88px;
$h: 104px;
$d: 10px; // case thickness
$r: 24px; // corner radius
.d-watch {
--metal: color-mix(in srgb, var(--muted) 55%, var(--surface-solid));
--band: color-mix(in srgb, var(--accent) 70%, #0b0d18);
transform-style: preserve-3d;
animation: d-watch-sway 7s ease-in-out infinite alternate;
&__body {
position: relative;
width: $w;
height: $h;
transform-style: preserve-3d;
}
// Straps hang off the BACK edge of the case and are hinged there (transform-origin on the
// case side), then tilted away so they curve round an invisible wrist.
&__strap {
position: absolute;
left: 16px;
width: $w - 32px;
height: 44px;
background:
linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
var(--band);
&--top {
bottom: 100%;
border-radius: 10px 10px 0 0;
transform-origin: 50% 100%;
transform: translateZ($d * -0.5 + 1px) rotateX(38deg);
}
&--bottom {
top: 100%;
border-radius: 0 0 10px 10px;
// a row of pin holes down the middle
background:
radial-gradient(circle, rgb(0 0 0 / 0.45) 0 2px, transparent 2.5px) 50% 8px / 100% 10px repeat-y,
linear-gradient(90deg, rgb(0 0 0 / 0.35), transparent 22% 78%, rgb(0 0 0 / 0.35)),
var(--band);
transform-origin: 50% 0;
transform: translateZ($d * -0.5 + 1px) rotateX(-38deg);
}
}
// No walls: seven rounded slabs 1.5px apart fill the case, corners included.
&__slab {
position: absolute;
inset: 0.5px;
border-radius: $r;
background: var(--metal);
transform: translateZ(calc((var(--i) - 3) * 1.5px));
// the middle slab (--i: 3, after the two straps) is a touch brighter: a polished band
&:nth-child(6) {
background: color-mix(in srgb, var(--text) 45%, var(--metal));
}
}
// the crown: a ribbed plate poking out of the right side, plus its end face turned side-on
&__crown {
position: absolute;
top: 30px;
left: calc(100% - 3px);
width: 7px;
height: 20px;
border-radius: 2px;
background: repeating-linear-gradient(var(--metal) 0 1.5px, color-mix(in srgb, var(--text) 50%, var(--metal)) 1.5px 3px);
transform-style: preserve-3d;
transform: translateZ(0.75px); // between two slabs, never coplanar with one
&::before {
content: '';
position: absolute;
top: 0;
left: calc(100% - 4px);
width: 8px;
height: 100%;
border-radius: 2px;
background: inherit;
transform: rotateY(90deg);
}
}
&__back,
&__face {
position: absolute;
inset: 0;
border-radius: $r;
backface-visibility: hidden;
}
&__back {
background:
radial-gradient(circle, #05060c 0 10px, color-mix(in srgb, var(--accent-2) 60%, #05060c) 11px 13px, transparent 14px),
radial-gradient(circle, color-mix(in srgb, var(--text) 30%, var(--metal)) 0 30px, var(--metal) 31px);
transform: rotateY(180deg) translateZ($d * 0.5);
}
// black glass with the bezel as padding
&__face {
display: flex;
flex-direction: column;
align-items: center;
padding: 9px 6px 6px;
overflow: hidden;
border: 5px solid #05060c;
background: radial-gradient(circle at 50% 30%, color-mix(in srgb, var(--accent) 22%, #05060c), #05060c 70%);
color: #fff;
transform: translateZ($d * 0.5);
// glass reflection, fixed to the glass
&::after {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(125deg, rgb(255 255 255 / 0.16), transparent 38%);
pointer-events: none;
}
small {
margin-top: 1px;
color: var(--accent-2);
font-size: 6px;
font-weight: 700;
letter-spacing: 1.2px;
text-transform: uppercase;
}
}
// Twelve tick marks: a repeating conic gradient, covered in the middle by a dark disc.
// An accent arc around it doubles as an activity ring.
&__dial {
position: relative;
flex: none;
width: 50px;
height: 50px;
border-radius: 50%;
background:
radial-gradient(circle, #0b0d18 0 19px, transparent 19.5px),
repeating-conic-gradient(from -1.5deg, rgb(255 255 255 / 0.85) 0 3deg, transparent 3deg 30deg),
conic-gradient(var(--hot) 0 68%, color-mix(in srgb, var(--hot) 25%, transparent) 0);
box-shadow: inset 0 0 0 1px rgb(255 255 255 / 0.12);
// centre cap, above the hands
&::after {
content: '';
position: absolute;
top: calc(50% - 2.5px);
left: calc(50% - 2.5px);
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--accent-2);
}
}
// Each hand stands on the centre (bottom: 50%) and turns about its foot. The angles only
// ever grow (JS counts from midnight), so no hand sweeps backwards when the minute rolls over.
&__hand {
position: absolute;
bottom: 50%;
border-radius: 2px;
background: #fff;
transform-origin: 50% 100%;
&--h {
left: calc(50% - 1.5px);
width: 3px;
height: 12px;
transform: rotate(var(--h, 300deg));
}
&--m {
left: calc(50% - 1px);
width: 2px;
height: 18px;
transform: rotate(var(--m, 54deg));
}
&--s {
left: calc(50% - 0.5px);
width: 1px;
height: 21px;
background: var(--accent-2);
transform: rotate(var(--s, 180deg));
}
}
// Digits without touching the DOM: the two integers from JS become counters, and
// `decimal-leading-zero` pads them to 09:05.
&__time {
margin-top: 5px;
counter-reset: hh var(--hh, 10) mm var(--mm, 9);
font: 700 15px/1 ui-monospace, 'SF Mono', Consolas, monospace;
letter-spacing: -0.5px;
&::before {
content: counter(hh, decimal-leading-zero) ':' counter(mm, decimal-leading-zero);
}
}
}
@keyframes d-watch-sway {
from {
transform: rotateX(10deg) rotateY(-30deg);
}
to {
transform: rotateX(6deg) rotateY(30deg);
}
}