---
title: "BRAND Spotify 2026 · Recipe: play-button"
url: https://memocore.ai/memos/6ae436e1cb977478f04eb87b
project: "Brand Styles"
tags: "brand-spotify"
updated_at: "2026-10-30T06:32:11.016Z"
---

# BRAND Spotify 2026 · Recipe: play-button

HEADER
brand: Spotify; design_date: 2026-10; kind: component; used_in: Page: home, Page: detail-view, Page: artist, Page: search-results
captured: https://open.spotify.com/ 2026-10-30, Japan (market JP) by direct connection with the user's agreement; logged out; UI in English
evidence: every timing below was recorded live from transitionrun and animationstart events and getAnimations() on 2026-10-30, unless marked. (code) = read in CSS or JS, not seen live. (inf) = inferred. The playing state needs an account and was not seen.

LOOK
A perfect green circle with a black triangle. It is the only saturated element in the player. Under the pointer it grows slightly and brightens almost instantly, then relaxes three times slower when the pointer leaves. On cards it is hidden and rises into the corner of the cover when the card is hovered.

STRUCTURE
button (transparent, no padding, carries the transform) > span.inner (the circle, carries the colour, set bright-accent) > span.icon wrapper > svg.
Hit area is the whole circle. Scale goes on the button; colour goes on the inner span.
Card variant adds a wrapper around the button: absolute, carries the rise and fade.

KEY PARAMETERS
sizes: 56 x 56 action bar (icon 24), measured; 48 x 48 on cards and in the sticky bar (code); hover rect at 56 becomes 58.24, measured
colours: rest #1ed760; hover #3be477; press #1abc54 (code); icon #000000
radius: 9999px; no border; no shadow on the action bar version
card wrapper: absolute bottom 8, inset-end 8, z-index 2, radius 500, shadow 0 8px 8px #0000004d (code); pointer-events none until the card is hovered, then auto for fine pointers (code)
track row variant: plain white icon 16 x 16, no circle, no scale, replaces the index number on row hover
disabled: opacity .3, no scale (code); unavailable content changes the label to the "unavailable" string (code)

MOTION: interaction records
- id: play.hover
  element: button and inner span
  trigger: pointer enter
  open: transform none -> scale(1.04), 50ms, delay 0, cubic-bezier(.3,0,0,1); background-color #1ed760 -> #3be477, 50ms, same curve
  close: transform scale(1.04) -> none, 150ms, same curve; background-color #3be477 -> #1ed760, 150ms, same curve
  switch: each button runs its own transition, so moving to a neighbour runs the 150ms close here and the 50ms open there at the same time (inf)
  keyboard: none
  touch and mobile: not recorded
  reduced motion: scale exists only under prefers-reduced-motion: no-preference (code)
- id: play.press
  element: button and inner span
  trigger: pointer down
  open: :active sets transform to scale(1); the computed matrix is already 1 at pointerdown, so there is no visible shrink animation. Inner background is still #1ed760 in that first frame, then goes to #1abc54 by the CSS rule (colour value from code, duration 50ms inf because :hover still holds)
  close: Play returned in 150ms as the logged-out dialog opened. A tertiary button with the pointer still over it goes scale(1) -> scale(1.04) in 50ms, same rule
  switch: n/a
  keyboard: not recorded
  touch and mobile: not recorded
  reduced motion: no scale (code)
- id: play.focus
  element: button
  trigger: Tab
  open: browser default ring, computed outline rgb(238,238,238) auto; no custom ring and no transition; the tooltip fades in as in play.tooltip; hover scale stays if the pointer is also over it
  close: instant
  keyboard: reachable by Tab; after a pointer click the code calls blur (code)
  reduced motion: same
- id: play.tooltip
  element: floating tooltip hover-or-focus-tooltip, 14px white, label "Play <title>"
  trigger: hover or focus; showDelay 1000ms on Play (code, seen live); other buttons pass their own delay (500, 1000, 1500 in code) or none
  open: opacity 0 -> 1, 200ms, ease-in-out
  close: no exit animation recorded
  keyboard: shown on focus
- id: play.card-rise
  element: wrapper on a card
  trigger: card hover or focus inside
  open: transform translateY(12px) -> translateY(0) and opacity 0 -> 1, both 200ms, ease-out; starts together with the card plate, see Recipe: card-shelf
  close: same values reversed, 200ms, ease-out
  switch: an interrupted transition reverses from its current value with a shorter time: from opacity .38 back to 0 took 76ms
  touch and mobile: wrapper display none under pointer: coarse (code)
  reduced motion: transition exists only under no-preference, so the wrapper appears without animation (code)
  top result card variant: same rise, 300ms, ease
- id: play.click-logged-out
  element: full screen dialog
  trigger: click while logged out; URL gets #login
  open: overlay rgba(0,0,0,.9), opacity 0 -> 1, 200ms, ease. Panel 810 x 428, radius 8, centered: translateY(20px) and opacity 0 -> none and 1, 400ms, delay 150ms, ease-in-out. Panel background moves to the cover colour, 500ms, ease (seen #9bf0e1 -> #b84810), with a gradient darkening downward
  close: Esc closes with no exit animation recorded, hash removed
  contents: cover 300 x 300 left, 32/700 centered title "Start listening with a free Spotify account", green pill "Sign up free" 153 x 48, secondary pill "Download app" 174 x 48, "Already have an account? Log in" 14/600 underlined, tertiary "Close" 105 x 48 under the panel, 16/700 #b3b3b3
- id: play.state-change (code, needs an account, not seen)
  element: icon and label
  trigger: playback starts or stops for this context
  open: icon swaps from play to pause in the render, no transition in the code; aria-label and tooltip swap between "Play <title>" and "Pause"
  children: playing track rows use an equaliser image instead of the index: equaliser-animated-green.gif or -white.gif, 14px, static svg fallback (code). Playing row title colour #1ed760 (inf)
  close: swap back

STATES
Hover, focus, press, disabled: above. Loading and mobile web player version: not recorded here; the mobile layout has play 48 at the right of the action row (see Page: detail-view).

HOW TO BUILD
Button with transition background-color and transform at 150ms cubic-bezier(.3,0,0,1). On :hover set the duration to 50ms, so entering is fast and leaving uses the slower base time. On :active set transform to scale(1). Put colour on an inner span. Scale only inside prefers-reduced-motion: no-preference. Delay the tooltip 1000ms, then fade 200ms.

PITFALLS
Do not use the same duration both ways; the 50/150 split is the feel. Do not scale above 1.04. Do not add a shadow on the action bar version. Press is a return to 1, not a shrink below 1. The focus ring is the browser default here, do not invent a green one. The rise is 12px at run time, though the CSS file says translateY(8px).

SCREENSHOTS
- file: spotify_2026_play-button_d1440_states.jpeg
  shows: composite zoomed x2: rest, hover, focus-visible with hover held, active; action row; track row rest and hover
  source: live, Japan market, direct, 2026-10-30; the active frame is the CSS rule values applied by script because :active cannot be held in a capture
  key values: #1ed760, #3be477, #1abc54; scale 1.04
  do not copy: covers, names
- file: spotify_2026_play-button_d1440_hover-tooltip.jpeg
  shows: hover after the tooltip delay, 1440x900
  source: live, Japan market, direct, 2026-10-30; cookie banner hidden
  key values: tooltip delay 1000ms, fade 200ms
  do not copy: photo, cover
- file: spotify_2026_play-button_d1440_after-click.jpeg
  shows: logged-out dialog after click, 1440x900
  source: live, Japan market, direct, 2026-10-30; cookie banner hidden
  key values: overlay .9; panel 810 x 428 radius 8
  do not copy: cover, logo, copy
