---
title: "BRAND Spotify 2026 · Recipe: card-shelf"
url: https://memocore.ai/memos/6ae42ececb977478f04eb81f
project: "Brand Styles"
tags: "brand-spotify"
updated_at: "2026-10-30T06:27:47.001Z"
---

# BRAND Spotify 2026 · Recipe: card-shelf

HEADER
brand: Spotify; design_date: 2026-10; kind: component; used_in: Page: home, Page: artist, Page: search-results
captured: https://open.spotify.com/ 2026-10-30, Japan (market JP) by direct connection with the user's agreement; content is Japanese, the UI language is English. Through the US proxy the config says market US but the shelves are the same Japanese content, because the API is called from the browser.
evidence: timings recorded live from transitionrun events and getAnimations() on that URL unless marked; (code) = read in CSS or JS, not seen live

LOOK
Horizontal rows of square covers on the dark main view, each with a title and a grey subtitle below. On hover a soft grey plate grows out from behind the card and a round green play button rises into the lower right corner of the cover. Round chevrons fade in at the row edges, and a dark fade covers cut-off cards. Clicking a chevron glides the row by three cards.

STRUCTURE
section shelf: header row (title link left, "Show all" right), carousel. Carousel: relative overflow hidden wrapper; scroller; absolute overlay with two chevrons. Card: box with ::after plate, invisible click layer, cover with shadow, play button wrapper, title link, subtitle links.

KEY PARAMETERS
card at 1440 with 331 sidebar: 195.5 x 252, padding 12, cover 172 x 172, radius 6
cover: background --card-color (extracted colour, default #333), padding-bottom 100%, shadow 0 8px 24px #00000080; artist cards radius 50%
gap image to title 8; title body-medium 16/400 white, clamp 2; subtitle body-small 14/400 #b3b3b3, clamp 2
plate: ::after, at rest inset 12 and transparent; hover inset 0 and #1f1f1f
play wrapper and button: Recipe: play-button
shelf header: h2 24/700 SpotifyMixUITitle; "Show all" 14/700 #b3b3b3
edge fade: pseudo 120px wide, full height, linear-gradient(-90deg, rgba(18,18,18,.7), transparent) measured on the live page; left one mirrored
chevron: 32 circle, bg #1f1f1f, hover #2a2a2a, icon opacity .7 -> 1, 8px inset from the edge on hover (code)
scroll step: 587px at 1440 = 3 cards

MOTION: interaction records
- id: card.hover
  element: card plate, play wrapper
  trigger: pointer enter on the card
  open: ::after top, right, bottom, left 12px -> 0, and background-color transparent -> #1f1f1f, all 200ms, delay 0, cubic-bezier(.3,0,0,1). Play wrapper translateY(12px) -> 0 and opacity 0 -> 1, 200ms, ease-out
  close: same properties and durations reversed
  switch: an interrupted transition reverses from its current value with a shorter time: plate from 5.57px back to 12px in 107ms, wrapper opacity from .38 to 0 in 76ms
  keyboard: focus inside the card shows the play button (code); card outline 2px white, offset -2 (code)
  touch and mobile: play wrapper display none under pointer: coarse (code); the mobile web player has flat cards 152 wide, radius 4, swipe only, no plate
  reduced motion: play wrapper transition exists only under no-preference, so it appears without animation (code)
- id: card.press
  element: plate
  trigger: pointer down on the card body
  open: ::after inset -> -4px, background-color -> #000000, on the same 200ms curve (code; :active cannot be held in a capture, the sheet applies these values)
  close: release navigates to the item
- id: card.links
  element: title link, subtitle link, Show all
  trigger: hover
  open: underline appears instantly with no transition; title stays #fff, subtitle stays #b3b3b3, Show all goes #b3b3b3 -> #fff
  close: instant
- id: shelf.chevrons
  element: chevron overlay
  trigger: pointer enters the carousel
  open: opacity 0 -> 1, 150ms, ease-out
  close: opacity 1 -> 0, 150ms, ease-out
  children: chevron hover: background #1f1f1f -> #2a2a2a, 150ms ease-out (code); icon opacity .7 -> 1
- id: shelf.scroll
  element: scroller
  trigger: click on a chevron
  open: native smooth scroll, moves 587px in about 530ms: scrollLeft at about 75ms steps was 17, 279, 449, 533, 565, 583, 587 (fast start, long settle)
  children: new covers fade opacity 0 -> 1, 250ms, ease-out, as they load; the previous chevron and left fade appear through a scroll-driven animation on timeline --carousel-scroller (opacity 0 -> 1, fallback .15s ease-out)
  switch: scroll-snap-type inline mandatory, cards snap to their start edge
  touch and mobile: swipe
  reduced motion: scroll-behavior smooth exists only under no-preference (code)
- id: card.top-result
  element: larger card on the search page, 392 x 225, radius 8
  trigger: hover or focus inside
  open: background #181818 -> #282828, 300ms, ease, no inset growth; play wrapper translateY(12px) -> 0 and opacity 0 -> 1, 300ms, ease
  close: reversed

STATES
Hover, press, focus: above. Touch: above. Loading: --card-color block before the image, then 250ms fade. Selected: n/a.

HOW TO BUILD
CSS only: pseudo element plate animating inset and background, a wrapper with opacity and transform for the play button, scroll snap for the row. Edge fades with scroll-driven animations, or a small script toggling a class. Keep the two curves separate: productive on the plate, ease-out on the button.

PITFALLS
The plate must grow from inset 12 to 0, not fade in at full size. The play button appears on the cover, not on the plate. The button is green at rest even though the rest of the card is grey. The rise is 12px at run time, though the CSS file says translateY(8px). Plate is 200ms, the button inside reacts at 50ms: the button on the cover responds faster than the card. Edge fade is 70% dark, not solid.

SCREENSHOTS
- file: spotify_2026_card-shelf_d1440_hover.jpeg
  shows: first card hovered with plate and play button, right chevron visible, 1440x900
  source: live, Japan market, 2026-10-30; cookie banner hidden from script
  key values: plate #1f1f1f; button hover #3be477; cover 172 radius 6
  do not copy: covers, artist names
- file: spotify_2026_home_d1440_top.jpeg
  shows: the same shelf at rest
  source: live, Japan market, 2026-10-30; cookie banner hidden from script
  key values: card 196 x 252
  do not copy: covers
