Spotify UHD Music Streaming Landing Page
A premium dark-themed music streaming landing page inspired by Spotify — featuring animated gradient mesh backgrounds, glassmorphism playlist cards, CSS-only album art, scroll-pinned feature reveals, animated stat counters, and a phone app mockup with Now Playing UI and parallax tilt.
Open in Lab
MCP
gsap scrolltrigger lenis css vanilla-js canvas
Targets: JS HTML
Code
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap");
/* =============================================
SPOTIFY UHD — Custom Properties
============================================= */
:root {
--sp-black: #121212;
--sp-dark: #191414;
--sp-surface: #1a1a1a;
--sp-surface-hover: #282828;
--sp-border: rgba(255, 255, 255, 0.08);
--sp-green: #1db954;
--sp-green-dark: #1aa34a;
--sp-green-glow: rgba(29, 185, 84, 0.25);
--sp-green-subtle: rgba(29, 185, 84, 0.08);
--sp-purple: #8b5cf6;
--sp-purple-glow: rgba(139, 92, 246, 0.2);
--sp-text: #ffffff;
--sp-muted: #b3b3b3;
--sp-dim: #535353;
--glass-bg: rgba(255, 255, 255, 0.04);
--glass-border: rgba(255, 255, 255, 0.08);
--glass-hover: rgba(255, 255, 255, 0.07);
--radius-sm: 8px;
--radius-md: 16px;
--radius-lg: 24px;
--radius-xl: 36px;
}
/* =============================================
RESET & BASE
============================================= */
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
scroll-behavior: auto;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--sp-black);
color: var(--sp-text);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow-x: hidden;
line-height: 1.6;
}
/* =============================================
CANVAS BACKGROUND
============================================= */
#mesh-bg {
position: fixed;
inset: 0;
z-index: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
/* =============================================
UTILITY CLASSES
============================================= */
.reveal {
opacity: 0;
transform: translateY(30px);
}
section {
position: relative;
z-index: 1;
padding: 120px 1.5rem;
}
.section-container {
max-width: 72rem;
margin: 0 auto;
}
.section-header {
margin-bottom: 3.5rem;
}
.section-title {
font-size: clamp(2.5rem, 7vw, 5rem);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.04em;
margin-bottom: 1.25rem;
background: linear-gradient(180deg, #ffffff 0%, #a0a0a0 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
}
.section-desc {
font-size: 1.15rem;
color: var(--sp-muted);
max-width: 520px;
line-height: 1.6;
}
/* =============================================
TAGS
============================================= */
.tag {
display: inline-block;
padding: 6px 18px;
border-radius: 20px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 1.5rem;
}
.tag-green {
background: var(--sp-green-subtle);
color: var(--sp-green);
border: 1px solid rgba(29, 185, 84, 0.15);
}
/* =============================================
BUTTONS
============================================= */
.btn-primary {
display: inline-block;
padding: 16px 40px;
border-radius: 500px;
font-weight: 700;
font-size: 1rem;
font-family: inherit;
text-decoration: none;
border: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
letter-spacing: 0.01em;
}
.btn-green {
background: var(--sp-green);
color: #000;
}
.btn-green:hover {
background: #1ed760;
transform: scale(1.04);
box-shadow: 0 8px 30px var(--sp-green-glow);
}
.btn-secondary {
display: inline-block;
padding: 16px 40px;
border-radius: 500px;
font-weight: 700;
font-size: 1rem;
font-family: inherit;
text-decoration: none;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
background: transparent;
color: var(--sp-text);
border: 2px solid rgba(255, 255, 255, 0.3);
letter-spacing: 0.01em;
}
.btn-secondary:hover {
border-color: #fff;
transform: scale(1.04);
}
.btn-glow {
box-shadow: 0 0 40px var(--sp-green-glow), 0 0 80px rgba(29, 185, 84, 0.1);
}
/* =============================================
GLASS CARD
============================================= */
.glass-card {
background: var(--glass-bg);
backdrop-filter: blur(24px);
-webkit-backdrop-filter: blur(24px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xl);
padding: 48px;
transition: all 0.5s cubic-bezier(0.23, 1, 0.32, 1);
}
.glass-card:hover {
background: var(--glass-hover);
border-color: rgba(255, 255, 255, 0.14);
}
/* =============================================
SECTION 1: HERO
============================================= */
.hero-section {
height: 100vh;
min-height: 700px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
position: relative;
padding: 0 1.5rem;
}
.hero-content {
text-align: center;
position: relative;
z-index: 2;
}
.hero-logo {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 2.5rem;
opacity: 0;
}
.hero-brand {
font-size: 1.5rem;
font-weight: 800;
letter-spacing: -0.02em;
}
.hero-title {
font-size: clamp(3.5rem, 12vw, 9rem);
font-weight: 900;
line-height: 0.95;
letter-spacing: -0.05em;
background: linear-gradient(180deg, #ffffff 20%, rgba(255, 255, 255, 0.6) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 1.5rem;
opacity: 0;
transform: scale(0.9);
filter: blur(10px);
}
.hero-subtitle {
font-size: clamp(1rem, 2.5vw, 1.35rem);
color: var(--sp-muted);
font-weight: 400;
margin-bottom: 2.5rem;
opacity: 0;
}
.hero-buttons {
display: flex;
align-items: center;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
opacity: 0;
}
.scroll-indicator {
position: absolute;
bottom: 40px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
color: var(--sp-dim);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.3em;
font-weight: 700;
opacity: 0;
z-index: 2;
}
.scroll-line {
width: 1px;
height: 50px;
background: linear-gradient(to bottom, var(--sp-green), transparent);
}
/* =============================================
SECTION 2: PLAYLIST SHOWCASE
============================================= */
.playlist-section {
padding: 140px 1.5rem;
}
.playlist-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 24px;
}
.playlist-card {
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 20px;
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
position: relative;
overflow: hidden;
opacity: 0;
transform: translateY(30px);
}
.playlist-card:hover {
background: var(--glass-hover);
border-color: rgba(255, 255, 255, 0.14);
transform: translateY(-6px);
}
.playlist-card:hover .play-btn {
opacity: 1;
transform: translateY(0);
}
/* CSS-only album art */
.playlist-art {
width: 100%;
aspect-ratio: 1;
border-radius: var(--radius-md);
margin-bottom: 16px;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
}
.art-icon {
position: relative;
z-index: 2;
opacity: 0.5;
}
.art-shape {
position: absolute;
pointer-events: none;
}
/* Card 1 — Chill: teal/blue gradient */
.art-chill {
background: linear-gradient(135deg, #0d4f3c 0%, #1a6b5a 30%, #2d9f90 60%, #1db954 100%);
}
.art-chill .art-shape-circle {
width: 60%;
height: 60%;
border-radius: 50%;
background: radial-gradient(circle, rgba(29, 185, 84, 0.4), transparent 70%);
top: 20%;
left: 20%;
}
.art-chill .art-shape-wave {
width: 120%;
height: 40%;
bottom: -5%;
left: -10%;
background: linear-gradient(180deg, transparent, rgba(0, 0, 0, 0.3));
border-radius: 50% 50% 0 0;
}
/* Card 2 — Hip Hop: orange/red gradient */
.art-hiphop {
background: linear-gradient(135deg, #3d1c02 0%, #8b3a0f 30%, #d4651e 60%, #ff8c42 100%);
}
.art-hiphop .art-shape-diamond {
width: 50%;
height: 50%;
background: rgba(255, 255, 255, 0.08);
top: 25%;
left: 25%;
transform: rotate(45deg);
}
.art-hiphop .art-shape-bars {
width: 100%;
height: 30%;
bottom: 0;
left: 0;
background: repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.06) 0px,
rgba(255, 255, 255, 0.06) 4px,
transparent 4px,
transparent 12px
);
}
/* Card 3 — Electronic: purple/blue gradient */
.art-electronic {
background: linear-gradient(135deg, #1a0533 0%, #4c1d95 30%, #7c3aed 60%, #a78bfa 100%);
}
.art-electronic .art-shape-rings {
width: 70%;
height: 70%;
top: 15%;
left: 15%;
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
box-shadow: inset 0 0 0 12px transparent, inset 0 0 0 14px rgba(255, 255, 255, 0.06), inset 0 0 0
26px transparent, inset 0 0 0 28px rgba(255, 255, 255, 0.04);
}
.art-electronic .art-shape-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.3);
top: 48%;
left: 48%;
}
/* Card 4 — Indie: pink/coral gradient */
.art-indie {
background: linear-gradient(135deg, #4a1942 0%, #893168 30%, #c2527a 60%, #e87ea1 100%);
}
.art-indie .art-shape-triangle {
width: 0;
height: 0;
border-left: 40px solid transparent;
border-right: 40px solid transparent;
border-bottom: 70px solid rgba(255, 255, 255, 0.08);
top: 20%;
left: calc(50% - 40px);
}
.art-indie .art-shape-cross {
width: 60%;
height: 2px;
background: rgba(255, 255, 255, 0.1);
top: 50%;
left: 20%;
}
.art-indie .art-shape-cross::after {
content: "";
position: absolute;
width: 2px;
height: 60px;
background: rgba(255, 255, 255, 0.1);
top: -30px;
left: 50%;
}
/* Card 5 — Jazz: amber/gold gradient */
.art-jazz {
background: linear-gradient(135deg, #3b2506 0%, #78500f 30%, #b8860b 60%, #daa520 100%);
}
.art-jazz .art-shape-arc {
width: 80%;
height: 80%;
top: 10%;
left: 10%;
border: 2px solid rgba(255, 255, 255, 0.1);
border-radius: 50%;
border-bottom-color: transparent;
border-left-color: transparent;
transform: rotate(-20deg);
}
.art-jazz .art-shape-lines {
width: 100%;
height: 100%;
top: 0;
left: 0;
background: repeating-linear-gradient(
45deg,
transparent 0px,
transparent 14px,
rgba(255, 255, 255, 0.03) 14px,
rgba(255, 255, 255, 0.03) 15px
);
}
/* Card 6 — Workout: red/orange gradient */
.art-workout {
background: linear-gradient(135deg, #3b0a0a 0%, #8b1a1a 30%, #dc2626 60%, #f87171 100%);
}
.art-workout .art-shape-bolt {
width: 40px;
height: 60px;
top: 20%;
left: calc(50% - 20px);
background: rgba(255, 255, 255, 0.1);
clip-path: polygon(50% 0%, 0% 50%, 35% 50%, 15% 100%, 100% 40%, 60% 40%);
}
.art-workout .art-shape-stripe {
width: 200%;
height: 30%;
bottom: 10%;
left: -50%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
transform: rotate(-15deg);
}
/* Playlist info */
.playlist-info {
margin-bottom: 4px;
}
.playlist-name {
font-size: 1rem;
font-weight: 700;
margin-bottom: 4px;
color: var(--sp-text);
}
.playlist-meta {
font-size: 0.8rem;
color: var(--sp-dim);
}
.play-btn {
position: absolute;
bottom: 92px;
right: 28px;
width: 48px;
height: 48px;
border-radius: 50%;
background: var(--sp-green);
color: #000;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transform: translateY(8px);
transition: all 0.3s cubic-bezier(0.23, 1, 0.32, 1);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
.play-btn:hover {
transform: scale(1.08) translateY(0);
background: #1ed760;
}
.play-btn svg {
margin-left: 2px;
}
/* =============================================
SECTION 3: HOW IT WORKS (PINNED)
============================================= */
.how-section {
padding: 0;
position: relative;
}
.how-track {
height: 300vh;
position: relative;
}
.how-inner {
position: sticky;
top: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
gap: 80px;
padding: 0 2rem;
max-width: 72rem;
margin: 0 auto;
}
.how-text {
flex: 1;
max-width: 480px;
}
.steps-container {
position: relative;
min-height: 200px;
}
.step-card {
opacity: 0;
transform: translateY(20px);
position: absolute;
top: 0;
left: 0;
width: 100%;
transition: opacity 0.5s ease, transform 0.5s ease;
pointer-events: none;
}
.step-card.active {
opacity: 1;
transform: translateY(0);
position: relative;
pointer-events: auto;
}
.step-number {
font-size: 0.8rem;
font-weight: 700;
color: var(--sp-green);
letter-spacing: 0.1em;
margin-bottom: 0.75rem;
}
.step-title {
font-size: clamp(1.5rem, 3vw, 2.25rem);
font-weight: 800;
margin-bottom: 0.75rem;
letter-spacing: -0.02em;
}
.step-desc {
font-size: 1.05rem;
color: var(--sp-muted);
line-height: 1.6;
}
.step-dots {
display: flex;
gap: 10px;
margin-top: 2rem;
}
.step-dot {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--sp-dim);
transition: all 0.4s ease;
cursor: pointer;
}
.step-dot.active {
background: var(--sp-green);
box-shadow: 0 0 12px var(--sp-green-glow);
transform: scale(1.2);
}
/* How visual — mini phones */
.how-visual {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
max-width: 320px;
height: 500px;
}
.mini-phone {
position: absolute;
width: 220px;
height: 440px;
background: var(--sp-dark);
border-radius: 30px;
border: 2px solid rgba(255, 255, 255, 0.1);
padding: 40px 14px 20px;
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
opacity: 0;
transform: scale(0.9);
transition: opacity 0.6s ease, transform 0.6s ease;
overflow: hidden;
}
.mini-phone.active {
opacity: 1;
transform: scale(1);
}
.mini-phone-notch {
position: absolute;
top: 12px;
left: 50%;
transform: translateX(-50%);
width: 80px;
height: 6px;
border-radius: 3px;
background: rgba(255, 255, 255, 0.15);
}
.mini-phone-screen {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
gap: 10px;
}
/* Mini phone 1: Browse */
.mini-search-bar {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 8px 12px;
font-size: 10px;
color: var(--sp-muted);
}
.mini-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
flex: 1;
}
.mini-tile {
border-radius: 8px;
min-height: 60px;
}
.mini-tile-1 {
background: linear-gradient(135deg, #1db954, #064e2b);
}
.mini-tile-2 {
background: linear-gradient(135deg, #8b5cf6, #3b1f7a);
}
.mini-tile-3 {
background: linear-gradient(135deg, #e74c3c, #6b1d1d);
}
.mini-tile-4 {
background: linear-gradient(135deg, #f39c12, #6b4406);
}
.mini-tile-5 {
background: linear-gradient(135deg, #3498db, #153d5e);
}
.mini-tile-6 {
background: linear-gradient(135deg, #e91e63, #601230);
}
/* Mini phone 2: Play */
.mini-now-playing {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding-top: 20px;
}
.mini-album-art {
width: 140px;
height: 140px;
border-radius: 12px;
background: linear-gradient(135deg, #1db954, #191414, #8b5cf6);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.mini-song-info {
text-align: center;
}
.mini-song-title {
width: 100px;
height: 10px;
background: rgba(255, 255, 255, 0.7);
border-radius: 5px;
margin: 0 auto 6px;
}
.mini-song-artist {
width: 70px;
height: 8px;
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
margin: 0 auto;
}
.mini-progress {
width: 85%;
height: 3px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
overflow: hidden;
}
.mini-progress::after {
content: "";
display: block;
width: 40%;
height: 100%;
background: var(--sp-green);
border-radius: 2px;
}
.mini-controls {
display: flex;
align-items: center;
gap: 20px;
}
.mini-ctrl-dot {
width: 16px;
height: 16px;
border-radius: 50%;
border: 1.5px solid rgba(255, 255, 255, 0.3);
}
.mini-ctrl-play {
width: 36px;
height: 36px;
border-radius: 50%;
background: #fff;
color: #000;
display: flex;
align-items: center;
justify-content: center;
}
.mini-ctrl-play svg {
margin-left: 2px;
}
/* Mini phone 3: Discover */
.mini-discover {
display: flex;
flex-direction: column;
gap: 10px;
padding-top: 10px;
}
.mini-discover-title {
width: 70%;
height: 10px;
background: rgba(255, 255, 255, 0.5);
border-radius: 5px;
}
.mini-discover-row {
display: flex;
gap: 8px;
}
.mini-disc-card {
flex: 1;
height: 80px;
border-radius: 8px;
}
.mini-disc-1 {
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
}
.mini-disc-2 {
background: linear-gradient(135deg, #1db954, #0a8f3f);
}
.mini-disc-3 {
background: linear-gradient(135deg, #a29bfe, #6c5ce7);
}
.mini-disc-4 {
background: linear-gradient(135deg, #fdcb6e, #e17055);
}
/* Equalizer bars */
.equalizer {
display: flex;
align-items: flex-end;
justify-content: center;
gap: 4px;
height: 40px;
margin-top: auto;
padding-bottom: 10px;
}
.eq-bar {
width: 4px;
background: var(--sp-green);
border-radius: 2px;
animation: eq-bounce 1.2s ease-in-out infinite;
}
.eq-bar:nth-child(1) {
height: 16px;
animation-delay: 0s;
}
.eq-bar:nth-child(2) {
height: 28px;
animation-delay: 0.15s;
}
.eq-bar:nth-child(3) {
height: 20px;
animation-delay: 0.3s;
}
.eq-bar:nth-child(4) {
height: 32px;
animation-delay: 0.45s;
}
.eq-bar:nth-child(5) {
height: 12px;
animation-delay: 0.6s;
}
@keyframes eq-bounce {
0%,
100% {
transform: scaleY(0.4);
}
50% {
transform: scaleY(1);
}
}
/* =============================================
SECTION 4: STATS COUNTER
============================================= */
.stats-section {
padding: 160px 1.5rem;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 48px;
text-align: center;
margin-top: 3rem;
}
.stat-item {
padding: 40px 20px;
}
.stat-number {
font-size: clamp(3rem, 8vw, 5rem);
font-weight: 900;
letter-spacing: -0.04em;
background: linear-gradient(180deg, #ffffff 0%, var(--sp-green) 100%);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.5rem;
line-height: 1;
}
.stat-label {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--sp-text);
}
.stat-desc {
font-size: 0.9rem;
color: var(--sp-dim);
max-width: 260px;
margin: 0 auto;
line-height: 1.5;
}
/* =============================================
SECTION 5: PREMIUM FEATURES
============================================= */
.premium-section {
padding: 140px 1.5rem;
}
.premium-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 24px;
margin-top: 3.5rem;
}
.premium-card {
background: var(--glass-bg);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-lg);
padding: 40px 32px;
transition: all 0.4s cubic-bezier(0.23, 1, 0.32, 1);
opacity: 0;
transform: translateY(30px);
}
.premium-card:hover {
background: var(--glass-hover);
border-color: rgba(29, 185, 84, 0.2);
transform: translateY(-6px);
}
.premium-icon {
width: 56px;
height: 56px;
border-radius: var(--radius-md);
background: var(--sp-green-subtle);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 1.5rem;
color: var(--sp-green);
}
.premium-title {
font-size: 1.25rem;
font-weight: 700;
margin-bottom: 0.75rem;
}
.premium-desc {
font-size: 0.95rem;
color: var(--sp-muted);
line-height: 1.6;
}
/* =============================================
SECTION 6: APP SHOWCASE (PHONE MOCKUP)
============================================= */
.app-section {
padding: 140px 1.5rem;
}
.app-layout {
display: flex;
align-items: center;
gap: 80px;
flex-wrap: wrap;
justify-content: center;
}
.app-text {
flex: 1;
min-width: 300px;
max-width: 480px;
}
.app-features {
display: flex;
flex-direction: column;
gap: 14px;
margin-top: 2rem;
}
.app-feature {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.95rem;
color: var(--sp-muted);
}
/* Phone Mockup */
.phone-container {
perspective: 1200px;
}
.phone-mockup {
width: 300px;
height: 620px;
background: var(--sp-dark);
border-radius: var(--radius-xl);
border: 3px solid rgba(255, 255, 255, 0.1);
padding: 50px 18px 24px;
position: relative;
box-shadow: 0 40px 100px rgba(0, 0, 0, 0.6), 0 0 0 1px rgba(255, 255, 255, 0.05), inset 0 1px 0
rgba(255, 255, 255, 0.06);
transform-style: preserve-3d;
transition: transform 0.3s ease;
}
.phone-notch {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
width: 100px;
height: 8px;
border-radius: 4px;
background: rgba(255, 255, 255, 0.12);
}
.phone-screen {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* Now Playing UI */
.np-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
color: var(--sp-muted);
}
.np-playlist-name {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.np-album-art {
width: 100%;
aspect-ratio: 1;
border-radius: var(--radius-md);
overflow: hidden;
margin-bottom: 24px;
box-shadow: 0 16px 48px rgba(0, 0, 0, 0.5);
}
.np-art-inner {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0d3320, #1db954, #064e2b, #8b5cf6, #2d1b69);
background-size: 200% 200%;
animation: art-shift 8s ease-in-out infinite;
position: relative;
overflow: hidden;
}
@keyframes art-shift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.np-art-shape {
position: absolute;
border-radius: 50%;
}
.np-art-shape-1 {
width: 60%;
height: 60%;
top: 20%;
left: 20%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.1), transparent 70%);
}
.np-art-shape-2 {
width: 30%;
height: 30%;
bottom: 10%;
right: 10%;
background: radial-gradient(circle, rgba(29, 185, 84, 0.3), transparent 70%);
}
.np-art-shape-3 {
width: 40%;
height: 40%;
top: 5%;
left: 5%;
background: radial-gradient(circle, rgba(139, 92, 246, 0.2), transparent 70%);
}
.np-song-details {
margin-bottom: 16px;
}
.np-song-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.np-song-title {
font-size: 1.1rem;
font-weight: 700;
margin-bottom: 2px;
}
.np-song-artist {
font-size: 0.85rem;
color: var(--sp-muted);
}
.np-heart {
cursor: pointer;
transition: transform 0.2s ease;
}
.np-heart:hover {
transform: scale(1.2);
}
/* Progress bar */
.np-progress {
margin-bottom: 16px;
}
.np-progress-bar {
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.12);
border-radius: 2px;
position: relative;
margin-bottom: 8px;
}
.np-progress-fill {
width: 38%;
height: 100%;
background: var(--sp-green);
border-radius: 2px;
transition: width 0.3s ease;
}
.np-progress-dot {
position: absolute;
top: 50%;
left: 38%;
transform: translate(-50%, -50%);
width: 12px;
height: 12px;
border-radius: 50%;
background: #fff;
opacity: 0;
transition: opacity 0.2s ease;
}
.np-progress:hover .np-progress-dot {
opacity: 1;
}
.np-times {
display: flex;
justify-content: space-between;
font-size: 10px;
color: var(--sp-dim);
}
/* Controls */
.np-controls {
display: flex;
align-items: center;
justify-content: center;
gap: 24px;
margin-top: auto;
}
.np-ctrl {
background: none;
border: none;
color: var(--sp-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease, transform 0.2s ease;
padding: 0;
}
.np-ctrl:hover {
color: var(--sp-text);
transform: scale(1.1);
}
.np-play {
width: 56px;
height: 56px;
border-radius: 50%;
background: #fff;
color: #000;
}
.np-play:hover {
transform: scale(1.06);
color: #000;
}
.np-play svg {
margin-left: 3px;
}
.np-shuffle,
.np-repeat {
color: var(--sp-dim);
}
/* =============================================
SECTION 7: FINAL CTA
============================================= */
.cta-section {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
min-height: 90vh;
overflow: hidden;
position: relative;
padding: 120px 1.5rem;
}
.cta-content {
position: relative;
z-index: 2;
}
.cta-title {
margin-bottom: 1rem;
}
.cta-subtitle {
font-size: 1.15rem;
color: var(--sp-muted);
margin-bottom: 2.5rem;
}
.cta-buttons {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.cta-rings {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
}
.ring {
position: absolute;
border-radius: 50%;
border: 1px solid rgba(29, 185, 84, 0.06);
}
.ring-1 {
width: 300px;
height: 300px;
}
.ring-2 {
width: 520px;
height: 520px;
}
.ring-3 {
width: 740px;
height: 740px;
}
.cta-orbs {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.cta-orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
opacity: 0.15;
}
.cta-orb-1 {
width: 500px;
height: 500px;
background: var(--sp-green);
top: 10%;
left: -10%;
}
.cta-orb-2 {
width: 400px;
height: 400px;
background: var(--sp-purple);
bottom: 10%;
right: -10%;
}
/* =============================================
REDUCED MOTION
============================================= */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
.hero-title {
opacity: 1;
transform: none;
filter: none;
}
.hero-subtitle,
.hero-logo,
.hero-buttons,
.scroll-indicator {
opacity: 1;
}
.reveal {
opacity: 1;
transform: none;
}
.playlist-card,
.premium-card {
opacity: 1;
transform: none;
}
.step-card {
position: relative;
opacity: 1;
transform: none;
}
.mini-phone {
opacity: 1;
transform: none;
}
.eq-bar {
animation: none;
transform: none;
}
.np-art-inner {
animation: none;
}
}
/* =============================================
RESPONSIVE: 1024px
============================================= */
@media (max-width: 1024px) {
.how-inner {
gap: 40px;
padding: 0 1.5rem;
}
.app-layout {
gap: 48px;
}
.phone-mockup {
width: 270px;
height: 560px;
}
.premium-grid {
grid-template-columns: repeat(2, 1fr);
}
}
/* =============================================
RESPONSIVE: 768px
============================================= */
@media (max-width: 768px) {
section {
padding: 80px 1rem;
}
.how-inner {
flex-direction: column;
gap: 40px;
text-align: center;
padding: 0 1rem;
}
.how-text {
max-width: 100%;
}
.how-visual {
max-width: 200px;
height: 360px;
}
.mini-phone {
width: 170px;
height: 340px;
padding: 32px 10px 16px;
border-radius: 22px;
}
.step-dots {
justify-content: center;
}
.app-layout {
flex-direction: column;
gap: 40px;
text-align: center;
}
.app-text {
min-width: 0;
}
.app-features {
align-items: center;
}
.section-desc {
margin-left: auto;
margin-right: auto;
}
.phone-mockup {
width: 260px;
height: 540px;
padding: 44px 16px 20px;
}
.playlist-grid {
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 16px;
}
.playlist-card {
padding: 14px;
}
.play-btn {
width: 40px;
height: 40px;
bottom: 76px;
right: 22px;
}
.premium-grid {
grid-template-columns: 1fr;
}
.premium-card {
padding: 28px 24px;
}
.glass-card {
padding: 28px;
border-radius: var(--radius-lg);
}
.stats-grid {
gap: 24px;
}
.stat-item {
padding: 24px 12px;
}
.ring-1 {
width: 200px;
height: 200px;
}
.ring-2 {
width: 360px;
height: 360px;
}
.ring-3 {
width: 520px;
height: 520px;
}
}
/* =============================================
RESPONSIVE: 480px
============================================= */
@media (max-width: 480px) {
.hero-title {
font-size: clamp(2.8rem, 14vw, 5rem);
}
.hero-buttons {
flex-direction: column;
width: 100%;
padding: 0 1rem;
}
.hero-buttons .btn-primary,
.hero-buttons .btn-secondary {
width: 100%;
text-align: center;
}
.section-title {
font-size: clamp(2rem, 9vw, 3.5rem);
}
.playlist-grid {
grid-template-columns: repeat(2, 1fr);
gap: 12px;
}
.playlist-card {
padding: 10px;
border-radius: var(--radius-md);
}
.playlist-name {
font-size: 0.85rem;
}
.play-btn {
width: 36px;
height: 36px;
bottom: 64px;
right: 16px;
}
.phone-mockup {
width: 240px;
height: 500px;
padding: 40px 14px 18px;
}
.np-album-art {
margin-bottom: 16px;
}
.np-controls {
gap: 16px;
}
.np-play {
width: 48px;
height: 48px;
}
.stat-number {
font-size: clamp(2.5rem, 12vw, 4rem);
}
.how-track {
height: 250vh;
}
.how-visual {
max-width: 160px;
height: 300px;
}
.mini-phone {
width: 140px;
height: 280px;
padding: 28px 8px 12px;
border-radius: 18px;
}
.mini-album-art {
width: 100px;
height: 100px;
}
.cta-section {
min-height: 70vh;
}
}// =============================================
// 1. Canvas gradient mesh — green/purple orbs
// =============================================
(function () {
const canvas = document.getElementById("mesh-bg");
if (!canvas) return;
const ctx = canvas.getContext("2d");
let w, h;
function resize() {
w = canvas.width = window.innerWidth;
h = canvas.height = window.innerHeight;
}
resize();
window.addEventListener("resize", resize);
const orbs = [
{ x: 0.2, y: 0.3, r: 0.45, color: [29, 185, 84], speed: 0.0003, phase: 0 },
{ x: 0.75, y: 0.6, r: 0.4, color: [139, 92, 246], speed: 0.00025, phase: 2 },
{ x: 0.5, y: 0.8, r: 0.35, color: [29, 185, 84], speed: 0.00035, phase: 4 },
{ x: 0.85, y: 0.2, r: 0.3, color: [88, 60, 180], speed: 0.0002, phase: 1 },
{ x: 0.15, y: 0.7, r: 0.25, color: [16, 130, 60], speed: 0.00028, phase: 3 },
];
function draw(t) {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "#121212";
ctx.fillRect(0, 0, w, h);
for (const orb of orbs) {
const ox = (orb.x + Math.sin(t * orb.speed + orb.phase) * 0.08) * w;
const oy = (orb.y + Math.cos(t * orb.speed * 0.7 + orb.phase) * 0.06) * h;
const or = orb.r * Math.min(w, h);
const grad = ctx.createRadialGradient(ox, oy, 0, ox, oy, or);
grad.addColorStop(0, `rgba(${orb.color[0]}, ${orb.color[1]}, ${orb.color[2]}, 0.12)`);
grad.addColorStop(0.5, `rgba(${orb.color[0]}, ${orb.color[1]}, ${orb.color[2]}, 0.04)`);
grad.addColorStop(1, `rgba(${orb.color[0]}, ${orb.color[1]}, ${orb.color[2]}, 0)`);
ctx.fillStyle = grad;
ctx.beginPath();
ctx.arc(ox, oy, or, 0, Math.PI * 2);
ctx.fill();
}
// Vignette overlay
const vignette = ctx.createRadialGradient(w / 2, h / 2, w * 0.2, w / 2, h / 2, w * 0.8);
vignette.addColorStop(0, "rgba(18, 18, 18, 0)");
vignette.addColorStop(1, "rgba(18, 18, 18, 0.6)");
ctx.fillStyle = vignette;
ctx.fillRect(0, 0, w, h);
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
})();
// =============================================
// 2. Lenis + GSAP setup
// =============================================
const lenis = new Lenis({
duration: 1.2,
easing: (t) => Math.min(1, 1.001 - Math.pow(2, -10 * t)),
smoothWheel: true,
autoRaf: false,
});
gsap.registerPlugin(ScrollTrigger);
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => {
lenis.raf(time * 1000);
});
gsap.ticker.lagSmoothing(0);
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const dur = (d) => (reduced ? 0 : d);
// =============================================
// 3. Hero entrance timeline
// =============================================
const heroTl = gsap.timeline({ defaults: { ease: "power4.out" } });
heroTl
.to(".hero-logo", {
opacity: 1,
y: 0,
duration: dur(1.2),
delay: dur(0.3),
})
.to(
".hero-title",
{
opacity: 1,
scale: 1,
filter: "blur(0px)",
duration: dur(2),
},
"-=0.8"
)
.to(
".hero-subtitle",
{
opacity: 1,
y: 0,
duration: dur(1.2),
},
"-=1.5"
)
.to(
".hero-buttons",
{
opacity: 1,
y: 0,
duration: dur(1),
},
"-=0.8"
)
.to(
".scroll-indicator",
{
opacity: 1,
y: 0,
duration: dur(0.8),
},
"-=0.5"
);
// =============================================
// 4. Hero parallax on scroll
// =============================================
gsap.to(".hero-content", {
scale: 1.15,
opacity: 0,
filter: "blur(16px)",
scrollTrigger: {
trigger: ".hero-section",
start: "top top",
end: "80% top",
scrub: true,
},
});
gsap.to(".scroll-indicator", {
opacity: 0,
scrollTrigger: {
trigger: ".hero-section",
start: "15% top",
end: "30% top",
scrub: true,
},
});
// =============================================
// 5. Generic .reveal scroll animation
// =============================================
const revealElements = gsap.utils.toArray(".reveal");
revealElements.forEach((el) => {
gsap.fromTo(
el,
{ opacity: 0, y: 40 },
{
opacity: 1,
y: 0,
duration: dur(1.2),
ease: "power3.out",
scrollTrigger: {
trigger: el,
start: "top 85%",
toggleActions: "play none none none",
},
}
);
});
// =============================================
// 6. Playlist cards stagger reveal
// =============================================
const playlistGrid = document.querySelector(".playlist-grid");
if (playlistGrid) {
gsap.fromTo(
".playlist-card",
{ opacity: 0, y: 40, scale: 0.95 },
{
opacity: 1,
y: 0,
scale: 1,
duration: dur(0.8),
stagger: 0.1,
ease: "power3.out",
scrollTrigger: {
trigger: playlistGrid,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
// =============================================
// 7. Pinned scroll section — How It Works
// =============================================
const howTrack = document.querySelector(".how-track");
const stepCards = document.querySelectorAll(".step-card");
const stepDots = document.querySelectorAll(".step-dot");
const miniPhones = document.querySelectorAll(".mini-phone");
function setActiveStep(index) {
stepCards.forEach((card, i) => {
card.classList.toggle("active", i === index);
});
stepDots.forEach((dot, i) => {
dot.classList.toggle("active", i === index);
});
miniPhones.forEach((phone, i) => {
phone.classList.toggle("active", i === index);
});
}
if (howTrack && stepCards.length > 0) {
ScrollTrigger.create({
trigger: howTrack,
start: "top top",
end: "bottom bottom",
scrub: true,
onUpdate: (self) => {
const progress = self.progress;
const totalSteps = stepCards.length;
const stepIndex = Math.min(totalSteps - 1, Math.floor(progress * totalSteps));
setActiveStep(stepIndex);
},
});
}
// =============================================
// 8. Stat counter count-up
// =============================================
const statNumbers = document.querySelectorAll(".stat-number[data-target]");
statNumbers.forEach((el) => {
const target = parseInt(el.dataset.target, 10);
const suffix = el.dataset.suffix || "";
if (isNaN(target) || target <= 0) return;
el.textContent = "0" + suffix;
ScrollTrigger.create({
trigger: el,
start: "top 85%",
once: true,
onEnter: () => {
gsap.to(
{ val: 0 },
{
val: target,
duration: dur(2.5),
ease: "power2.out",
onUpdate: function () {
el.textContent = Math.round(this.targets()[0].val) + suffix;
},
}
);
},
});
});
// =============================================
// 9. Premium features stagger reveal
// =============================================
const premiumGrid = document.querySelector(".premium-grid");
if (premiumGrid) {
gsap.fromTo(
".premium-card",
{ opacity: 0, y: 40, scale: 0.95 },
{
opacity: 1,
y: 0,
scale: 1,
duration: dur(0.8),
stagger: 0.15,
ease: "power3.out",
scrollTrigger: {
trigger: premiumGrid,
start: "top 80%",
toggleActions: "play none none none",
},
}
);
}
// =============================================
// 10. Phone mockup mouse-tilt parallax
// =============================================
const phoneMockup = document.querySelector(".phone-mockup");
if (phoneMockup && !reduced) {
const phoneContainer = document.querySelector(".phone-container");
window.addEventListener("mousemove", (e) => {
if (!phoneContainer) return;
const rect = phoneContainer.getBoundingClientRect();
// Only apply tilt if the phone is visible in the viewport
if (rect.top > window.innerHeight || rect.bottom < 0) return;
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const x = (e.clientX - centerX) / (rect.width / 2);
const y = (e.clientY - centerY) / (rect.height / 2);
// Clamp to prevent extreme angles
const clampedX = Math.max(-1, Math.min(1, x));
const clampedY = Math.max(-1, Math.min(1, y));
gsap.to(phoneMockup, {
rotateY: clampedX * 12,
rotateX: -clampedY * 8,
duration: 0.6,
ease: "power2.out",
});
});
// Scroll parallax for phone
gsap.to(phoneMockup, {
y: -30,
scrollTrigger: {
trigger: ".app-section",
start: "top bottom",
end: "bottom top",
scrub: true,
},
});
}
// =============================================
// 11. CTA rings expansion
// =============================================
gsap.utils.toArray(".ring").forEach((ring, i) => {
gsap.fromTo(
ring,
{ scale: 0.4, opacity: 0 },
{
scale: 1,
opacity: 1,
duration: dur(1.8),
delay: i * 0.2,
ease: "power3.out",
scrollTrigger: {
trigger: ".cta-section",
start: "top 80%",
toggleActions: "play none none none",
},
}
);
});
// CTA orb parallax
gsap.to(".cta-orb-1", {
y: -80,
x: 40,
scrollTrigger: {
trigger: ".cta-section",
start: "top bottom",
end: "bottom top",
scrub: 1,
},
});
gsap.to(".cta-orb-2", {
y: 60,
x: -40,
scrollTrigger: {
trigger: ".cta-section",
start: "top bottom",
end: "bottom top",
scrub: 1,
},
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Spotify | Music for everyone</title>
<link rel="stylesheet" href="./style.css">
</head>
<body>
<!-- Canvas gradient mesh background -->
<canvas id="mesh-bg"></canvas>
<!-- Main Content -->
<main id="main">
<!-- Section 1: Hero -->
<section class="hero-section">
<div class="hero-content">
<div class="hero-logo">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="12" fill="#1DB954"/>
<path d="M17.5 15.2c-.2 0-.3-.1-.5-.2-2-1.2-4.5-1.8-7.3-1.5-.3.1-.6.1-.8.1-.3 0-.5-.2-.5-.5s.2-.5.5-.6c3.1-.4 5.9.2 8.1 1.6.2.1.3.3.3.6 0 .3-.1.5-.4.5h.1zm1-2.5c-.2 0-.4-.1-.5-.2-2.3-1.4-5.8-2.2-8.5-1.5-.3.1-.5.1-.7.1-.4 0-.6-.3-.6-.6 0-.4.2-.6.5-.7 3.2-.8 7 0 9.6 1.7.3.2.4.4.4.7 0 .3-.2.5-.5.6l.3-.1zm1.2-2.9c-.2 0-.3-.1-.5-.2C16.4 8 11.8 7.5 8.5 8.3c-.2.1-.4.1-.6.1-.4 0-.7-.3-.7-.7 0-.4.2-.7.6-.8C11.5 6 16.5 6.5 19.9 8.4c.3.2.4.4.4.8 0 .4-.3.7-.7.7l.1-.1z" fill="white"/>
</svg>
<span class="hero-brand">Spotify</span>
</div>
<h1 class="hero-title">Music for<br/>everyone.</h1>
<p class="hero-subtitle">Millions of songs. No credit card needed.</p>
<div class="hero-buttons">
<button class="btn-primary btn-green">Get Spotify Free</button>
<button class="btn-secondary">Get Premium</button>
</div>
</div>
<div class="scroll-indicator">
<span>Scroll</span>
<div class="scroll-line"></div>
</div>
</section>
<!-- Section 2: Playlist Showcase -->
<section class="playlist-section">
<div class="section-container">
<div class="reveal section-header">
<span class="tag tag-green">Your Library</span>
<h2 class="section-title">Playlists made<br/>for you.</h2>
<p class="section-desc">Discover curated playlists that match every mood, moment, and genre.</p>
</div>
<div class="playlist-grid">
<!-- Card 1 -->
<div class="playlist-card">
<div class="playlist-art art-chill">
<div class="art-shape art-shape-circle"></div>
<div class="art-shape art-shape-wave"></div>
<svg class="art-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1.5">
<path d="M9 18V5l12-2v13"/>
<circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>
</svg>
</div>
<div class="playlist-info">
<h3 class="playlist-name">Chill Vibes</h3>
<p class="playlist-meta">128 songs</p>
</div>
<button class="play-btn" aria-label="Play Chill Vibes">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</button>
</div>
<!-- Card 2 -->
<div class="playlist-card">
<div class="playlist-art art-hiphop">
<div class="art-shape art-shape-diamond"></div>
<div class="art-shape art-shape-bars"></div>
<svg class="art-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1.5">
<circle cx="5.5" cy="17.5" r="2.5"/><circle cx="17.5" cy="15.5" r="2.5"/>
<path d="M8 17.5V5l12-2v12.5"/>
</svg>
</div>
<div class="playlist-info">
<h3 class="playlist-name">Hip Hop Beats</h3>
<p class="playlist-meta">256 songs</p>
</div>
<button class="play-btn" aria-label="Play Hip Hop Beats">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</button>
</div>
<!-- Card 3 -->
<div class="playlist-card">
<div class="playlist-art art-electronic">
<div class="art-shape art-shape-rings"></div>
<div class="art-shape art-shape-dot"></div>
<svg class="art-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1.5">
<path d="M3 18v-6a9 9 0 0 1 18 0v6"/>
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3zM3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z"/>
</svg>
</div>
<div class="playlist-info">
<h3 class="playlist-name">Electronic Pulse</h3>
<p class="playlist-meta">192 songs</p>
</div>
<button class="play-btn" aria-label="Play Electronic Pulse">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</button>
</div>
<!-- Card 4 -->
<div class="playlist-card">
<div class="playlist-art art-indie">
<div class="art-shape art-shape-triangle"></div>
<div class="art-shape art-shape-cross"></div>
<svg class="art-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1.5">
<path d="M12 3v18M5 8l7-5 7 5M5 16l7 5 7-5"/>
</svg>
</div>
<div class="playlist-info">
<h3 class="playlist-name">Indie Discovery</h3>
<p class="playlist-meta">84 songs</p>
</div>
<button class="play-btn" aria-label="Play Indie Discovery">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</button>
</div>
<!-- Card 5 -->
<div class="playlist-card">
<div class="playlist-art art-jazz">
<div class="art-shape art-shape-arc"></div>
<div class="art-shape art-shape-lines"></div>
<svg class="art-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1.5">
<path d="M2 10s3-3 5-3 4 6 7 6 5-3 5-3"/>
<path d="M2 16s3-3 5-3 4 6 7 6 5-3 5-3"/>
</svg>
</div>
<div class="playlist-info">
<h3 class="playlist-name">Jazz Lounge</h3>
<p class="playlist-meta">67 songs</p>
</div>
<button class="play-btn" aria-label="Play Jazz Lounge">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</button>
</div>
<!-- Card 6 -->
<div class="playlist-card">
<div class="playlist-art art-workout">
<div class="art-shape art-shape-bolt"></div>
<div class="art-shape art-shape-stripe"></div>
<svg class="art-icon" width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.6)" stroke-width="1.5">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
</svg>
</div>
<div class="playlist-info">
<h3 class="playlist-name">Workout Power</h3>
<p class="playlist-meta">312 songs</p>
</div>
<button class="play-btn" aria-label="Play Workout Power">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</button>
</div>
</div>
</div>
</section>
<!-- Section 3: How Spotify Works (Pinned) -->
<section class="how-section">
<div class="how-track">
<div class="how-inner">
<div class="how-text">
<div class="reveal">
<span class="tag tag-green">How It Works</span>
<h2 class="section-title">Three steps to<br/>your soundtrack.</h2>
</div>
<div class="steps-container">
<div class="step-card step-card-1 active">
<div class="step-number">01</div>
<h3 class="step-title">Browse</h3>
<p class="step-desc">Explore millions of songs, playlists, and podcasts. Search by mood, genre, or artist.</p>
</div>
<div class="step-card step-card-2">
<div class="step-number">02</div>
<h3 class="step-title">Play</h3>
<p class="step-desc">Hit play and let the music flow. Create your own playlists or let us do it for you.</p>
</div>
<div class="step-card step-card-3">
<div class="step-number">03</div>
<h3 class="step-title">Discover</h3>
<p class="step-desc">Our algorithm learns your taste and surfaces new tracks you'll love every week.</p>
</div>
</div>
<div class="step-dots">
<span class="step-dot active" data-step="0"></span>
<span class="step-dot" data-step="1"></span>
<span class="step-dot" data-step="2"></span>
</div>
</div>
<div class="how-visual">
<!-- Mini phone: Browse -->
<div class="mini-phone mini-phone-1 active">
<div class="mini-phone-notch"></div>
<div class="mini-phone-screen">
<div class="mini-search-bar">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>
<span>Search artists, songs...</span>
</div>
<div class="mini-grid">
<div class="mini-tile mini-tile-1"></div>
<div class="mini-tile mini-tile-2"></div>
<div class="mini-tile mini-tile-3"></div>
<div class="mini-tile mini-tile-4"></div>
<div class="mini-tile mini-tile-5"></div>
<div class="mini-tile mini-tile-6"></div>
</div>
</div>
</div>
<!-- Mini phone: Play -->
<div class="mini-phone mini-phone-2">
<div class="mini-phone-notch"></div>
<div class="mini-phone-screen">
<div class="mini-now-playing">
<div class="mini-album-art"></div>
<div class="mini-song-info">
<div class="mini-song-title"></div>
<div class="mini-song-artist"></div>
</div>
<div class="mini-progress"></div>
<div class="mini-controls">
<div class="mini-ctrl-dot"></div>
<div class="mini-ctrl-play">
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</div>
<div class="mini-ctrl-dot"></div>
</div>
</div>
</div>
</div>
<!-- Mini phone: Discover -->
<div class="mini-phone mini-phone-3">
<div class="mini-phone-notch"></div>
<div class="mini-phone-screen">
<div class="mini-discover">
<div class="mini-discover-title"></div>
<div class="mini-discover-row">
<div class="mini-disc-card mini-disc-1"></div>
<div class="mini-disc-card mini-disc-2"></div>
</div>
<div class="mini-discover-row">
<div class="mini-disc-card mini-disc-3"></div>
<div class="mini-disc-card mini-disc-4"></div>
</div>
<div class="equalizer">
<span class="eq-bar"></span>
<span class="eq-bar"></span>
<span class="eq-bar"></span>
<span class="eq-bar"></span>
<span class="eq-bar"></span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Section 4: Stats Counter -->
<section class="stats-section">
<div class="section-container">
<div class="reveal section-header" style="text-align:center;">
<h2 class="section-title">The numbers<br/>speak for themselves.</h2>
</div>
<div class="stats-grid">
<div class="stat-item reveal">
<div class="stat-number" data-target="100" data-suffix="M+">0</div>
<h3 class="stat-label">Songs</h3>
<p class="stat-desc">The world's largest music library at your fingertips.</p>
</div>
<div class="stat-item reveal">
<div class="stat-number" data-target="500" data-suffix="M+">0</div>
<h3 class="stat-label">Active Users</h3>
<p class="stat-desc">Join the community of music lovers worldwide.</p>
</div>
<div class="stat-item reveal">
<div class="stat-number" data-target="5" data-suffix="B+">0</div>
<h3 class="stat-label">Playlists</h3>
<p class="stat-desc">Curated by humans and algorithms, just for you.</p>
</div>
</div>
</div>
</section>
<!-- Section 5: Premium Features -->
<section class="premium-section">
<div class="section-container">
<div class="reveal section-header" style="text-align:center;">
<span class="tag tag-green">Premium</span>
<h2 class="section-title">Upgrade your<br/>listening.</h2>
<p class="section-desc" style="margin:0 auto;">Get the most out of Spotify with Premium features designed for serious music lovers.</p>
</div>
<div class="premium-grid">
<div class="premium-card">
<div class="premium-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="1" y1="1" x2="23" y2="23"/>
<path d="M16.72 11.06A10.94 10.94 0 0 1 19 12.55"/>
<path d="M5 12.55a10.94 10.94 0 0 1 5.17-2.39"/>
<path d="M10.71 5.05A16 16 0 0 1 22.56 9"/>
<path d="M1.42 9a15.91 15.91 0 0 1 4.7-2.88"/>
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
<line x1="12" y1="20" x2="12.01" y2="20"/>
</svg>
</div>
<h3 class="premium-title">Ad-Free Music</h3>
<p class="premium-desc">Listen without interruptions. No ads, ever. Just pure music from start to finish.</p>
</div>
<div class="premium-card">
<div class="premium-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
<polyline points="7 10 12 15 17 10"/>
<line x1="12" y1="15" x2="12" y2="3"/>
</svg>
</div>
<h3 class="premium-title">Download Music</h3>
<p class="premium-desc">Save your favorite tracks offline. Listen anywhere, even without an internet connection.</p>
</div>
<div class="premium-card">
<div class="premium-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
<path d="M19.07 4.93a10 10 0 0 1 0 14.14"/>
<path d="M15.54 8.46a5 5 0 0 1 0 7.07"/>
</svg>
</div>
<h3 class="premium-title">High Quality Audio</h3>
<p class="premium-desc">Crystal clear sound up to 320kbps. Hear every detail the artist intended.</p>
</div>
<div class="premium-card">
<div class="premium-icon">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75"/>
</svg>
</div>
<h3 class="premium-title">Group Sessions</h3>
<p class="premium-desc">Listen together with friends in real time. Share control and enjoy the same music, in sync.</p>
</div>
</div>
</div>
</section>
<!-- Section 6: App Showcase (Phone Mockup) -->
<section class="app-section">
<div class="section-container">
<div class="app-layout">
<div class="app-text reveal">
<span class="tag tag-green">Now Playing</span>
<h2 class="section-title">Your music,<br/>beautifully presented.</h2>
<p class="section-desc">An immersive listening experience designed to keep you in the zone.</p>
<div class="app-features">
<div class="app-feature">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--sp-green)" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span>Lyrics in real time</span>
</div>
<div class="app-feature">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--sp-green)" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span>Queue management</span>
</div>
<div class="app-feature">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="var(--sp-green)" stroke-width="2">
<polyline points="20 6 9 17 4 12"/>
</svg>
<span>Crossfade playback</span>
</div>
</div>
</div>
<div class="phone-container">
<div class="phone-mockup">
<div class="phone-notch"></div>
<div class="phone-screen">
<!-- Now Playing UI -->
<div class="np-header">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 12 15 18 9"/>
</svg>
<span class="np-playlist-name">Chill Vibes</span>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/>
</svg>
</div>
<div class="np-album-art">
<div class="np-art-inner">
<div class="np-art-shape np-art-shape-1"></div>
<div class="np-art-shape np-art-shape-2"></div>
<div class="np-art-shape np-art-shape-3"></div>
</div>
</div>
<div class="np-song-details">
<div class="np-song-row">
<div>
<div class="np-song-title">Midnight Dreams</div>
<div class="np-song-artist">Luna Echo</div>
</div>
<svg class="np-heart" width="20" height="20" viewBox="0 0 24 24" fill="var(--sp-green)" stroke="var(--sp-green)" stroke-width="2">
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
</svg>
</div>
</div>
<div class="np-progress">
<div class="np-progress-bar">
<div class="np-progress-fill"></div>
<div class="np-progress-dot"></div>
</div>
<div class="np-times">
<span>1:24</span>
<span>3:47</span>
</div>
</div>
<div class="np-controls">
<button class="np-ctrl np-shuffle" aria-label="Shuffle">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="16 3 21 3 21 8"/>
<line x1="4" y1="20" x2="21" y2="3"/>
<polyline points="21 16 21 21 16 21"/>
<line x1="15" y1="15" x2="21" y2="21"/>
<line x1="4" y1="4" x2="9" y2="9"/>
</svg>
</button>
<button class="np-ctrl np-prev" aria-label="Previous">
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<polygon points="19 20 9 12 19 4 19 20"/>
<line x1="5" y1="19" x2="5" y2="5" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button class="np-ctrl np-play" aria-label="Play">
<svg width="28" height="28" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 3 19 12 5 21 5 3"/>
</svg>
</button>
<button class="np-ctrl np-next" aria-label="Next">
<svg width="22" height="22" viewBox="0 0 24 24" fill="currentColor">
<polygon points="5 4 15 12 5 20 5 4"/>
<line x1="19" y1="5" x2="19" y2="19" stroke="currentColor" stroke-width="2"/>
</svg>
</button>
<button class="np-ctrl np-repeat" aria-label="Repeat">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="17 1 21 5 17 9"/>
<path d="M3 11V9a4 4 0 0 1 4-4h14"/>
<polyline points="7 23 3 19 7 15"/>
<path d="M21 13v2a4 4 0 0 1-4 4H3"/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Section 7: Final CTA -->
<section class="cta-section">
<div class="cta-orbs">
<div class="cta-orb cta-orb-1"></div>
<div class="cta-orb cta-orb-2"></div>
</div>
<div class="cta-rings">
<div class="ring ring-1"></div>
<div class="ring ring-2"></div>
<div class="ring ring-3"></div>
</div>
<div class="cta-content">
<h2 class="reveal section-title cta-title">Ready to start<br/>listening?</h2>
<p class="reveal cta-subtitle">Millions of songs and podcasts. No credit card needed.</p>
<div class="reveal cta-buttons">
<button class="btn-primary btn-green btn-glow">Get Spotify Free</button>
</div>
</div>
</section>
</main>
<!-- Dependencies -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lenis@1.1.18/dist/lenis.min.js"></script>
<script src="./script.js"></script>
</body>
</html>Spotify UHD Music Streaming Landing Page
A premium, ultra-high-definition landing page inspired by Spotify’s dark design language with vibrant green accents.
Highlights
- Animated gradient mesh — Canvas 2D background with drifting green/purple orbs
- Glassmorphism playlist cards — Frosted-glass cards with CSS-only album art
- Scroll-pinned “How It Works” — 3-step pinned section with GSAP ScrollTrigger
- Animated stat counters — Songs, users, and playlists
- CSS-only phone mockup — Now Playing UI with progress bar and controls
- Smooth scroll — Lenis-powered throughout
Sections
- Hero — Canvas gradient mesh background with green/purple orbs, massive headline, dual CTA buttons, scroll indicator
- Playlist Showcase — Horizontal grid of glassmorphism cards with CSS-only album art, playlist names, play buttons
- How Spotify Works — Pinned 300vh section with 3-step walkthrough (Browse, Play, Discover) and mini phone visuals
- Stats Counter — Animated numbers for songs, users, and playlists with scroll-triggered count-up
- Premium Features — Glass card layout for ad-free, downloads, high quality audio, and group sessions
- App Showcase — CSS-only phone mockup with Now Playing UI, album art gradient, progress bar, and mouse-tilt parallax
- Final CTA — Expanding green rings with gradient orbs and signup prompt
Tech Stack
- GSAP & ScrollTrigger — High-performance scroll-driven animations and pinned sections
- Lenis — Smooth, inertial scrolling
- Canvas 2D — Animated gradient mesh background with drifting orbs
- Vanilla CSS3 — Glassmorphism, custom properties, gradient album art, phone mockup
- Responsive — Breakpoints at 1024px, 768px, and 480px