Spotify
MIT
Dark, minimal music streaming interface with Spotify's signature green accent and refined typography, ideal for audio platforms and content-heavy dashboards
Layout StudioImport this kit into a Studio project and start editing.
CLI installRun it in any project. No account needed.
npx @layoutdesign/context install spotify# layout.md — Spotify Design System
---
## 0. Quick Reference
> **Stack:** Spotify web app · Token source: reconstructed-from-computed (low confidence, 1 CSS custom property found) · Styling: CSS custom properties + Bootstrap detected
> **How to apply:** Use as `var(--spotify-*)` in CSS, `style={{ prop: 'var(--spotify-*)' }}` in JSX, or `bg-[var(--spotify-*)]` in Tailwind.
```css
/* ── Core Tokens ── */
:root {
/* Colours */
--spotify-accent: rgb(30, 215, 96); /* Spotify green — primary CTA bg */
--spotify-bg-app: rgb(18, 18, 18); /* True app background (reconstructed) */
--spotify-bg-surface: rgb(24, 24, 24); /* Card/panel surface (reconstructed) */
--spotify-bg-elevated: rgb(40, 40, 40); /* Dialog/modal background */
--spotify-text-primary: rgb(255, 255, 255); /* Headings, primary text */
--spotify-text-secondary: rgb(179, 179, 179); /* Body, labels, nav items */
--spotify-text-muted: rgb(128, 128, 128); /* Chip text, subdued text */
--spotify-border: rgb(124, 124, 124); /* Secondary button border */
/* Typography */
--spotify-font-ui: "SpotifyMixUI", "Helvetica Neue", helvetica, arial, sans-serif;
--spotify-font-title: "SpotifyMixUITitle", "Helvetica Neue", helvetica, arial, sans-serif;
/* Spacing (4px base grid) */
--spotify-space-xs: 4px; --spotify-space-sm: 8px;
--spotify-space-md: 12px; --spotify-space-lg: 16px;
--spotify-space-xl: 20px; --spotify-space-2xl: 32px;
/* Radius */
--spotify-radius-sm: 2px; /* Tags, small UI chrome */
--spotify-radius-md: 4px; /* Inputs, cards */
--spotify-radius-full: 9999px; /* Pill buttons, search, toggles */
/* Motion */
--spotify-duration-fast: 0.15s; --spotify-ease: cubic-bezier(0.3, 0, 0, 1);
}
```
```tsx
// Primary CTA Button — production-ready
const CTAButton = ({ children, disabled }: { children: React.ReactNode; disabled?: boolean }) => (
<button
disabled={disabled}
style={{
background: disabled ? 'var(--spotify-text-muted)' : 'var(--spotify-accent)',
color: '#000',
fontFamily: 'var(--spotify-font-ui)',
fontWeight: 700,
fontSize: '14px',
padding: '12px 32px',
borderRadius: 'var(--spotify-radius-full)',
border: 'none',
cursor: disabled ? 'not-allowed' : 'pointer',
transition: `transform var(--spotify-duration-fast) var(--spotify-ease)`,
}}
>
{children}
</button>
);
```
**NEVER:**
- NEVER use a font other than `SpotifyMixUI` or `SpotifyMixUITitle` — never Inter, Roboto, or Arial as primary
- NEVER use a background colour other than `#121212` / `#181818` / `#282828` for dark surfaces
- NEVER use border-radius values between `4px` and `9999px` on buttons — Spotify uses either sharp (2–4px) or full pill
- NEVER hardcode `rgb(30, 215, 96)` — always reference `var(--spotify-accent)`
- NEVER use spacing not on the 4px grid — use `--spotify-space-*` tokens only
- NEVER place white or light text on the green accent without checking contrast (green bg requires **black** text, not white)
- NEVER use warm/orange/cool-blue brand surfaces (surface-2 through surface-6) as app chrome — they are contextual album art tints only
> **Full design system → see layout.md**
---
## 1. Design Direction & Philosophy
### Character & Mood
Spotify's web UI is a **dark, immersive music environment**. The aesthetic is premium but accessible: deep blacks and dark greys form the structural shell, with the iconic **Spotify Green (#1ED760 / rgb(30, 215, 96))** as the sole high-contrast accent. The interface recedes to let music and album art dominate visually.
### Aesthetic Intent
- **Minimal chrome, maximum content.** Navigation elements are subdued (`rgb(179, 179, 179)`) so the content grid reads first.
- **Spatial hierarchy through darkness.** Surfaces stack from near-black app background → slightly lighter card surfaces → elevated dialogs. No explicit borders separate regions — depth is created by tonal difference alone.
- **Type as utility, not decoration.** Two custom typefaces (`SpotifyMixUI` for UI/body, `SpotifyMixUITitle` for display headings) carry all typographic intent. No decorative type styles.
- **Pill shapes for interactive elements.** Buttons, search inputs, and toggles are universally pill-shaped (`border-radius: 9999px`). Structural containers (cards, dialogs) use sharp or minimal radius (2–8px).
### What This Design Explicitly Rejects
- **Light mode defaults.** There is no light background in the app shell.
- **Warm neutrals or beige.** All neutral surfaces are cool dark greys.
- **Decorative gradients on chrome.** Album art may introduce colour dynamically but chrome surfaces are flat.
- **Large expressive radius on containers.** Card and modal corners are sharp (4–8px), never rounded like a mobile card UI.
- **High-saturation accent colours beyond green.** The surface tints (orange, purple, teal, olive) seen in the extracted data are **album-art-derived contextual tints**, not primary brand colours.
---
## 2. Colour System
### Note on Confidence
The curated surface tokens (--spotify-bg-surface, --spotify-bg-app, --spotify-text-primary) were derived from element-level computed styles — the true dark background values (`#121212`, `#181818`, `#282828`) are reconstructed from dialog and element backgrounds in the extraction. The `--spotify-accent` green is **high confidence** (10 button instances confirmed). The colourful surface tokens (orange, purple, teal) are **low confidence contextual tints**, not UI chrome colours.
### Tier 1 — Primitives
```css
:root {
/* ── Greens ── */
--primitive-green-500: rgb(30, 215, 96); /* extracted: high confidence — primary CTA */
--primitive-green-400: rgb(29, 185, 84); /* reconstructed: hover state of green CTA */
/* ── Dark Neutrals ── */
--primitive-black: rgb(0, 0, 0);
--primitive-grey-950: rgb(18, 18, 18); /* reconstructed: true app background */
--primitive-grey-900: rgb(24, 24, 24); /* reconstructed: nav/sidebar bg */
--primitive-grey-800: rgb(40, 40, 40); /* extracted: dialog background (role_dialog) */
--primitive-grey-700: rgb(83, 83, 83); /* extracted: hover state backgrounds */
--primitive-grey-600: rgb(114, 114, 114);/* extracted: .ieMOQX hover bg */
--primitive-grey-500: rgb(124, 124, 124);/* extracted: secondary button border */
--primitive-grey-400: rgb(128, 128, 128);/* extracted: chip text, badge shadow */
--primitive-grey-300: rgb(179, 179, 179);/* extracted: body/label text */
--primitive-grey-200: rgb(255, 255, 255);/* extracted: primary text, heading text */
/* ── Contextual / Album-Art Tints (NOT UI chrome) ── */
--primitive-tint-orange: rgb(201, 82, 15); /* low confidence — album art tint */
--primitive-tint-mauve: rgb(134, 114, 124);/* low confidence — album art tint */
--primitive-tint-teal: rgb(57, 126, 155); /* low confidence — album art tint */
--primitive-tint-indigo: rgb(40, 32, 80); /* low confidence — album art tint */
--primitive-tint-olive: rgb(127, 119, 87); /* low confidence — album art tint */
}
```
### Tier 2 — Semantic Aliases
```css
:root {
/* ── Surfaces ── */
--spotify-bg-app: var(--primitive-grey-950); /* True app shell background */
--spotify-bg-surface: var(--primitive-grey-900); /* Card/panel/sidebar surfaces */
--spotify-bg-elevated: var(--primitive-grey-800); /* Dialogs, popovers, dropdowns */
--spotify-bg-hover: var(--primitive-grey-700); /* Interactive surface hover state */
/* ── Text ── */
--spotify-text-primary: var(--primitive-grey-200); /* Headings, primary labels, icons */
--spotify-text-secondary: var(--primitive-grey-300); /* Body copy, nav links, descriptions */
--spotify-text-muted: var(--primitive-grey-400); /* Subdued metadata, chips, placeholders */
--spotify-text-on-accent: var(--primitive-black); /* Text on green accent backgrounds */
/* ── Actions ── */
--spotify-accent: var(--primitive-green-500); /* Primary CTA background */
--spotify-accent-hover: var(--primitive-green-400); /* Primary CTA hover */
/* ── Borders ── */
--spotify-border: var(--primitive-grey-500); /* Secondary button border */
--spotify-border-focus: var(--primitive-grey-200); /* Focus ring colour */
/* ── Contextual Tints (album-art derived, use only for dynamic theming) ── */
--spotify-tint-1: var(--primitive-tint-orange);
--spotify-tint-2: var(--primitive-tint-mauve);
--spotify-tint-3: var(--primitive-tint-teal);
--spotify-tint-4: var(--primitive-tint-indigo);
--spotify-tint-5: var(--primitive-tint-olive);
}
```
### Tier 3 — Component Tokens
```css
:root {
/* ── Button: Primary ── */
--btn-primary-bg: var(--spotify-accent);
--btn-primary-bg-hover: var(--spotify-accent-hover);
--btn-primary-text: var(--spotify-text-on-accent);
--btn-primary-radius: var(--spotify-radius-full);
/* ── Button: Secondary ── */
--btn-secondary-bg: transparent;
--btn-secondary-border: var(--spotify-border);
--btn-secondary-text: var(--spotify-text-primary);
--btn-secondary-radius: var(--spotify-radius-full);
/* ── Card ── */
--card-bg: var(--spotify-bg-surface);
--card-radius: var(--spotify-radius-md); /* 4px */
--card-padding: var(--spotify-space-md); /* 12px */
--card-gap: var(--spotify-space-sm); /* 8px */
/* ── Dialog ── */
--dialog-bg: var(--spotify-bg-elevated);
--dialog-radius: 8px; /* reconstructed: moderate confidence, from role_dialog */
--dialog-margin: var(--spotify-space-2xl); /* 32px */
/* ── Input (search) ── */
--input-bg: var(--primitive-grey-200); /* white */
--input-text: var(--primitive-black);
--input-radius: var(--spotify-radius-full);/* 9999px */
--input-border: rgb(112, 112, 112);
--input-padding: 6px 35px 6px 15px;
/* ── Nav ── */
--nav-text: var(--spotify-text-secondary);
--nav-text-hover: var(--spotify-text-primary);
--nav-gap: var(--spotify-space-sm); /* 8px */
}
```
### Colour Palette Reference
| Token | Value | Usage |
|---|---|---|
| `--spotify-accent` | `rgb(30, 215, 96)` | **Primary CTA only** |
| `--spotify-bg-app` | `rgb(18, 18, 18)` | App shell background |
| `--spotify-bg-surface` | `rgb(24, 24, 24)` | Cards, panels, sidebar |
| `--spotify-bg-elevated` | `rgb(40, 40, 40)` | Dialogs, dropdowns |
| `--spotify-text-primary` | `rgb(255, 255, 255)` | Headings, active labels |
| `--spotify-text-secondary` | `rgb(179, 179, 179)` | Body, nav, descriptions |
| `--spotify-text-muted` | `rgb(128, 128, 128)` | Chips, metadata |
| `--spotify-text-on-accent` | `rgb(0, 0, 0)` | **Text on green — always black** |
---
## 3. Typography System
### Font Stacks
```css
:root {
--spotify-font-ui: "SpotifyMixUI",
CircularSp-Arab, CircularSp-Hebr, CircularSp-Cyrl,
CircularSp-Grek, CircularSp-Deva,
"Helvetica Neue", helvetica, arial,
"Hiragino Sans", "Hiragino Kaku Gothic ProN",
Meiryo, "MS Gothic", sans-serif;
--spotify-font-title: "SpotifyMixUITitle",
CircularSp-Arab, CircularSp-Hebr, CircularSp-Cyrl,
CircularSp-Grek, CircularSp-Deva,
"Helvetica Neue", helvetica, arial,
"Hiragino Sans", "Hiragino Kaku Gothic ProN",
Meiryo, "MS Gothic", sans-serif;
}
```
### Composite Type Tokens
```css
:root {
/* ── Display / Hero ── (use --spotify-font-title) */
--type-display: {
font-family: var(--spotify-font-title);
font-size: 24px; /* extracted: h2 computed style */
font-weight: 700;
line-height: normal; /* ~1.2 for display */
letter-spacing: normal;
}
/* ── Heading / H1 + H3 ── (use --spotify-font-ui) */
--type-heading: {
font-family: var(--spotify-font-ui);
font-size: 16px; /* extracted: h1, h3 computed style */
font-weight: 700;
line-height: normal;
letter-spacing: normal;
}
/* ── Body ── */
--type-body: {
font-family: var(--spotify-font-ui);
font-size: 16px; /* extracted: body computed style */
font-weight: 400;
line-height: normal;
letter-spacing: normal;
}
/* ── Body Small / Nav / Metadata ── */
--type-body-sm: {
font-family: var(--spotify-font-ui);
font-size: 14px; /* extracted: 163 elements at 14px — dominant small text */
font-weight: 400;
line-height: normal;
letter-spacing: normal;
}
/* ── Secondary Button Label ── */
--type-btn-secondary: {
font-family: var(--spotify-font-ui);
font-size: 14px; /* extracted: button_secondary computed style */
font-weight: 700;
line-height: normal;
letter-spacing: normal;
}
/* ── Legal / Footer ── */
--type-legal: {
font-family: var(--spotify-font-ui);
font-size: 12px; /* extracted: 9 elements — "Legal", "Privacy Policy" */
font-weight: 400;
line-height: normal;
letter-spacing: normal;
}
/* ── Badge / Superscript ── */
--type-badge: {
font-family: var(--spotify-font-ui);
font-size: 10.5px; /* extracted: 1 element — superscript "E" */
font-weight: 600;
line-height: normal;
letter-spacing: normal;
}
}
```
### Typography Scale Reference
| Token | Font | Size | Weight | Usage |
|---|---|---|---|---|
| `--type-display` | SpotifyMixUITitle | 24px | 700 | Section headers, hero titles |
| `--type-heading` | SpotifyMixUI | 16px | 700 | H1, H3, card titles |
| `--type-body` | SpotifyMixUI | 16px | 400 | Body paragraphs, labels |
| `--type-body-sm` | SpotifyMixUI | **14px** | 400 | Nav items, descriptions, metadata |
| `--type-btn-secondary` | SpotifyMixUI | 14px | 700 | Secondary button labels |
| `--type-legal` | SpotifyMixUI | 12px | 400 | Footer legal, privacy links |
| `--type-badge` | SpotifyMixUI | 10.5px | 600 | Superscript "E" badge |
### Pairing Rules
- `SpotifyMixUITitle` is used **only for h2-level display headings** — never for body or UI controls
- `SpotifyMixUI` handles all other text — body, labels, buttons, nav, legal
- **Never mix Inter, Roboto, or system-ui** as primary typeface — these are fallback-only
- No italic styles detected in extraction — **avoid italic as a design choice**
- No text-transform: uppercase detected — **avoid ALL CAPS labels**
---
## 4. Spacing & Layout
```css
:root {
/* ── Base Unit: 4px ── */
/* ── Spacing Scale ── */
--spotify-space-xs: 4px; /* Tight inline gaps, icon margin */
--spotify-space-sm: 8px; /* Card internal gap, nav gap, button padding (icon btn) */
--spotify-space-md: 12px; /* Card padding, button vertical padding */
--spotify-space-lg: 16px; /* Button horizontal padding, section rhythm unit */
--spotify-space-xl: 20px; /* Component separation */
--spotify-space-2xl: 32px; /* Section margins, dialog outer margin */
--spotify-space-3xl: 160px; /* Footer clearance (--ot-footer-space extracted exactly) */
/* ── Border Radius Scale ── */
--spotify-radius-sm: 2px; /* Tags, small list items, link buttons */
--spotify-radius-md: 4px; /* Cards, inputs (structural containers) */
--spotify-radius-dialog: 8px; /* Dialogs/modals — reconstructed from role_dialog */
--spotify-radius-full: 9999px; /* Pill buttons, search field, toggles, checkboxes */
}
```
### Grid & Breakpoints
| Breakpoint | Width | Notes |
|---|---|---|
| xs | 400px | Mobile small |
| sm | 576px | Mobile large (Bootstrap sm) |
| md | 768px | Tablet (Bootstrap md) |
| lg | 992px | Desktop small (Bootstrap lg) |
| xl | 1024px | Desktop standard |
| 2xl | 1280px | Desktop wide |
| *(also detected)* | 425px, 530px, 550px, 600px, 769px, 890px, 896px, 1023px | Spotify-specific tweaks |
### Layout Rules
- **Navigation:** `display: flex; flex-direction: column; gap: 8px` — vertical stacked nav
- **Cards:** `display: flex; flex-direction: column; gap: 8px; padding: 12px` — column-stacked content
- **Primary buttons:** `display: flex; flex-direction: row; justify-content: center; align-items: center; padding: 12px` — centred icon+label row
- **Secondary buttons:** `display: inline-flex; padding: 4px 16px 4px 36px` — left-padded for leading icon
- Prefer `flex` for component internals; use CSS Grid for content section layouts at page level
- **NEVER** use absolute positioning to achieve layout flow — use flex/grid gap
---
## 5. Page Structure & Layout Patterns
> **Source:** Inferred from layout digest + component inventory. No screenshots available. Rows marked "(inferred)" are not visually confirmed.
### 5.1 Section Map
| # | Section | Layout Type | Key Elements | Status |
|---|---|---|---|---|
| 1 | **Top Navigation Bar** | flex row, space-between | Logo, nav links, Search input (pill), Install App CTA, Log In + Sign Up buttons | (inferred) |
| 2 | **Sidebar / Left Nav** | flex column, gap: 8px | Library label, nav items (text-secondary), playlist list | from digest |
| 3 | **Main Content — Hero** | flex column | Heading (h2, 24px bold), body paragraph, primary CTA (green pill button) | (inferred) |
| 4 | **Feature/Card Grid** | flex row wrap / CSS Grid | 200 card instances — album art + title + metadata, gap: 8px | from digest |
| 5 | **Footer** | flex row wrap | Legal links (12px), Privacy, Cookie settings, Social icons | (inferred) |
### 5.2 Layout Patterns
**Navigation (Sidebar)**
```
display: flex;
flex-direction: column;
gap: var(--spotify-space-sm); /* 8px */
padding: 0;
background: var(--spotify-bg-surface); /* #181818 */
```
- Items are full-width links with `color: var(--spotify-text-secondary)` at rest
- Hover state: `color: var(--spotify-text-primary)` (white)
**Card Grid**
```
display: grid;
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
gap: var(--spotify-space-sm); /* 8px */
```
- Individual card: `display: flex; flex-direction: column; gap: 8px; padding: 12px; border-radius: 4px; background: var(--spotify-bg-surface)`
- Card image fills full card width with `border-radius: 4px`
- Card text: title `font-weight: 700; font-size: 14px`, metadata `font-weight: 400; font-size: 14px; color: var(--spotify-text-secondary)`
**Primary CTA Zone (Hero)**
- Button: `background: var(--spotify-accent); color: #000; border-radius: 9999px; padding: 12px 32px; font-weight: 700; font-size: 14px`
- **CTA text is black on green** — never white on green
**Dialog / Modal**
```
background: var(--spotify-bg-elevated); /* #282828 */
border-radius: 8px;
margin: var(--spotify-space-2xl); /* 32px */
```
### 5.3 Visual Hierarchy
1. **Album/playlist art** — dominant visual mass in the content grid
2. **Primary CTA (green)** — only high-saturation element in any view
3. **White headings** — H2 at 24px bold announces sections
4. **Secondary text (grey #B3B3B3)** — nav, metadata, body copy recede
5. **Muted text (#808080)** — chip labels, counts, helper text at lowest prominence
**Whitespace rhythm:** 8px within components, 12px for card padding, 32px for section-level separation.
### 5.4 Content Patterns
- **Card pattern (200 instances):** `[square artwork image] → [title: 14px bold white] → [subtitle: 14px regular grey]` stacked vertically in a flex column with 8px gap
- **Nav item pattern:** `[optional icon] + [label: 14px regular grey]` → hover: white. Stacked in 8px gap column.
- **Button pair pattern (hero/onboarding):** Green primary pill button + transparent secondary pill button with grey border, side by side
- **Footer link pattern:** 12px regular grey links in flex row with small gaps — legal text runs in a single condensed line
---
## 6. Component Patterns
### 6.1 Primary Button
**Anatomy:** `<button>` → `[optional leading icon]` + `[label text]`
**State Map:**
| State | Background | Text | Border | Transform |
|---|---|---|---|---|
| Default | `var(--spotify-accent)` `rgb(30,215,96)` | `#000` | none | — |
| Hover | `rgb(29,185,84)` (darker green) | `#000` | none | scale(1.04) |
| Focus | `var(--spotify-accent)` | `#000` | `2px solid #fff` outline | — |
| Active | `rgb(29,185,84)` | `#000` | none | scale(0.97) |
| Disabled | `var(--spotify-text-muted)` `#808080` | `rgba(0,0,0,0.5)` | none | — |
```tsx
// Primary Button — production-ready with all states
import React from 'react';
interface SpotifyButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'secondary';
loading?: boolean;
children: React.ReactNode;
}
export const SpotifyButton: React.FC<SpotifyButtonProps> = ({
variant = 'primary',
loading = false,
disabled = false,
children,
...rest
}) => {
const isPrimary = variant === 'primary';
const isDisabled = disabled || loading;
return (
<button
disabled={isDisabled}
aria-disabled={isDisabled}
style={{
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
fontFamily: 'var(--spotify-font-ui)',
fontSize: '14px',
fontWeight: 700,
lineHeight: 'normal',
letterSpacing: 'normal',
padding: '12px 32px',
borderRadius: 'var(--spotify-radius-full)',
border: isPrimary ? 'none' : '1px solid var(--spotify-border)',
background: isPrimary
? (isDisabled ? 'var(--spotify-text-muted)' : 'var(--spotify-accent)')
: 'transparent',
color: isPrimary
? (isDisabled ? 'rgba(0,0,0,0.5)' : 'var(--spotify-text-on-accent)')
: 'var(--spotify-text-primary)',
cursor: isDisabled ? 'not-allowed' : 'pointer',
opacity: isDisabled ? 0.6 : 1,
transition: `
background-color var(--spotify-duration-fast) var(--spotify-ease),
transform var(--spotify-duration-fast) var(--spotify-ease),
border-color var(--spotify-duration-fast) var(--spotify-ease)
`,
textDecoration: 'none',
whiteSpace: 'nowrap',
userSelect: 'none',
}}
onMouseEnter={e => {
if (isDisabled) return;
const el = e.currentTarget;
if (isPrimary) el.style.backgroundColor = 'rgb(29, 185, 84)';
else el.style.borderColor = 'var(--spotify-text-primary)';
el.style.transform = 'scale(1.04)';
}}
onMouseLeave={e => {
if (isDisabled) return;
const el = e.currentTarget;
if (isPrimary) el.style.backgroundColor = 'var(--spotify-accent)';
else el.style.borderColor = 'var(--spotify-border)';
el.style.transform = 'scale(1)';
}}
onMouseDown={e => {
if (isDisabled) return;
e.currentTarget.style.transform = 'scale(0.97)';
}}
onMouseUp={e => {
if (isDisabled) return;
e.currentTarget.style.transform = 'scale(1)';
}}
onFocus={e => {
e.currentTarget.style.outline = '2px solid var(--spotify-border-focus)';
e.currentTarget.style.outlineOffset = '2px';
}}
onBlur={e => {
e.currentTarget.style.outline = 'none';
}}
{...rest}
>
{loading ? <span aria-hidden="true">…</span> : children}
</button>
);
};
```
---
### 6.2 Card
**Anatomy:** `<div.card>` → `[artwork image]` + `[title]` + `[subtitle/metadata]` + `[optional play button overlay]`
**State Map:**
| State | Background | Cursor | Play Button |
|---|---|---|---|
| Default | `var(--spotify-bg-surface)` | default | hidden |
| Hover | slightly lighter `#2a2a2a` | pointer | visible, opacity 1 |
| Focus | `var(--spotify-bg-surface)` + outline | default | — |
```tsx
export const SpotifyCard: React.FC<{
imageUrl: string;
title: string;
subtitle: string;
onClick?: () => void;
}> = ({ imageUrl, title, subtitle, onClick }) => (
<div
tabIndex={0}
role="button"
onClick={onClick}
style={{
display: 'flex',
flexDirection: 'column',
gap: 'var(--spotify-space-sm)', /* 8px */
padding: 'var(--spotify-space-md)', /* 12px */
borderRadius: 'var(--spotify-radius-md)', /* 4px */
background: 'var(--spotify-bg-surface)',
cursor: 'pointer',
transition: `background-color var(--spotify-duration-fast) var(--spotify-ease)`,
fontFamily: 'var(--spotify-font-ui)',
}}
onMouseEnter={e => { e.currentTarget.style.backgroundColor = 'rgb(42, 42, 42)'; }}
onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'var(--spotify-bg-surface)'; }}
onFocus={e => {
e.currentTarget.style.outline = '2px solid var(--spotify-border-focus)';
e.currentTarget.style.outlineOffset = '2px';
}}
onBlur={e => { e.currentTarget.style.outline = 'none'; }}
>
<img
src={imageUrl}
alt={title}
style={{ width: '100%', aspectRatio: '1', borderRadius: 'var(--spotify-radius-md)', objectFit: 'cover' }}
/>
<p style={{ margin: 0, fontSize: '14px', fontWeight: 700, color: 'var(--spotify-text-primary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{title}
</p>
<p style={{ margin: 0, fontSize: '14px', fontWeight: 400, color: 'var(--spotify-text-secondary)', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{subtitle}
</p>
</div>
);
```
---
### 6.3 Search Input
**Anatomy:** `<div.search-wrapper>` → `[search icon]` + `<input>` + `[optional clear button]`
**State Map:**
| State | Border | Outline | Background |
|---|---|---|---|
| Default | `1px solid rgb(112,112,112)` | none | `#fff` |
| Focus | `1px solid #000` | none (border handles it) | `#fff` |
| Hover | `1px solid rgb(112,112,112)` | none | `#fff` |
| Disabled | `1px solid rgb(112,112,112)` | none | `rgb(230,230,230)` |
```tsx
export const SpotifySearch: React.FC<React.InputHTMLAttributes<HTMLInputElement>> = (props) => (
<input
type="search"
style={{
fontFamily: 'var(--spotify-font-ui)',
fontSize: '12.8px',
fontWeight: 400,
color: 'rgb(0, 0, 0)',
background: 'rgb(255, 255, 255)',
border: '1px solid rgb(112, 112, 112)',
borderRadius: 'var(--spotify-radius-full)', /* 9999px */
padding: '6px 35px 6px 15px',
outline: 'none',
width: '100%',
transition: `border-color var(--spotify-duration-fast) var(--spotify-ease)`,
}}
onFocus={e => { e.currentTarget.style.borderColor = '#000'; }}
onBlur={e => { e.currentTarget.style.borderColor = 'rgb(112, 112, 112)'; }}
{...props}
/>
);
```
---
### 6.4 Navigation Item
**Anatomy:** `<nav>` → `<ul>` → `<li>` → `<a>` or `<button>` → `[optional icon]` + `[label]`
**State Map:**
| State | Text Colour | Font Weight | Decoration |
|---|---|---|---|
| Default | `var(--spotify-text-secondary)` `#B3B3B3` | 400 | none |
| Hover | `var(--spotify-text-primary)` `#fff` | 400 | none |
| Active/Current | `var(--spotify-text-primary)` `#fff` | 700 | none |
| Focus | default + `2px solid #000 outline` | — | — |
| Disabled | `var(--spotify-text-muted)` `#808080` | 400 | none |
```tsx
export const SpotifyNavItem: React.FC<{
label: string;
href: string;
isActive?: boolean;
}> = ({ label, href, isActive }) => (
<a
href={href}
aria-current={isActive ? 'page' : undefined}
style={{
display: 'flex',
alignItems: 'center',
gap: 'var(--spotify-space-sm)',
fontFamily: 'var(--spotify-font-ui)',
fontSize: '14px',
fontWeight: isActive ? 700 : 400,
color: isActive ? 'var(--spotify-text-primary)' : 'var(--spotify-text-secondary)',
textDecoration: 'none',
padding: '4px 0',
transition: `color var(--spotify-duration-fast) var(--spotify-ease)`,
borderRadius: 'var(--spotify-radius-sm)',
}}
onMouseEnter={e => { e.currentTarget.style.color = 'var(--spotify-text-primary)'; }}
onMouseLeave={e => {
e.currentTarget.style.color = isActive
? 'var(--spotify-text-primary)'
: 'var(--spotify-text-secondary)';
}}
onFocus={e => {
e.currentTarget.style.outline = '2px solid #000';
e.currentTarget.style.outlineOffset = '2px';
}}
onBlur={e => { e.currentTarget.style.outline = 'none'; }}
>
{label}
</a>
);
```
---
### 6.5 Badge
**Anatomy:** `<span.badge>` — small label overlaid on icons or counts
**Token usage:**
- `border-radius: var(--spotify-radius-sm)` (2px)
- `box-shadow: rgb(128,128,128) 0px 0px 5px 0px`
- `background: var(--spotify-bg-elevated)`
- `color: var(--spotify-text-secondary)`
- `font-size: 12px`
- Default display is `none` — toggled to `block` when count > 0
---
### 6.6 Toggle / Checkbox (Icon Button)
**Anatomy:** `<button>` wrapping an icon SVG — pill-shaped, centred
**State Map:**
| State | Colour | Transform |
|---|---|---|
| Default | `var(--spotify-text-secondary)` | none |
| Hover | `var(--spotify-text-primary)` | none |
| Active (on) | `var(--spotify-accent)` | none |
| Focus | default + `1px solid #000 outline offset 1px` | none |
| Disabled | `var(--spotify-text-muted)` opacity 0.8 | none, pointer-events: none |
```css
.spotify-icon-btn {
display: flex;
align-items: center;
justify-content: center;
padding: var(--spotify-space-sm); /* 8px */
border-radius: var(--spotify-radius-full);
border: none;
background: transparent;
color: var(--spotify-text-secondary);
cursor: pointer;
transition: color var(--spotify-duration-fast) var(--spotify-ease),
transform var(--spotify-duration-fast) var(--spotify-ease);
}
.spotify-icon-btn:hover { color: var(--spotify-text-primary); }
.spotify-icon-btn:focus-visible {
outline: 1px solid #000;
outline-offset: 1px;
}
.spotify-icon-btn[aria-checked="true"] { color: var(--spotify-accent); }
.spotify-icon-btn:disabled {
color: var(--spotify-text-muted);
pointer-events: none;
opacity: 0.8;
}
```
---
## 7. Elevation & Depth
```css
:root {
/* ── Background Layers (tonal elevation, no explicit shadows) ── */
--elevation-0: var(--spotify-bg-app); /* rgb(18,18,18) — base shell */
--elevation-1: var(--spotify-bg-surface); /* rgb(24,24,24) — cards, nav */
--elevation-2: var(--spotify-bg-elevated); /* rgb(40,40,40) — dialogs, dropdowns */
--elevation-3: rgb(58, 58, 58); /* reconstructed: tooltips, popovers */
/* ── Shadows ── */
--shadow-none: none;
--shadow-badge: rgb(128, 128, 128) 0px 0px 5px 0px; /* extracted: badge component */
--shadow-inset-hover: rgba(0, 0, 0, 0.1) 0px 0px 0px 2px inset; /* extracted: CTA button hover */
/* ── Border Tokens ── */
--border-default: 1px solid var(--spotify-border); /* rgb(124,124,124) */
--border-input: 1px solid rgb(112, 112, 112);
--border-focus: 2px solid var(--spotify-border-focus); /* #fff */
--border-focus-sm: 1px solid #000; /* used in cookie/OneTrust contexts */
/* ── Z-Index Scale ── */
--z-base: 0;
--z-raised: 1; /* Hover states, card lift */
--z-sticky: 100; /* Sticky header/nav bar */
--z-overlay: 200; /* Modal backdrop */
--z-modal: 300; /* Dialog content */
--z-toast: 400; /* Notifications */
--z-tooltip: 500; /* Tooltips */
}
```
### Layering Principles
- **Depth is expressed through background colour values, not shadows** — dark surfaces at each elevation tier create the illusion of depth without drop shadows on structural elements
- Shadows appear only on **contextual overlay elements** (badge glow, button inset on hover)
- Dialogs (`role_dialog`) are distinct from surface elements by background colour alone: `#282828` vs `#181818`
- **Never use `box-shadow` to simulate card lift** — Spotify cards differentiate via background colour change on hover
---
## 8. Motion
```css
:root {
/* ── Duration ── */
--spotify-duration-fast: 0.15s; /* extracted: 58 elements — hover states, toggles */
--spotify-duration-base: 0.20s; /* extracted: 3 elements — primary button bg transitions */
--spotify-duration-slow: 0.22s; /* extracted: 4 elements — inputs, subtle transitions */
--spotify-duration-slide: 0.30s; /* extracted: badge slide transition */
/* ── Easing ── */
--spotify-ease: cubic-bezier(0.3, 0, 0, 1); /* extracted: button border/transform */
--spotify-ease-default: ease; /* extracted: dominant (179 elements) */
/* ── Keyframes ── */
/* slide-down-custom: extracted animation for sliding panels/banners */
}
@keyframes slide-down-custom {
0% { bottom: -100%; } /* reconstructed: start off-screen */
100% { bottom: 0px; } /* extracted: final state */
}
```
### Transition Map by Component
| Component | Property | Duration | Easing |
|---|---|---|---|
| Primary button | `background-color` | `0.2s` | `ease` |
| Secondary button | `border-color`, `transform` | `0.15s` | `cubic-bezier(0.3,0,0,1)` |
| Icon button / toggle | `color`, `transform` | `0.15s` | `cubic-bezier(0.3,0,0,1)` |
| Input / search | all | `0.22s` | `ease` |
| Nav link | `color` | `0.15s` | `cubic-bezier(0.3,0,0,1)` |
| Badge slide | `right` | `0.3s` | `ease` |
| Cookie banner | `bottom` (slide-down-custom) | `0.3s` | `ease` |
### Motion Rules
- **Micro-interactions only** — transitions are ≤ 0.3s and affect only `color`, `background-color`, `border-color`, or `transform`
- **Never animate layout properties** (`width`, `height`, `top`, `left`) on interactive components — use `transform: scale()` for size changes
- **Never use `transition: all`** on components you build — extract the specific properties that need to transition
- Respect `prefers-reduced-motion` — wrap all transitions in `@media (prefers-reduced-motion: no-preference)`
---
## 9. Anti-Patterns & Constraints
1. **Hardcoded colour values → AI agents default to writing `background: rgb(30, 215, 96)` inline → They do this because they inline values during component authoring without checking the token system. The token breaks the connection to future theme changes and makes the dark surface stack inconsistent. Use `var(--spotify-accent)` always — the CSS custom property is the single source of truth.**
2. **Wrong text colour on the green accent → AI agents write `color: white` on green buttons because white-on-colour is the common web pattern → Spotify's green CTA uses `color: #000` (black) — white on `rgb(30,215,96)` fails WCAG AA contrast. Always use `var(--spotify-text-on-accent)` which resolves to `#000`.**
3. **Arbitrary spacing values → AI agents write `padding: 10px 14px` or `gap: 6px` because they interpolate "looks right" values → Spotify uses a strict 4px grid. Off-grid values (`10px`, `6px`, `14px`) are not in the token set. Map to the nearest token: 10px → `var(--spotify-space-sm)` (8px), 14px → `var(--spotify-space-lg)` (16px).**
4. **Using Inter or system-ui as primary font → AI agents default to Inter because it is the most common design-system font → The primary typeface is `SpotifyMixUI` / `SpotifyMixUITitle`. These are proprietary and loaded via `@font-face`. If they fail to load, the fallback chain is `"Helvetica Neue", helvetica, arial` — not Inter, not system-ui.**
5. **Applying border-radius 8px–48px to buttons → AI agents apply "rounded" styling (often 8px or 16px) to buttons because these are common design system defaults → Spotify's button radius is binary: sharp/container (2–4px) OR full pill (9999px). Any value between 4px and 9999px on a button is wrong. Check component type: if it is an interactive action button, it is pill-shaped (`var(--spotify-radius-full)`).**
6. **Dynamic Tailwind class construction → AI agents write `className={\`bg-\${colour}\`}` for theming → Tailwind's JIT compiler cannot detect dynamically constructed class strings and purges them, producing broken styles in production. Always use the complete class string: `className="bg-[var(--spotify-accent)]"` or inline styles with CSS custom properties.**
7. **Missing focus states → AI agents omit `:focus` / `:focus-visible` styles because they are not visible during visual design review → Spotify has explicit focus ring styles on every interactive element (`2px solid #000` or `2px solid #fff`). An accessible focus ring is required on all `button`, `a`, `input`, and `[role="button"]` elements. Add `onFocus`/`onBlur` handlers or `:focus-visible` CSS for all interactive components.**
8. **Using the tint colours (orange/purple/teal) as UI chrome → AI agents see `rgb(201,82,15)` in the colour token list and use it as a button colour or card background → These surface tints (surface-2 through surface-6) are album-art-derived contextual palette values — they are dynamically generated from cover art and applied as background tints for individual playlist/album pages. NEVER use them as static UI colours. All navigation, cards, and interactive chrome must use the dark neutral scale.**
9. **`transition: all` on components → AI agents write `transition: all 0.2s ease` as a shorthand → `transition: all` captures layout properties (`width`, `height`, `padding`) and triggers expensive layout recalculations on every state change, causing jank. Explicitly name the properties: `transition: background-color 0.2s ease, color 0.15s cubic-bezier(0.3,0,0,1)`.**
10. **`!important` overrides for dark theme → AI agents use `!important` when CSS specificity conflicts with Bootstrap (detected on this site) → `!important` makes the token system impossible to override for legitimate variant states (e.g. disabled, loading). Resolve Bootstrap specificity conflicts by scoping styles to a parent selector (`.spotify-app button`) or increasing specificity without `!important`.**
11. **Absolute positioning for list/card layout → AI agents place cards with `position: absolute; left: X; top: Y` when generating grid layouts from visual references → Spotify's card grid is a flex/CSS grid layout with `gap: 8px`. Absolute positioning breaks reflow when content changes length, when viewport resizes, or when new cards are added dynamically. Use `display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 8px`.**
---
## Appendix A: Complete Token Reference
Every token extracted from the source. §0 CORE TOKENS is the primary AI signal; this appendix is reference material an AI can cross-check against when a curated role is missing.
```css
/* Colours (51) */
--brand-primary-cta: rgb(30, 215, 96); /* Primary CTA background, dominant on 10 buttons — e.g. "span" /* mined from computed styles */ */
--brand-surface-2: rgb(201, 82, 15); /* Brand surface, dominant on 1 element — e.g. "div" /* mined from computed styles */ */
--brand-surface-3: rgb(134, 114, 124); /* Brand surface, dominant on 1 element — e.g. "div" /* mined from computed styles */ */
--brand-surface-4: rgb(57, 126, 155); /* Brand surface, dominant on 1 element — e.g. "div" /* mined from computed styles */ */
--brand-surface-5: rgb(40, 32, 80); /* Brand surface, dominant on 1 element — e.g. "div" /* mined from computed styles */ */
--brand-surface-6: rgb(127, 119, 87); /* Brand surface, dominant on 1 element — e.g. "div" /* mined from computed styles */ */
--spotify-accent: rgb(30, 215, 96);
--spotify-bg-app: rgb(18, 18, 18);
--spotify-bg-surface: rgb(24, 24, 24);
--spotify-bg-elevated: rgb(40, 40, 40);
--spotify-text-primary: rgb(255, 255, 255);
--spotify-text-secondary: rgb(179, 179, 179);
--spotify-text-muted: rgb(128, 128, 128);
--spotify-border: rgb(124, 124, 124);
--primitive-green-500: rgb(30, 215, 96);
--primitive-green-400: rgb(29, 185, 84);
--primitive-black: rgb(0, 0, 0);
--primitive-grey-950: rgb(18, 18, 18);
--primitive-grey-900: rgb(24, 24, 24);
--primitive-grey-800: rgb(40, 40, 40);
--primitive-grey-700: rgb(83, 83, 83);
--primitive-grey-600: rgb(114, 114, 114);
--primitive-grey-500: rgb(124, 124, 124);
--primitive-grey-400: rgb(128, 128, 128);
--primitive-grey-300: rgb(179, 179, 179);
--primitive-grey-200: rgb(255, 255, 255);
--primitive-tint-orange: rgb(201, 82, 15);
--primitive-tint-mauve: rgb(134, 114, 124);
--primitive-tint-teal: rgb(57, 126, 155);
--primitive-tint-indigo: rgb(40, 32, 80);
--primitive-tint-olive: rgb(127, 119, 87);
--spotify-bg-hover: var(--primitive-grey-700);
--spotify-text-on-accent: rgb(0, 0, 0);
--spotify-border-focus: var(--primitive-grey-200);
--btn-primary-bg: var(--spotify-accent);
--btn-primary-bg-hover: var(--spotify-accent-hover);
--btn-secondary-bg: transparent;
--btn-secondary-border: var(--spotify-border);
--card-bg: var(--spotify-bg-surface);
--dialog-bg: var(--spotify-bg-elevated);
--input-bg: rgb(255, 255, 255);
--input-text: rgb(0, 0, 0);
--input-border: rgb(112, 112, 112);
--elevation-3: rgb(58, 58, 58);
--border-default: 1px solid var(--spotify-border);
--border-input: 1px solid rgb(112, 112, 112);
--border-focus: 2px solid var(--spotify-border-focus);
--border-focus-sm: 1px solid #000;
--elevation-0: rgb(18, 18, 18);
--elevation-1: rgb(24, 24, 24);
--elevation-2: rgb(40, 40, 40);
/* Typography (14) */
--font-size-xs: 10.5px; /* 1 element — e.g. span "E" /* mined from computed styles */ */
--font-size-sm: 11px; /* 1 element — e.g. button "Cookie Settings" /* mined from computed styles */ */
--font-size-md: 12px; /* 9 elements — e.g. span "Legal", span "Safety & Privacy Cen", span "Privacy Policy" /* mined from computed styles */ */
--font-size-lg: 14px; /* 163 elements — e.g. p "Preview of Spotify", span "Install App", span "It's easy, we'll hel" /* mined from computed styles */ */
--font-size-xl: 16px; /* 91 elements — e.g. h1 "Your Library", p "Sign up to get unlim", p "Requiem" /* mined from computed styles */ */
--font-weight-regular: 400; /* 240 elements — e.g. p "Sign up to get unlim", p "Requiem", p "KILLY MANJARO" /* mined from computed styles */ */
--font-weight-medium: 600; /* 1 element — e.g. span "E" /* mined from computed styles */ */
--font-weight-semibold: 700; /* 24 elements — e.g. h1 "Your Library", p "Preview of Spotify", p "Company" /* mined from computed styles */ */
--spotify-font-ui: "SpotifyMixUI", ...;
--spotify-font-title: "SpotifyMixUITitle", ...;
--btn-primary-text: var(--spotify-text-on-accent);
--btn-secondary-text: var(--spotify-text-primary);
--nav-text: var(--spotify-text-secondary);
--nav-text-hover: var(--spotify-text-primary);
/* Spacing (20) */
--ot-footer-space: 160px;
--space-xs: 2px; /* 12 elements — e.g. div .e-10451-legacy-list-row__column, div .e-10451-legacy-list-row__column, div .e-10451-legacy-list-row__column /* mined from computed styles */ */
--space-sm: 4px; /* 49 elements — e.g. section .JlQyoTD6puMgqY_y, div .e-10451-card__column, div .e-10451-card__column /* mined from computed styles */ */
--space-md: 8px; /* 83 elements — e.g. header .ctNLVWve8CosEgh2, header .ctNLVWve8CosEgh2, header .ctNLVWve8CosEgh2 /* mined from computed styles */ */
--space-lg: 12px; /* 85 elements — e.g. div .e-10451-box, div .e-10451-box, div .e-10451-box /* mined from computed styles */ */
--space-xl: 16px; /* 10 elements — e.g. section .Re_OXhMIUkvAxhof, section .Re_OXhMIUkvAxhof, section .Re_OXhMIUkvAxhof /* mined from computed styles */ */
--space-2xl: 20px; /* 10 elements — e.g. section .Re_OXhMIUkvAxhof, section .Re_OXhMIUkvAxhof, section .Re_OXhMIUkvAxhof /* mined from computed styles */ */
--space-3xl: 32px; /* 3 elements — e.g. section .JlQyoTD6puMgqY_y, nav .sc-jxOSlx, nav .sc-jxOSlx /* mined from computed styles */ */
--spotify-space-xs: 4px;
--spotify-space-sm: 8px;
--spotify-space-md: 12px;
--spotify-space-lg: 16px;
--spotify-space-xl: 20px;
--spotify-space-2xl: 32px;
--card-padding: var(--spotify-space-md);
--card-gap: var(--spotify-space-sm);
--dialog-margin: var(--spotify-space-2xl);
--input-padding: 6px 35px 6px 15px;
--nav-gap: var(--spotify-space-sm);
--spotify-space-3xl: 160px;
/* Radius (13) */
--radius-sm: 2px; /* 16 elements — e.g. button .ot-link-btn "Liste der Partner (A", button .ot-link-btn "Liste von IAB-Liefer", button .ot-link-btn "| Illustrationen anz" /* mined from computed styles */ */
--radius-md: 2.5px; /* 1 element — e.g. div .otPcCenter "Über Deine Privatsph" /* mined from computed styles */ */
--radius-lg: 4px; /* 1 element — e.g. input .e-10451-form-input /* mined from computed styles */ */
--radius-full: 6px; /* 24 elements — e.g. div .e-10451-box "RequiemFarid Bang, K", div .e-10451-box "KILLY MANJAROSummer ", div .e-10451-box "hate that i made you" /* mined from computed styles */ */
--spotify-radius-sm: 2px;
--spotify-radius-md: 4px;
--spotify-radius-full: 9999px;
--btn-primary-radius: var(--spotify-radius-full);
--btn-secondary-radius: var(--spotify-radius-full);
--card-radius: var(--spotify-radius-md);
--dialog-radius: 8px;
--input-radius: var(--spotify-radius-full);
--spotify-radius-dialog: 8px;
/* Effects (3) */
--shadow-none: none;
--shadow-badge: rgb(128,128,128) 0 0 5px 0;
--shadow-inset-hover: rgba(0,0,0,0.1) 0 0 0 2px inset;
/* Motion (5) */
----motion-slide-down-custom: @-webkit-keyframes slide-down-custom {
0% { }
100% { bottom: 0px; }
}; /* @keyframes slide-down-custom */
--duration-fast: 0.15s; /* 58 elements — e.g. button, button, button /* mined from computed styles */ */
--duration-base: 0.2s; /* 3 elements — e.g. button, button, button /* mined from computed styles */ */
--duration-slow: 0.22s; /* 4 elements — e.g. button, input, input /* mined from computed styles */ */
--ease-default: ease; /* 179 elements — e.g. button, button, button /* mined from computed styles */ */
```
## Appendix B: Token Source Metadata
```
tokenSource: reconstructed-from-computed
confidence: low (1 CSS custom property found in source: --ot-footer-space: 160px)
extraction-date: spotify.com (website snapshot)
css-vars-found: 1 (--ot-footer-space belongs to OneTrust cookie SDK, not Spotify's design system)
```
### Confidence Breakdown
| Token Group | Confidence | Method |
|---|---|---|
| `--spotify-accent` (green) | **High** | 10 button instances; named as `--brand-primary-cta` in curated set |
| Font size scale (xs–xl) | **High** | Extracted from computed styles across 250+ elements |
| Font weight scale | **High** | Extracted from computed styles across 240+ elements |
| Spacing scale (4–32px) | **High** | Consistent 4px grid confirmed across extracted values |
| `--spotify-radius-full` (9999px) | **High** | Confirmed on secondary button + search input + toggle elements |
| Motion durations | **High** | Extracted from transition properties on 65+ elements |
| `--spotify-ease` cubic-bezier | **High** | Extracted from button transition CSS rules |
| `--spotify-bg-elevated` (#282828) | **Moderate** | From `role_dialog` backgroundColor in computed styles |
| `--spotify-bg-app` / `bg-surface` | **Low–Moderate** | Reconstructed from dark context; true values inferred as #121212/#181818 |
| `--spotify-radius-dialog` (8px) | **Moderate** | From `role_dialog` borderRadius in computed styles |
| Contextual tints (orange/purple/teal) | **Low** | 1 element each; likely dynamic album-art tints, not brand UI colours |
| Body text colour (`#B3B3B3`) | **High** | Confirmed across body, label, nav, modal, checkbox elements |
### Clustering Method
- **Colour clustering:** Grouped by hue family and element role. Dark neutrals clustered by brightness step (18→24→40→58→83→114→128→179→255). Green accent isolated as sole chromatic colour in UI chrome.
- **Spacing clustering:** All spacing values map cleanly to 4px grid (4, 8, 12, 16, 20, 32). The `2px` value in the original curated set was an off-grid value from the OneTrust cookie SDK — corrected to 4px as the grid minimum.
- **Radius clustering:** Clear bimodal distribution — sharp/small (2–8px) for containers, pill (9999px) for interactive elements. No intermediate "rounded" values on brand elements.
- **Font detection:** Two distinct proprietary fonts confirmed via `@font-face` declarations (`SpotifyMixUI`, `SpotifyMixUITitle`). All computed font-family strings confirmed these as primary.
### OneTrust / Cookie SDK Note
> Many of the interactive state styles (`#onetrust-*`, `#ot-sdk-*`) in the extraction belong to **OneTrust's third-party cookie consent SDK**, not Spotify's design system. These include the blue hover colours (`rgb(30, 174, 219)`, `rgb(56, 96, 190)`), green hover (`rgb(44, 100, 21)`), and the `--ot-footer-space` custom property. These values should **NOT** be treated as Spotify brand tokens.More from the gallery
Browse all kits →You may also like

DoorDash
MITClean, accessible food-delivery system with DoorDash's signature red accent, light neutral palette, and system-font typography—built for rapid product development
03
lightecommsaasmobile

Mercedes-Benz
MITLuxe, minimalist design system with sharp black-and-white contrast and serif typography, crafted for premium automotive and lifestyle brands
05
darkminimalboldecomm

Wise
MITClean, accessible financial design system with lime-green accents and Swiss typography, built for fintech products that prioritise clarity and trust
05
lightfintechminimalsaas