Nº 353 of 520
Phone showcase mockup
A pure-CSS titanium phone with side keys and an island, four finishes, three live screens, pointer-driven 3D tilt and a light or dark gallery stage.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Phone showcase mockup" (piece mockup-phone-showcase) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 353 · "Phone showcase mockup" · designlounge.vercel.app -->
# Phone showcase mockup
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
A device frame for presenting phone work in a case study or a landing page, drawn entirely in CSS. No images, no SVG device outlines. The phone, a fictional Corvo 3, has a brushed titanium edge, three keys on the left, a side button on the right, a black bezel, and a pill-shaped island with a camera lens. Inside sits real HTML: a lock screen, a journal list and a run summary. A side panel switches the finish (Raw, Graphite, Dune, Fjord), the screen and the stage (warm bone gallery or espresso dark). Moving the pointer over the stage turns the phone in 3D, and a glare band slides across the glass. The detail worth copying is the stacked depth layers: four copies of the body outline at −4, −8, −12 and −16px on Z give the phone real thickness when it turns, so it never looks like a flat card.
## Reference behaviour
1. First frame: light stage, Raw finish, Lock screen. The phone rests at `rotateX(5deg) rotateY(-20deg)` so the left keys and the edge thickness are visible.
2. Pointer moves over the stage: target tilt is `rotateX = -ny * 24`, `rotateY = nx * 56`, where `nx`, `ny` are pointer position in −0.5…0.5 of the stage box. The current tilt eases toward the target by 10% per animation frame.
3. Pointer leaves the stage: the target returns to the rest pose and the phone eases back.
4. The glare band on the screen moves with `rotateY`: its centre is at `42 − ry × 1.6` percent across the screen.
5. The floor shadow under the phone shifts sideways by `−1.4px × ry` and narrows as the tilt grows.
6. Finish swatches (radio group): the edge gradient, depth layers and keys recolour instantly; the legend reads "Finish Graphite" etc.
7. Screen segmented control (Lock, Journal, Run): the outgoing screen fades out, the incoming one fades in from `scale(1.04)` to `1`.
8. When Run is shown, the island widens from 29% to 52% of the phone width and shows a green dot, "Run" and an elapsed time "24:18". It shrinks back for other screens.
9. Stage segmented control (Light, Dark): background, text and the floor shadow switch over 400ms.
10. The right side button is a real button. Pressing it puts the screen to sleep (all screens fade to black, island shrinks). Pressing again or choosing any screen wakes it.
11. A polite live region announces each change: "Graphite finish", "Run screen", "dark stage", "Screen asleep".
## Structure
```
1280 × 800
┌──────────────────────────────────────────────────────────────────────┐
│ padding 40px / clamp(24px, 6vw, 96px) │
│ ┌── copy 260–340px ──┐ ┌──────── stage (perspective 1100) ─┐│
│ │ CORVO 3 · PRES… │ │ ┌───────────┐ ││
│ │ Hold it to │ │ │ ◖ island ◗│ 300 × 628 ││
│ │ the light. 56px │ │ keys ▌ │ screen │ ▐ side btn ││
│ │ lede 32ch │ │ │ │ ││
│ │ FINISH Raw │ │ └───────────┘ ││
│ │ ○ ○ ○ ○ 40px │ │ floor shadow ││
│ │ SCREEN STAGE │ │ hint 12px, bottom 12px ││
│ │ [Lock|Journal|Run] │ └───────────────────────────────────┘│
│ └────────────────────┘ │
… 161 more lines. Copy or download for the full brief. Loading the source…Brief
201 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
A device frame for presenting phone work in a case study or a landing page, drawn entirely in CSS. No images, no SVG device outlines. The phone, a fictional Corvo 3, has a brushed titanium edge, three keys on the left, a side button on the right, a black bezel, and a pill-shaped island with a camera lens. Inside sits real HTML: a lock screen, a journal list and a run summary. A side panel switches the finish (Raw, Graphite, Dune, Fjord), the screen and the stage (warm bone gallery or espresso dark). Moving the pointer over the stage turns the phone in 3D, and a glare band slides across the glass. The detail worth copying is the stacked depth layers: four copies of the body outline at −4, −8, −12 and −16px on Z give the phone real thickness when it turns, so it never looks like a flat card.
Reference behaviour
- First frame: light stage, Raw finish, Lock screen. The phone rests at
rotateX(5deg) rotateY(-20deg)so the left keys and the edge thickness are visible. - Pointer moves over the stage: target tilt is
rotateX = -ny * 24,rotateY = nx * 56, wherenx,nyare pointer position in −0.5…0.5 of the stage box. The current tilt eases toward the target by 10% per animation frame. - Pointer leaves the stage: the target returns to the rest pose and the phone eases back.
- The glare band on the screen moves with
rotateY: its centre is at42 − ry × 1.6percent across the screen. - The floor shadow under the phone shifts sideways by
−1.4px × ryand narrows as the tilt grows. - Finish swatches (radio group): the edge gradient, depth layers and keys recolour instantly; the legend reads “Finish Graphite” etc.
- Screen segmented control (Lock, Journal, Run): the outgoing screen fades out, the incoming one fades in from
scale(1.04)to1. - When Run is shown, the island widens from 29% to 52% of the phone width and shows a green dot, “Run” and an elapsed time “24:18”. It shrinks back for other screens.
- Stage segmented control (Light, Dark): background, text and the floor shadow switch over 400ms.
- The right side button is a real button. Pressing it puts the screen to sleep (all screens fade to black, island shrinks). Pressing again or choosing any screen wakes it.
- A polite live region announces each change: “Graphite finish”, “Run screen”, “dark stage”, “Screen asleep”.
Structure
1280 × 800
┌──────────────────────────────────────────────────────────────────────┐
│ padding 40px / clamp(24px, 6vw, 96px) │
│ ┌── copy 260–340px ──┐ ┌──────── stage (perspective 1100) ─┐│
│ │ CORVO 3 · PRES… │ │ ┌───────────┐ ││
│ │ Hold it to │ │ │ ◖ island ◗│ 300 × 628 ││
│ │ the light. 56px │ │ keys ▌ │ screen │ ▐ side btn ││
│ │ lede 32ch │ │ │ │ ││
│ │ FINISH Raw │ │ └───────────┘ ││
│ │ ○ ○ ○ ○ 40px │ │ floor shadow ││
│ │ SCREEN STAGE │ │ hint 12px, bottom 12px ││
│ │ [Lock|Journal|Run] │ └───────────────────────────────────┘│
│ └────────────────────┘ │
└──────────────────────────────────────────────────────────────────────┘
main.pageis a two-column grid:minmax(260px, 340px) 1fr, gap 40px.section.copyholds eyebrowp, the onlyh1, a ledep, and threefieldsets withlegends.section.stageis labelled (“Corvo 3 phone in Raw, showing the Lock screen”) and carriesaria-roledescription="device mockup"..rigsets the phone width and iscontainer-type: inline-size, so every part of the phone and its screens is sized incqw..phone(aspect 300/628,transform-style: preserve-3d) contains: four.depthlayers, three decorative.keyelements on the left,button.k-powon the right, then.edge > .bezel > .screen..screenholds.island, three.scrpanels and a.glareoverlay.
Tokens
:root {
--bg: #ebe5da; /* bone wall */
--bg-2: #f6f2ea; /* spotlight centre */
--ink: #1d1a16;
--ink-2: #5b544a;
--ink-3: #7a7266;
--line: #d3cabb;
--accent: #b4561f; /* ochre: headline italic, focus, journal FAB */
--floor: rgba(40, 28, 14, .32);
--serif: "Instrument Serif", Georgia, serif;
--sans: "Geist", system-ui, sans-serif;
--ease: cubic-bezier(.2, .7, .2, 1);
--ease-out: cubic-bezier(.16, 1, .3, 1);
/* finish ramp, light → dark */
--f1: #e4e0d8; --f2: #b3ada3; --f3: #77726a; --f4: #4e4a44; /* Raw */
}
body.dark { --bg: #14120f; --bg-2: #26221c; --ink: #efe8dc; --ink-2: #b6ad9f; --ink-3: #91887a; --line: #3a342b; --accent: #e08a4f; --floor: rgba(0,0,0,.7); }
[data-finish="graphite"] { --f1: #8d9096; --f2: #4a4d52; --f3: #2b2d31; --f4: #17181a; }
[data-finish="dune"] { --f1: #f1dcc6; --f2: #c9a385; --f3: #8f6c52; --f4: #5c4433; }
[data-finish="fjord"] { --f1: #dfe8ee; --f2: #9fb2bf; --f3: #677c8a; --f4: #3f4f5a; }
Phone geometry, all in cqw of the rig (rig = 300px at full size): body radius 15.5cqw, edge padding 1.7cqw, bezel radius 13.8cqw and padding 1.5cqw, screen radius 12.4cqw, island 29 × 8.6cqw at top 3.4cqw.
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Headline | Instrument Serif | 56px (44px narrow) | 400, second line italic in accent | 1 | −0.01em | sentence |
| Eyebrow / legends | Geist | 12px | 500 | 1.45 | 0.14em | uppercase |
| Lede | Geist | 15px | 400 | 1.45 | 0 | sentence |
| Segmented labels | Geist | 14px | 500 | 1 | 0 | sentence |
| Lock time | Instrument Serif | 30cqw | 400 | 1 | −0.02em | — |
| Journal title, run distance | Instrument Serif | 11cqw / 24cqw | 400 | 1 | −0.02em | — |
| In-screen body | Geist | 3.4–3.6cqw | 400/600 | 1.3 | 0 | — |
The serif carries every big number and title, inside and outside the phone. That shared voice is what makes the mockup feel art-directed rather than dropped in.
Motion
| Thing | Trigger | Property | From → to | Duration / easing | Reduced motion |
|---|---|---|---|---|---|
| Tilt | pointermove on stage | rotateX, rotateY | current → target | rAF lerp 0.1 per frame, stops under 0.02° | disabled, phone stays at rest pose |
| Return | pointerleave | rotateX, rotateY | current → 5°, −20° | same lerp | — |
| Glare | follows tilt | gradient stop | 42% − ry × 1.6 | with tilt | static |
| Screen swap | screen radio | opacity, scale | 0, 1.04 → 1, 1 | 380ms standard / 500ms expo out | instant |
| Island | Run on/off | width | 29cqw ↔ 52cqw, content fades in after 200ms | 450ms expo out | instant |
| Sleep | side button | screen opacity, scale | 1 → 0, 0.98 | 380ms | instant |
| Stage | stage radio | background, colour | light ↔ dark | 400ms standard | instant |
| Swatch | checked | inner dot scale | 1 → 0.86, ring appears | 180ms | instant |
States
- Swatch resting: 28px gradient dot inside a 40px hit target. Checked: dot scales to 0.86 and a 1.5px
--inkring appears 2px inside the target. - Segmented option checked: filled
--inkpill with--bgtext. Unchecked:--ink-2text on transparent. - Focus-visible on radios: 2px
--accentoutline, 2px offset, drawn on the label (label:has(input:focus-visible)). - Side button:
aria-pressed="true"while the screen sleeps. Active: brightness 0.8. - Screen asleep: black glass, island collapsed even on Run.
- Dark stage: espresso background, the accent lifts to
#e08a4fto keep contrast.
Accessibility
- Finish, Screen and Stage are radio groups inside
fieldset/legend, so arrow keys move within each group for free. Swatch inputs carryaria-labelwith the finish name. - The stage
sectionlabel is rebuilt on every change: “Corvo 3 phone in Dune, showing the Journal screen” or ”…, screen asleep”. - Hidden screens carry
aria-hidden="true". - The side button is a real
buttonwitharia-label="Side button: sleep or wake the screen"and an invisible hit extension (::beforeinset −6px −16px −6px −8px) because the visible key is only ~5px wide. - Tilt is pointer-only decoration; nothing is lost without it.
- Contrast:
#1d1a16on#ebe5da≈ 14:1;#5b544alede ≈ 6.3:1;#7a7266labels ≈ 4.1:1 at 12px caps, used for labels only.
Responsive rules
- ≥1280: two columns, phone 300px wide, height-limited by
(100vh − 120px) × 0.48. - 1024: same layout, the copy column shrinks to 260px.
- ≤820: one column, stage first, phone
min(240px, 100vw − 96px), headline 44px, hint flows under the phone. - At 375 wide the phone is 240px and every in-screen size scales through
cqw. The stage clips overflow so the 3D turn never creates horizontal scroll. - Never scale the phone with
transform: scale(); size the rig and letcqwdo the rest.
Acceptance checklist
Always
- The device is HTML/CSS only: no images, no device SVG.
- Every size inside the device is in container units of the rig.
- At least three Z-offset depth layers give visible thickness when tilted.
- Finish, screen and stage are radio groups with legends; finish swatches have 40px targets.
- Tilt eases toward a target with a lerp, stops its rAF when settled, and returns to a rest pose on leave.
- Reduced motion: no tilt, no transitions; every control still works.
- The side button is a real button with an enlarged hit area and
aria-pressed. - No horizontal scroll at 375px wide.
This demo
- Four finishes: Raw, Graphite, Dune, Fjord, using the ramps in Tokens.
- Three screens: Lock (9:41, “Sunday 4 October”, Fieldnote reminder), Journal (5 dated entries, “Search 214 notes”), Run (8.42 km, pace 5’24”, time 45:28, climb 62 m, splits).
- Island widens to 52cqw with “Run 24:18” only on the Run screen.
- Rest pose
rotateX(5deg) rotateY(-20deg), perspective 1100px. - Headline “Hold it to the light.” in Instrument Serif, italic half in ochre.
Implementation notes
Edge and thickness. The edge is one gradient that fakes a rounded titanium band: dark at the outer 1.6%, a highlight just inside, flat mid-tone across. Stack depth layers behind it.
.phone { aspect-ratio: 300/628; transform-style: preserve-3d;
transform: rotateX(calc(var(--rx,5)*1deg)) rotateY(calc(var(--ry,-20)*1deg)); }
.depth { position:absolute; inset:0; border-radius:15.5cqw; background:var(--f4); }
.depth.d1 { transform:translateZ(-4px); background:var(--f2); }
.depth.d2 { transform:translateZ(-8px); background:var(--f3); }
.depth.d3 { transform:translateZ(-12px); }
.depth.d4 { transform:translateZ(-16px); }
.edge { position:absolute; inset:0; border-radius:15.5cqw; padding:1.7cqw;
background:linear-gradient(90deg,var(--f4),var(--f1) 1.6%,var(--f2) 5%,var(--f2) 95%,var(--f1) 98.4%,var(--f4));
box-shadow:inset 0 0 0 .35cqw rgba(255,255,255,.25),
inset 0 .8cqw .6cqw -.4cqw rgba(255,255,255,.55),
inset 0 -.8cqw .8cqw -.4cqw rgba(0,0,0,.35); }
Tilt loop. Write unitless numbers to custom properties and let CSS multiply by 1deg, so the same value drives the glare and the shadow.
const REST = { rx: 5, ry: -20 }; let t = { ...REST }, c = { ...REST }, raf = 0;
function tick() {
c.rx += (t.rx - c.rx) * .1; c.ry += (t.ry - c.ry) * .1;
phone.style.setProperty('--rx', c.rx); phone.style.setProperty('--ry', c.ry);
screen.style.setProperty('--gx', 42 - c.ry * 1.6);
raf = Math.abs(t.rx - c.rx) + Math.abs(t.ry - c.ry) > .02 ? requestAnimationFrame(tick) : 0;
}
stage.addEventListener('pointermove', e => {
const r = stage.getBoundingClientRect();
t = { rx: -((e.clientY - r.top) / r.height - .5) * 24, ry: ((e.clientX - r.left) / r.width - .5) * 56 };
if (!raf) raf = requestAnimationFrame(tick);
});
Keys behind, button in front. Decorative keys sit at translateZ(-8px) so the edge overlaps their inner half. The clickable side button must sit at translateZ(1px): in a 3D context, hit testing follows Z, and a button behind the edge never receives the click.
Common mistakes:
- Using
box-shadowspread for thickness. It scales with the element and reads as a halo, not depth. - Putting
overflow: hiddenon.phone; it flattenspreserve-3d. Clip on the stage instead. - A drop shadow attached to the phone. The floor shadow is a separate blurred ellipse that stays on the floor.
- Leaving the rAF running forever after the pointer stops.
- Drawing a brand logo on the device. Corvo is invented; keep the back and edge blank.
Details
- Palette
- Type
- Instrument Serif · Geist
- Motion
- Rich motion
- Build
- Multi-step
- Tags
Studied
Icon MuseumLet the icons be the only colour. Give each one a shelf, a lift on hover, and a quiet label.
All sourcesOn the shelves