Design Lounge
Install

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

Open full size
1280 × 800 · 21.6 KB · 290 lines

Use it

Free to use · credit appreciated

1 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.

Brief

201 lines

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] │          └───────────────────────────────────┘│
│  └────────────────────┘                                               │
└──────────────────────────────────────────────────────────────────────┘
  • main.page is a two-column grid: minmax(260px, 340px) 1fr, gap 40px.
  • section.copy holds eyebrow p, the only h1, a lede p, and three fieldsets with legends.
  • section.stage is labelled (“Corvo 3 phone in Raw, showing the Lock screen”) and carries aria-roledescription="device mockup".
  • .rig sets the phone width and is container-type: inline-size, so every part of the phone and its screens is sized in cqw.
  • .phone (aspect 300/628, transform-style: preserve-3d) contains: four .depth layers, three decorative .key elements on the left, button.k-pow on the right, then .edge > .bezel > .screen.
  • .screen holds .island, three .scr panels and a .glare overlay.

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

RoleFamilySizeWeightLine-heightTrackingCase
HeadlineInstrument Serif56px (44px narrow)400, second line italic in accent1−0.01emsentence
Eyebrow / legendsGeist12px5001.450.14emuppercase
LedeGeist15px4001.450sentence
Segmented labelsGeist14px50010sentence
Lock timeInstrument Serif30cqw4001−0.02em—
Journal title, run distanceInstrument Serif11cqw / 24cqw4001−0.02em—
In-screen bodyGeist3.4–3.6cqw400/6001.30—

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

ThingTriggerPropertyFrom → toDuration / easingReduced motion
Tiltpointermove on stagerotateX, rotateYcurrent → targetrAF lerp 0.1 per frame, stops under 0.02°disabled, phone stays at rest pose
ReturnpointerleaverotateX, rotateYcurrent → 5°, −20°same lerp—
Glarefollows tiltgradient stop42% − ry × 1.6with tiltstatic
Screen swapscreen radioopacity, scale0, 1.04 → 1, 1380ms standard / 500ms expo outinstant
IslandRun on/offwidth29cqw ↔ 52cqw, content fades in after 200ms450ms expo outinstant
Sleepside buttonscreen opacity, scale1 → 0, 0.98380msinstant
Stagestage radiobackground, colourlight ↔ dark400ms standardinstant
Swatchcheckedinner dot scale1 → 0.86, ring appears180msinstant

States

  • Swatch resting: 28px gradient dot inside a 40px hit target. Checked: dot scales to 0.86 and a 1.5px --ink ring appears 2px inside the target.
  • Segmented option checked: filled --ink pill with --bg text. Unchecked: --ink-2 text on transparent.
  • Focus-visible on radios: 2px --accent outline, 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 #e08a4f to keep contrast.

Accessibility

  • Finish, Screen and Stage are radio groups inside fieldset/legend, so arrow keys move within each group for free. Swatch inputs carry aria-label with the finish name.
  • The stage section label 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 button with aria-label="Side button: sleep or wake the screen" and an invisible hit extension (::before inset −6px −16px −6px −8px) because the visible key is only ~5px wide.
  • Tilt is pointer-only decoration; nothing is lost without it.
  • Contrast: #1d1a16 on #ebe5da ≈ 14:1; #5b544a lede ≈ 6.3:1; #7a7266 labels ≈ 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 let cqw do 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-shadow spread for thickness. It scales with the element and reads as a halo, not depth.
  • Putting overflow: hidden on .phone; it flattens preserve-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
Designed bySusan AcharyaKathmandu

Studied

Icon Museum

Let the icons be the only colour. Give each one a shelf, a lift on hover, and a quiet label.

All sources