Design Lounge
Install

Nº 195 of 520

Click-wheel player mockup

A CSS click-wheel music player with a working wheel: trace a circle to scroll the menu, press the centre to drill in, play tracks, scroll volume.

Preview

Open full size
1280 × 800 · 19.9 KB · 285 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 "Click-wheel player mockup" (piece mockup-ipod-classic) 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

196 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 classic pocket music player, the fictional Aubade 160, drawn in CSS on an acid-lime poster stage with a giant “Spin to scroll.” headline. The player has a chrome rim, a hold switch and a headphone jack on the top edge, a colour LCD with a pixel-font menu, and a working click wheel. Drag your finger around the wheel and the highlight moves one row per 22° of rotation; press the centre to go in; press MENU to go back. Choosing a song opens Now Playing, where the wheel becomes a volume control. Finishes are White and Black. The detail worth copying is the angular accumulator: rotation is measured as the signed change in atan2 between pointer events, unwrapped across ±180°, so the wheel works in both directions, at any speed, from any starting point.

Reference behaviour

  1. First frame: White finish, main menu with “Music” highlighted, a split screen with the menu on the left 52% and album art on the right. A track is paused at 1:04.
  2. Main menu items: Music, Playlists, Artists, Settings, Shuffle Songs, Now Playing. All but Now Playing and the toggles show a › chevron.
  3. Pointer down on the wheel ring, then move around it: once total rotation passes 12°, the gesture becomes a drag (pointer captured, a soft shaded arc appears under the finger). Every 22° clockwise moves the highlight down one row; anticlockwise moves it up. It stops at the ends, no wrap.
  4. The right-hand art preview changes with the highlighted item.
  5. A press on the ring without dragging hits the button under it: MENU (top) back, ⏮ (left) previous, ⏭ (right) next, ⏯ (bottom) play/pause.
  6. Centre button: in a list, enter the item. Music → Songs (7). Playlists → 4 playlists. Artists → 3 artists. Settings → Shuffle / Repeat / Backlight, where pressing the centre cycles the value shown on the right of the row. Shuffle Songs plays a random track. A song, playlist or artist starts playback and opens Now Playing.
  7. Now Playing: art (27cqw square), “3 of 7”, title, artist, album, a progress bar with elapsed and -remaining. The header icon shows play when playing, pause when paused. Time advances once per second; at the end the next track starts.
  8. Rotating the wheel in Now Playing changes volume by 4 per step; the progress bar is replaced by a volume bar for 1400ms after the last step.
  9. Centre in Now Playing toggles play/pause. MENU goes back up the stack, one level per press.
  10. Lists show six rows and scroll the window to keep the highlight visible.
  11. Finish toggle: White or Black recolours the front, wheel, centre button, glyphs and rim.
  12. A polite live region announces the highlighted item, “Now playing Paper Engines by The Velours”, “Paused”, or the finish.

Structure

1280 × 800, lime #d6f03b
┌──────────────────────────────────────────────────────────────────────┐
│  padding 40px / clamp(24px, 7vw, 110px)                               │
│  Spin                    148px bold                ┌─ hold ─── jack ┐ │
│  to scroll.              outline stroke 2px        │┌──────────────┐│ │
│  Aubade 160, a click-wheel player mockup…          ││▶  Aubade  ▭  ││ │
│  FINISH (● White | ● Black)                        ││Music  ›│ art ││ │
│  Drag around the wheel  ↑ ↓ scroll · Enter …       ││…       │     ││ │
│                                                    │└──────────────┘│ │
│                                                    │     MENU       │ │
│                                                    │ ⏮   ( ◯ )   ⏭ │ │
│                                                    │      ⏯         │ │
│                                                    └─ 330 × 550 ────┘ │
└──────────────────────────────────────────────────────────────────────┘
  • main.page: grid 1fr auto, gap 40px. section.poster holds the h1 (second line outlined), a sub paragraph, the finish group and a hint.
  • .rig: width min(330px, 100vw − 72px, (100vh − 110px) × .6), container-type: inline-size.
  • .shell (aspect 0.6, radius 9cqw, 1cqw chrome rim gradient) > .hold, .jack, .front.
  • .front > .lcd (84 × 64cqw, black surround) > .disp > .hdr + #v-list (ul role="listbox" + .peek) + #v-np.
  • .wheel (72cqw circle, 12cqw from the bottom) > .ring (role="slider", tabindex=0, covers the wheel), four button.wb, button.center (27cqw).

Tokens

:root {
  --stage: #d6f03b;     /* acid lime poster */
  --ink: #0f0f0e;  --ink-2: #2c3010;
  --hl: #ff4fa3;        /* selection bar, progress, wheel focus */
  --sans: "Familjen Grotesk", system-ui, sans-serif;
  --pix: "Pixelify Sans", ui-monospace, monospace;
  --ease: cubic-bezier(.2, .7, .2, 1);
  --ease-out: cubic-bezier(.16, 1, .3, 1);
  --step: 22deg;        /* rotation per menu row */
  /* White finish */
  --front: #f6f6f3; --front-2: #e4e4df; --wheel: #fbfbf9; --wheel-2: #e9e9e4;
  --glyph: #a4a4a0; --rim-1: #f4f5f6; --rim-2: #9ea2a7;
}
[data-finish="black"] { --front: #1d1d1f; --front-2: #0d0d0e; --wheel: #2b2b2e; --wheel-2: #1f1f22;
  --glyph: #8a8a8f; --rim-1: #7a7d82; --rim-2: #2a2b2e; }

LCD palette: page #f7f8f6, text #141414, header gradient #fdfdfd → #d9dbd8 with a #a9aca8 rule, selection gradient #ff7cbc → #ff4fa3 with white text. Album art is CSS only: teal with a yellow sun (a0), red and cream stripes (a1), black with a yellow disc (a2).

Typography

RoleFamilySizeWeightLine-heightTrackingCase
HeadlineFamiljen Groteskclamp(64px, 10.5vw, 148px)7000.86−0.045emsentence; line 2 outlined 2px
SubFamiljen Grotesk17px5001.40sentence
Labels, segmentedFamiljen Grotesk12–14px70010.14em for labelsuppercase labels
LCD menuPixelify Sans5.1cqw40010title case
LCD headerPixelify Sans5.1cqw60010—
LCD meta / timesPixelify Sans4–4.4cqw40010—
Wheel MENUFamiljen Grotesk4.4cqw70010.06emuppercase

Motion

ThingTriggerPropertyFrom → toDuration / easingReduced motion
Drag arcdraggingopacity of conic shade0 → 1200ms standardinstant
Arc positionpointer angleconic from anglefollows fingerlivesame
Highlighteach 22°row selectioninstant row change—same
Volume barwheel in Now Playingdisplay swapshown for 1400ms—same
Centre press:activescale1 → .97100msinstant
Wheel glyph press:activecolourglyph → pink120msinstant
Finishtogglecoloursswap300msinstant

The menu itself does not animate. A real click-wheel highlight jumps; easing it would feel laggy against the finger.

States

  • Row highlighted: pink gradient bar, white text, aria-selected="true".
  • Toggle rows (Settings): value right-aligned at 75% opacity, no chevron.
  • Now Playing row and song rows: no chevron.
  • Ring focus-visible: 3px pink outline, 2px offset, following the circle.
  • Buttons on the poster: 3px ink outline, 3px offset.
  • Finish button pressed: ink pill, lime text; its colour dot gets a lime ring.
  • Playing / paused: header glyph swaps; progress advances only while playing and the tab is visible.

Accessibility

  • The ring is a focusable role="slider" labelled “Click wheel. Up and down arrows scroll, Enter selects, Escape goes back”. aria-valuetext is the highlighted item, or “Volume 62 percent” in Now Playing.
  • Keys on the ring: ArrowDown/ArrowRight next, ArrowUp/ArrowLeft previous, Enter select, Escape or Backspace back, Space play/pause.
  • MENU, previous, next, play/pause and centre are real buttons with labels, reachable by Tab.
  • The menu ul is role="listbox" with role="option" rows and aria-activedescendant.
  • A drag never fires the button under the finger: the click is swallowed in the capture phase when a drag occurred.
  • Contrast: #0f0f0e on #d6f03b ≈ 15:1; #2c3010 sub ≈ 11:1; LCD text #141414 on #f7f8f6 ≈ 17:1; white on #ff4fa3 selection is ≈ 3:1 and is reinforced by the bar itself (it is a picture of a device UI).

Responsive rules

  • ≥1280: poster left, player right at 330px.
  • 1024: same; headline scales with 10.5vw.
  • ≤820: one column, player first, then poster; headline 64px.
  • At 375 the player is 100vw − 72px (303px) and everything inside scales with cqw; the wheel stays ≥ 210px, comfortable for a thumb.
  • Do not shrink the wheel below 200px; below that a circular drag becomes unreliable.

Acceptance checklist

Always

  • The player is CSS only; menus and screens are live HTML.
  • Rotation is measured from unwrapped atan2 deltas and accumulated; one row per fixed angle step.
  • A drag threshold (12°) separates a press from a turn; a drag never triggers a wheel button.
  • Centre selects, MENU goes back through a stack, the four wheel zones are real buttons.
  • In the playing screen the wheel controls volume with a temporary bar.
  • Keyboard parity on the focused ring.
  • touch-action: none on the wheel.
  • No horizontal scroll at 375px.

This demo

  • Name “Aubade 160”; finishes White and Black.
  • Main menu: Music, Playlists, Artists, Settings, Shuffle Songs, Now Playing.
  • Seven songs: Salt Lines, Harbour Bells, Paper Engines, Ribbon Road, Night Bus to Vale, Sodium Glow, Last Ferry (artists Marit Sand, The Velours, Odun).
  • 22° per row, volume step 4, volume bar shown 1400ms.
  • Headline “Spin / to scroll.” with the second line outlined.

Implementation notes

Angular accumulator. Unwrap each delta into −180…180 so crossing the 9 o’clock seam doesn’t jump a full turn.

const ang = e => { const r = wheel.getBoundingClientRect();
  return Math.atan2(e.clientY - r.top - r.height / 2, e.clientX - r.left - r.width / 2) * 180 / Math.PI; };
wheel.addEventListener('pointermove', e => {
  if (!active) return;
  const a = ang(e); let d = a - prevA;
  if (d > 180) d -= 360; if (d < -180) d += 360;
  prevA = a; acc += d; total += Math.abs(d);
  if (!dragging && total > 12) { dragging = true; wheel.setPointerCapture(e.pointerId); }
  if (dragging) {
    while (acc >= 22) { acc -= 22; scroll(1); }
    while (acc <= -22) { acc += 22; scroll(-1); }
  }
});

Swallow the click after a drag. Listen in the capture phase on the wheel and stop it before it reaches a button.

wheel.addEventListener('click', e => {
  if (swallow) { e.stopPropagation(); e.preventDefault(); swallow = false; return; }
  const b = e.target.closest('[data-act]'); if (b) act(b.dataset.act);
}, true);

Shade under the finger. One conic gradient whose start follows the pointer angle (+90° because atan2 zero is east and conic zero is north).

.wheel::before { content: ""; position: absolute; inset: 0; border-radius: 50%;
  background: conic-gradient(from calc(var(--a, 0) * 1deg - 45deg), transparent, rgba(0,0,0,.1) 45deg, transparent 90deg 360deg);
  opacity: 0; transition: opacity .2s var(--ease); }
.wheel.drag::before { opacity: 1; }

Common mistakes:

  • Mapping absolute angle to a list index. It jumps on touch-down and can’t scroll long lists.
  • Easing the highlight between rows.
  • Using vertical drag distance instead of rotation; it breaks on the left half of the wheel.
  • Making the centre button part of the drag surface.
  • Real product names or the original wheel’s exact graphics. The glyphs here are generic transport icons.

Details

Palette
Type
Familjen Grotesk · Pixelify Sans
Motion
Subtle motion
Build
Multi-step
Designed bySusan AcharyaKathmandu

Studied

UseArticle

Let the visitor press the controls and watch the card change. Thick black edges and hard shadows keep it friendly.

All sources

On the shelves