Nº 469 of 520
Vinyl now-playing widget
A 70s now-playing widget in small and medium sizes: CSS-only album art, a record that spins while playing, a scrubber, and equaliser bars.
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 "Vinyl now-playing widget" (piece widget-now-playing) 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º 469 · "Vinyl now-playing widget" · designlounge.vercel.app -->
# Vinyl now-playing widget
> **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 home-screen style music widget for a fictional player, "Turntide", in two sizes. The page is cream with horizontal 70s stripes. The widget is espresso brown with a cream fat-italic display face and a mono for everything else. In the medium size, a square album sleeve sits on the left with a black record sliding out of it; the record spins while music plays and stops dead on pause. The right side has a four-bar equaliser, the title, the artist, a scrubber, times, and three round controls. The small size is a 212px square: the art fills it, the title and a play button sit on a dark fade at the bottom, and a 3px line shows progress. The detail worth copying: all three album covers are pure CSS gradients, swapped by one class on the widget, so the piece ships with no images.
## Reference behaviour
1. First frame: medium size, playing. Track "Monsoon Radio" by The Marigold Hours, album "Low Sun Over Patan", 3:48 long, at 1:14. The record spins (3.2s per turn). The four equaliser bars bounce. The size switch above reads Small | **Medium**.
2. Every second while playing, position advances one second: the scrubber thumb and orange fill move, elapsed counts up, remaining (`-m:ss`) counts down.
3. Play/pause (48px orange circle): toggles. On pause the record and the bars freeze where they are (`animation-play-state: paused`), the label above the title reads "Paused", the icon becomes a triangle, the button label becomes "Play".
4. Next: loads the next track at 0:00 and swaps the cover by class. Previous: if more than 3 seconds in, restarts the track; otherwise loads the previous track. Tracks loop.
5. Dragging the scrubber or pressing arrow keys on it seeks. While dragging, the clock does not move the thumb.
6. When a track ends it loads the next one automatically and a polite live region announces "Late Tram by Nilo Sato".
7. Hovering the sleeve (medium only) slides the record 12px further out of the sleeve over 420ms.
8. Small: the widget animates from 580 × 212 to 212 × 212 over 420ms expo out. The record fades out, the cover goes edge to edge, the medium body hides, and the mini overlay shows title, a small equaliser with the artist, a 44px play button, and the 3px progress line. Medium reverses it.
9. Reduced motion: no spin, no bouncing bars (they sit still at 70% height while playing), the size change is instant. The clock still runs.
## Structure
```
1280 × 800 · body grid, centred, padding 32px 16px · stack gap 28px
( SMALL | MEDIUM ) segmented switch, 44px tall
┌──────────────────────────────────────────────────────────────┐
│ ┌──────────┐◖) ▮▮▮▮ NOW PLAYING │ medium 580 × 212
│ │ cover │ record ◖) Monsoon Radio (28px display) │ padding 20px, r 28px
│ │ 172×172 │ 160px The Marigold Hours │ grid 240px | 1fr, gap 20px
│ │Low Sun… │◖) ━━━━━━━●──────────────── │ scrubber 24px hit, 4px track
│ └──────────┘ 1:14 -2:34 │
│ ⏮ (⏸) ⏭ Turntide │ 40px, 48px, 40px
└──────────────────────────────────────────────────────────────┘
small: 212 × 212, padding 0
┌────────────────┐
│ cover, full │
│ │
… 197 more lines. Copy or download for the full brief. Loading the source…Brief
237 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 home-screen style music widget for a fictional player, “Turntide”, in two sizes. The page is cream with horizontal 70s stripes. The widget is espresso brown with a cream fat-italic display face and a mono for everything else. In the medium size, a square album sleeve sits on the left with a black record sliding out of it; the record spins while music plays and stops dead on pause. The right side has a four-bar equaliser, the title, the artist, a scrubber, times, and three round controls. The small size is a 212px square: the art fills it, the title and a play button sit on a dark fade at the bottom, and a 3px line shows progress. The detail worth copying: all three album covers are pure CSS gradients, swapped by one class on the widget, so the piece ships with no images.
Reference behaviour
- First frame: medium size, playing. Track “Monsoon Radio” by The Marigold Hours, album “Low Sun Over Patan”, 3:48 long, at 1:14. The record spins (3.2s per turn). The four equaliser bars bounce. The size switch above reads Small | Medium.
- Every second while playing, position advances one second: the scrubber thumb and orange fill move, elapsed counts up, remaining (
-m:ss) counts down. - Play/pause (48px orange circle): toggles. On pause the record and the bars freeze where they are (
animation-play-state: paused), the label above the title reads “Paused”, the icon becomes a triangle, the button label becomes “Play”. - Next: loads the next track at 0:00 and swaps the cover by class. Previous: if more than 3 seconds in, restarts the track; otherwise loads the previous track. Tracks loop.
- Dragging the scrubber or pressing arrow keys on it seeks. While dragging, the clock does not move the thumb.
- When a track ends it loads the next one automatically and a polite live region announces “Late Tram by Nilo Sato”.
- Hovering the sleeve (medium only) slides the record 12px further out of the sleeve over 420ms.
- Small: the widget animates from 580 × 212 to 212 × 212 over 420ms expo out. The record fades out, the cover goes edge to edge, the medium body hides, and the mini overlay shows title, a small equaliser with the artist, a 44px play button, and the 3px progress line. Medium reverses it.
- Reduced motion: no spin, no bouncing bars (they sit still at 70% height while playing), the size change is instant. The clock still runs.
Structure
1280 × 800 · body grid, centred, padding 32px 16px · stack gap 28px
( SMALL | MEDIUM ) segmented switch, 44px tall
┌──────────────────────────────────────────────────────────────┐
│ ┌──────────┐◖) ▮▮▮▮ NOW PLAYING │ medium 580 × 212
│ │ cover │ record ◖) Monsoon Radio (28px display) │ padding 20px, r 28px
│ │ 172×172 │ 160px The Marigold Hours │ grid 240px | 1fr, gap 20px
│ │Low Sun… │◖) ━━━━━━━●──────────────── │ scrubber 24px hit, 4px track
│ └──────────┘ 1:14 -2:34 │
│ ⏮ (⏸) ⏭ Turntide │ 40px, 48px, 40px
└──────────────────────────────────────────────────────────────┘
small: 212 × 212, padding 0
┌────────────────┐
│ cover, full │
│ │
│░Copper Bus (⏸)│ fade from 0% to 92% espresso, 44px top padding
│░▮▮▮ Kesar & … │
│░━━━━━━─────────│ 3px progress at bottom 6px
└────────────────┘
- Segmented control:
div[role=group][aria-label="Widget size"]with twobutton[aria-pressed]. - Widget:
section[aria-label="Turntide now playing"]withdata-size="medium|small", classplayingwhen playing, and a track classt1|t2|t3. .sleeve(aria-hidden):.rec(the record, absolute, top 6px, left 80px, 160px) under.art(absolute, inset 0, r 10px). The art carries the album name in the display face, bottom-left..body: top line (equaliser + state word),p.title,p.artist,.scrub(input[type=range]+ times row),.ctrls(prev, play/pause, next, wordmark)..mini: shown only in small. It repeats title, artist, play/pause, and a decorative progress bar.- One
p[aria-live=polite], visually hidden.
Tokens
:root {
--page: #e9dcc0; /* cream page */
--stripe: #e2d2b1; /* 4px stripes every 26px */
--card: #2b1d14; /* espresso widget */
--card-2: #3a281c; /* scrubber track, button hover */
--ink: #f3e7cf; /* title, icons on orange */
--ink-2: #d2c1a2; /* artist, idle icons */
--ink-3: #a8957a; /* labels, times, wordmark */
--orange: #d9541e; /* accent: play button, scrubber fill, record label, focus */
--mustard: #e3a92b; /* equaliser, selected size */
--olive: #7b7a33; /* cover 2 */
--vinyl: #141010;
--disp: "Shrikhand", Georgia, serif;
--mono: "Space Mono", ui-monospace, monospace;
--medium-w: 580px; --medium-h: 212px; --small: 212px;
--pad: 20px; --r-widget: 28px; --r-cover: 10px;
--cover: 172px; --record: 160px; --record-left: 80px;
--shadow: 0 2px 0 #1a110b, 0 30px 50px -30px rgba(43, 29, 20, .7);
--std: cubic-bezier(.2, .7, .2, 1);
--expo: cubic-bezier(.16, 1, .3, 1);
--t-size: 420ms;
--t-spin: 3.2s; /* linear, one turn */
}
Covers, set as --art on the widget class:
.t1 { --art: radial-gradient(circle at 50% 118%, #f3e7cf 0 14%, #e3a92b 14% 26%, #d9541e 26% 38%,
#a5371a 38% 50%, #5a2a17 50% 62%, transparent 62%), linear-gradient(#efd9a8, #efd9a8); --ac: #2b1d14; }
.t2 { --art: radial-gradient(circle at 70% 30%, #e3a92b 0 18%, transparent 18.5%),
repeating-linear-gradient(90deg, #7b7a33 0 18px, #5f5e26 18px 36px); --ac: #f3e7cf; }
.t3 { --art: repeating-radial-gradient(circle at 30% 70%, #f3e7cf 0 8px, #d9541e 8px 16px, #2b1d14 16px 24px); --ac: #f3e7cf; }
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Title | Shrikhand | 28px | 400 | 1.05 | 0 | Title, ellipsis |
| Cover album name | Shrikhand | 19px | 400 | 1 | -0.01em | Title, colour --ac |
| Mini title | Shrikhand | 18px | 400 | 1.05 | 0 | Title, ellipsis |
| Wordmark | Shrikhand | 14px | 400 | 1 | 0 | Title |
| State label | Space Mono | 10px | 700 | 1.4 | 0.16em | UPPER |
| Artist | Space Mono | 13px | 400 | 1.4 | 0 | Title, ellipsis |
| Times | Space Mono | 11px | 400 | 1.4 | 0 | tabular |
| Size switch | Space Mono | 12px | 700 | 1 | 0.08em | UPPER |
Shrikhand has one weight. Do not fake bold it. It is the only display face; everything else is Space Mono.
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Reduced motion |
|---|---|---|---|---|---|---|
| Record | playing | rotate | 0 → 360°, loop | 3.2s | linear | static |
| Record | pause | animation-play-state | running → paused | instant | — | — |
| Equaliser bar 1–4 | playing | scaleY | .25 → 1 → .45, alternate | 900 / 620 / 1100 / 740ms, delays 0 / −200 / −500 / −300ms | --std | static at .7 |
| Widget | size switch | width, height | 580×212 ↔ 212×212 | 420ms | --expo | instant |
| Record | size switch | opacity | 1 ↔ 0 | 240ms | --std | instant |
| Record | sleeve hover | left | 80 → 92px | 420ms | --expo | instant |
| Icon button | press | scale | 1 → .92 | 120ms | --std | same |
| Scrubber thumb | drag | scale | 1 → 1.25 | 120ms | --std | same |
| Segment | select | background, colour | transparent → mustard | 200ms | --std | instant |
Linear is right for the spin: a record turns at constant speed. Every UI move uses --std or --expo.
States
- Playing:
.playingon the widget; record spinning, bars moving, pause icon, label “Now playing”. - Paused: record and bars frozen mid-motion, play icon, label “Paused”.
- Medium / small:
data-sizeon the widget; the matching segment is mustard with espresso text andaria-pressed="true". - Icon button hover: background
--card-2, icon--ink. Play button hover#e8662f. - Focus-visible: 2px orange outline, 3px offset (0 on the range input).
- Seeking: while the range is being dragged, the clock does not overwrite it.
- Track change: cover class, title, artist, album name, and times update at once; position 0:00.
- Empty: a real product shows “Nothing playing” in the title slot with a flat
--card-2cover and disabled controls. Not shown in the demo.
Accessibility
- The widget is a
sectionlabelled “Turntide now playing”. The sleeve is decorative (aria-hidden). - Play/pause is a button whose
aria-labelflips between “Play” and “Pause”. Both play buttons (medium and mini) stay in sync. - Previous and next have labels “Previous track” and “Next track”.
- The scrubber is a native
input[type=range]witharia-label="Seek"andaria-valuetext="1:14 of 3:48", updated each second. Arrow keys seek by 1s, Page Up/Down by 10% (native). - The size switch is a group of two toggle buttons with
aria-pressed. - A polite live region names the new track on next, previous, or auto-advance. It does not announce the clock.
- Hit targets: 40px for prev/next, 48px play (44px in small), 24px-tall scrubber hit area, 36px segments inside a 44px pill.
- Contrast:
--inkon--card13:1.--ink-3on--card5.6:1.--inkon--orange3.6:1 is icon-only (≥ 3:1 for UI graphics).
Responsive rules
- ≥ 640: medium is 580 × 212, small 212 × 212, centred.
- < 540: medium becomes a column: width 100%, height auto, padding 16px, cover 150px, record 140px at left 70px, title 24px. Small stays 212px.
- The widget’s width is
min(580px, 100%), so it never causes horizontal scroll at 375px. - Long titles and artists ellipsis on one line in both sizes; they never wrap.
- If shown on a phone home screen, small maps to a 2 × 2 widget and medium to 4 × 2.
Acceptance checklist
Always
- Two sizes from one element, switched with a
data-sizeattribute and a 420ms width/height transition. - The record spins only while playing, and pause freezes it in place rather than resetting.
- Equaliser bars use different durations and negative delays so they never move in step.
- Covers are CSS gradients, swapped by one class; no image files.
- Scrubber is a native range with a filled track and
aria-valuetextin m:ss. - Previous restarts the track if more than 3s in.
- Both play buttons share one state and one label.
- Track changes are announced politely; the clock is not.
- Hit targets ≥ 40px for buttons.
- Reduced motion: no spin, still bars, instant resize.
This demo
- Turntide, tracks Monsoon Radio (3:48), Copper Bus (4:12), Late Tram (3:05).
- Opens at 1:14 of Monsoon Radio, playing, medium.
- Widget
#2b1d14, r 28px; page#e9dcc0with#e2d2b1stripes 4px every 26px. - Play button 48px
#d9541e; equaliser#e3a92b. - Shrikhand for title and cover text; Space Mono for the rest.
Implementation notes
Pause by freezing, not removing. Removing the animation snaps the record back to 0°. animation-play-state holds the frame:
.rec { animation: spin 3.2s linear infinite; animation-play-state: paused; }
.w.playing .rec { animation-play-state: running; }
.eq span { transform-origin: bottom; animation: eq 900ms var(--std) infinite alternate; animation-play-state: paused; }
.w.playing .eq span { animation-play-state: running; }
@keyframes eq { 0% { transform: scaleY(.25) } 50% { transform: scaleY(1) } 100% { transform: scaleY(.45) } }
A filled range track with one custom property. Write the percentage from JS; the track gradient reads it:
input[type=range] { -webkit-appearance: none; appearance: none; height: 24px; background: transparent; --v: 0%; }
input[type=range]::-webkit-slider-runnable-track {
height: 4px; border-radius: 2px;
background: linear-gradient(90deg, var(--orange) var(--v), var(--card-2) var(--v)); }
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; width: 14px; height: 14px; margin-top: -5px;
border-radius: 50%; background: var(--ink); border: 3px solid var(--orange); }
Set --v on the widget too, so the small size’s 3px bar reads the same number.
Don’t fight the user’s drag. The 1s clock must skip writing seek.value while the thumb is held:
seek.addEventListener('input', () => { dragging = true; pos = +seek.value; paint(); });
seek.addEventListener('change', () => { dragging = false; });
setInterval(() => { if (!playing || dragging) return; pos++; pos >= dur ? next() : paint(); }, 1000);
The record is a radial gradient for the label and spindle hole plus a repeating-radial-gradient of 1px lighter rings every 3px for grooves, with a faint conic highlight in ::after. The highlight spins with the record; that is acceptable at this size.
Common mistakes:
- A glossy gradient play button. The button is flat orange.
- Spinning the cover instead of the record.
- Letting the title wrap to two lines in medium; the scrubber gets pushed out of the widget.
- Using
easefor the resize. Use expo out so the small square settles. - Duplicating state for the mini play button instead of sharing one
setPlaying(). - Purple or neon equaliser bars. They are mustard.
Rebuild order:
- Page stripes, segmented switch.
- Medium shell, sleeve with cover and record.
- Body: label, title, artist, range, times, controls.
- Track data,
load(),paint(), the 1s clock. - Play/pause with frozen animations; equaliser.
- Small size overlay and the resize transition.
- Live region, reduced motion, the column layout under 540px.
Details
- Palette
- Type
- Shrikhand · Space Mono
- Motion
- Subtle motion
- Build
- One session
- Tags
On the shelves