Pages Hard
Product Landing Page
Apple-style wireless earbuds product page with 3D hero, scroll-pinned feature reveals, and cinematic text choreography.
Open in Lab
MCP
three.js gsap lenis splittext scrolltrigger
Targets: JS HTML
Code
:root {
--page-bg: #0a0a0f;
--page-surface: #141418;
--page-text: #f5f5f7;
--page-muted: #86868b;
--page-accent: #2997ff;
--page-accent-alt: #bf5af2;
--page-gold: #ffd60a;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
background: var(--page-bg);
color: var(--page-text);
font-family: "SF Pro Display", "Inter", system-ui, -apple-system, sans-serif;
line-height: 1.6;
overflow-x: hidden;
}
/* โโ Three.js canvas โโ */
#canvas-container {
position: fixed;
inset: 0;
width: 100%;
height: 100%;
z-index: 0;
pointer-events: none;
}
#canvas-container canvas {
width: 100%;
height: 100%;
}
/* โโ Sections โโ */
.section {
position: relative;
z-index: 1;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
}
/* โโ Hero โโ */
.hero-section {
text-align: center;
}
.hero-content {
position: relative;
z-index: 2;
}
.hero-title {
font-size: clamp(3rem, 10vw, 8rem);
font-weight: 700;
letter-spacing: -0.03em;
line-height: 1;
margin-bottom: 0.5rem;
perspective: 800px;
}
.hero-tagline {
font-size: clamp(1rem, 3vw, 1.5rem);
color: var(--page-muted);
font-weight: 400;
letter-spacing: 0.02em;
opacity: 0;
}
.scroll-indicator {
margin-top: 4rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
color: var(--page-muted);
font-size: 0.7rem;
letter-spacing: 0.1em;
text-transform: uppercase;
opacity: 0;
}
.scroll-indicator svg {
animation: scroll-bounce 2s ease-in-out infinite;
}
@keyframes scroll-bounce {
0%,
100% {
transform: translateY(0);
opacity: 1;
}
50% {
transform: translateY(6px);
opacity: 0.4;
}
}
.reduced-motion .scroll-indicator svg {
animation: none;
}
/* โโ Feature Track (pinned) โโ */
.feature-track {
position: relative;
z-index: 1;
height: 300vh;
}
.feature-container {
position: sticky;
top: 0;
height: 100vh;
display: flex;
align-items: center;
justify-content: flex-end;
padding-right: 8%;
}
.feature-content {
max-width: 420px;
position: relative;
}
/* Step dots */
.step-dots {
display: flex;
gap: 0.5rem;
margin-bottom: 2rem;
}
.step-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transition: background 0.4s ease, transform 0.4s ease;
}
.step-dot.active {
background: var(--page-accent);
transform: scale(1.3);
}
/* Feature cards */
.feature-card {
position: absolute;
top: 0;
left: 0;
right: 0;
opacity: 0;
transform: translateY(30px);
pointer-events: none;
transition: opacity 0.5s ease, transform 0.5s ease;
}
.feature-card.active {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
position: relative;
}
.reduced-motion .feature-card {
transition: none;
}
.feature-icon {
width: 48px;
height: 48px;
color: var(--page-accent);
margin-bottom: 1.5rem;
}
.feature-label {
font-size: 0.7rem;
color: var(--page-accent);
text-transform: uppercase;
letter-spacing: 0.15em;
font-weight: 600;
}
.feature-heading {
font-size: clamp(1.5rem, 4vw, 2.5rem);
font-weight: 700;
letter-spacing: -0.02em;
margin: 0.5rem 0 1rem;
line-height: 1.15;
}
.feature-desc {
font-size: 0.95rem;
color: var(--page-muted);
line-height: 1.7;
margin-bottom: 1.5rem;
}
.feature-stat {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.stat-value {
font-size: 3rem;
font-weight: 700;
color: var(--page-accent);
line-height: 1;
font-variant-numeric: tabular-nums;
}
.stat-unit {
font-size: 0.8rem;
color: var(--page-muted);
text-transform: uppercase;
letter-spacing: 0.04em;
}
/* โโ Specs Grid โโ */
.specs-section {
padding: 8rem 2rem;
}
.specs-heading {
font-size: clamp(1.8rem, 5vw, 3rem);
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 4rem;
text-align: center;
}
.specs-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1.5rem;
max-width: 800px;
width: 100%;
}
.spec-card {
background: var(--page-surface);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 16px;
padding: 2rem 1.5rem;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
opacity: 0;
transform: translateY(30px) scale(0.95);
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.spec-card:hover {
border-color: rgba(41, 151, 255, 0.2);
box-shadow: 0 8px 32px rgba(41, 151, 255, 0.08);
}
.reduced-motion .spec-card {
opacity: 1;
transform: none;
}
.spec-icon {
width: 24px;
height: 24px;
color: var(--page-accent);
}
.spec-value {
font-size: 1.5rem;
font-weight: 700;
color: var(--page-text);
}
.spec-label {
font-size: 0.7rem;
color: var(--page-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* โโ CTA Section โโ */
.cta-section {
position: relative;
overflow: hidden;
}
.cta-content {
position: relative;
z-index: 2;
text-align: center;
}
.cta-price {
font-size: clamp(3rem, 10vw, 6rem);
font-weight: 700;
letter-spacing: -0.02em;
line-height: 1;
margin-bottom: 1rem;
color: var(--page-text);
}
.cta-tagline {
font-size: clamp(0.9rem, 2vw, 1.15rem);
color: var(--page-muted);
margin-bottom: 3rem;
opacity: 0;
}
.cta-buttons {
display: flex;
gap: 1rem;
justify-content: center;
flex-wrap: wrap;
}
.btn {
padding: 0.9rem 2.5rem;
border-radius: 999px;
font: 600 0.9rem / 1 "SF Pro Display", "Inter", system-ui, sans-serif;
cursor: pointer;
transition: all 0.3s ease;
border: none;
opacity: 0;
transform: translateY(20px);
}
.btn-primary {
background: var(--page-gold);
color: #0a0a0f;
}
.btn-primary:hover {
background: #ffe040;
box-shadow: 0 0 24px rgba(255, 214, 10, 0.3);
}
.btn-secondary {
background: transparent;
border: 1.5px solid rgba(255, 255, 255, 0.2);
color: var(--page-accent);
}
.btn-secondary:hover {
border-color: var(--page-accent);
box-shadow: 0 0 16px rgba(41, 151, 255, 0.15);
}
.reduced-motion .btn,
.reduced-motion .cta-tagline {
opacity: 1;
transform: none;
}
/* Gradient orbs */
.orb {
position: absolute;
border-radius: 50%;
filter: blur(100px);
pointer-events: none;
z-index: 0;
}
.orb-1 {
width: 500px;
height: 500px;
background: rgba(41, 151, 255, 0.12);
bottom: -100px;
left: -100px;
}
.orb-2 {
width: 400px;
height: 400px;
background: rgba(191, 90, 242, 0.1);
top: -50px;
right: -80px;
}
/* โโ Responsive โโ */
@media (max-width: 768px) {
.feature-container {
justify-content: center;
padding: 0 2rem;
}
.specs-grid {
grid-template-columns: repeat(2, 1fr);
}
.section {
padding: 4rem 1.25rem;
}
}
@media (max-width: 480px) {
.specs-grid {
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.spec-card {
padding: 1.5rem 1rem;
}
}if (!window.MotionPreference) {
const __mql = window.matchMedia("(prefers-reduced-motion: reduce)");
const __listeners = new Set();
const MotionPreference = {
prefersReducedMotion() {
return __mql.matches;
},
setOverride(value) {
const reduced = Boolean(value);
document.documentElement.classList.toggle("reduced-motion", reduced);
window.dispatchEvent(new CustomEvent("motion-preference", { detail: { reduced } }));
for (const listener of __listeners) {
try {
listener({ reduced, override: reduced, systemReduced: __mql.matches });
} catch {}
}
},
onChange(listener) {
__listeners.add(listener);
try {
listener({
reduced: __mql.matches,
override: null,
systemReduced: __mql.matches,
});
} catch {}
return () => __listeners.delete(listener);
},
getState() {
return { reduced: __mql.matches, override: null, systemReduced: __mql.matches };
},
};
window.MotionPreference = MotionPreference;
}
function prefersReducedMotion() {
return window.MotionPreference.prefersReducedMotion();
}
function initDemoShell() {
// No-op shim in imported standalone snippets.
}
import * as THREE from "three";
import gsap from "gsap";
import { ScrollTrigger } from "gsap/ScrollTrigger";
import { SplitText } from "gsap/SplitText";
import { ScrambleTextPlugin } from "gsap/ScrambleTextPlugin";
import Lenis from "lenis";
gsap.registerPlugin(ScrollTrigger, SplitText, ScrambleTextPlugin);
// โโ Demo Shell โโ
initDemoShell({
title: "Product Landing Page",
category: "pages",
tech: ["three.js", "gsap", "lenis", "splittext"],
});
// โโ Lenis โโ
const lenis = new Lenis({ lerp: 0.1, smoothWheel: true });
lenis.on("scroll", ScrollTrigger.update);
gsap.ticker.add((time) => lenis.raf(time * 1000));
gsap.ticker.lagSmoothing(0);
let reduced = prefersReducedMotion();
if (reduced) document.documentElement.classList.add("reduced-motion");
window.addEventListener("motion-preference", (e) => {
reduced = e.detail.reduced;
document.documentElement.classList.toggle("reduced-motion", reduced);
ScrollTrigger.refresh();
});
const dur = (d) => (reduced ? 0 : d);
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// THREE.JS SETUP
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const container = document.getElementById("canvas-container");
const scene = new THREE.Scene();
scene.background = new THREE.Color("#0a0a0f");
const camera = new THREE.PerspectiveCamera(45, window.innerWidth / window.innerHeight, 0.1, 100);
camera.position.set(0, 0.5, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.3;
container.appendChild(renderer.domElement);
// โโ Lighting โโ
scene.add(new THREE.AmbientLight(0x111122, 0.4));
const keyLight = new THREE.DirectionalLight(0xffffff, 2.5);
keyLight.position.set(4, 5, 5);
scene.add(keyLight);
const fillLight = new THREE.DirectionalLight(0x2997ff, 1.2);
fillLight.position.set(-4, 2, -3);
scene.add(fillLight);
const rimLight = new THREE.DirectionalLight(0xbf5af2, 1.5);
rimLight.position.set(0, -2, -5);
scene.add(rimLight);
// โโ Product โ AirPods-style Earbuds โโ
const productMat = new THREE.MeshPhysicalMaterial({
color: 0xf5f5f7,
metalness: 0.95,
roughness: 0.05,
clearcoat: 1.0,
clearcoatRoughness: 0.03,
reflectivity: 1.0,
});
// Earbud body profile (LatheGeometry) โ traces cross-section of an earbud
const earbudProfile = [
new THREE.Vector2(0.0, -0.3), // Bottom center (stem junction)
new THREE.Vector2(0.12, -0.25), // Slight outward taper at base
new THREE.Vector2(0.22, -0.15), // Widening toward body
new THREE.Vector2(0.32, 0.0), // Main body widest point (lower)
new THREE.Vector2(0.35, 0.15), // Body maximum width
new THREE.Vector2(0.34, 0.3), // Still wide, slight taper
new THREE.Vector2(0.3, 0.42), // Tapering toward speaker face
new THREE.Vector2(0.24, 0.5), // Speaker face edge
new THREE.Vector2(0.15, 0.55), // Speaker face rounding
new THREE.Vector2(0.0, 0.57), // Top center (speaker face)
];
function createEarbud() {
const group = new THREE.Group();
// Body โ revolved earbud shape
const bodyGeo = new THREE.LatheGeometry(earbudProfile, 48);
const bodyMesh = new THREE.Mesh(bodyGeo, productMat);
group.add(bodyMesh);
// Stem โ capsule extending downward
const stemGeo = new THREE.CapsuleGeometry(0.04, 0.52, 8, 16);
const stemMesh = new THREE.Mesh(stemGeo, productMat);
stemMesh.position.y = -0.56;
group.add(stemMesh);
return { group, bodyGeo, stemGeo };
}
const leftEarbud = createEarbud();
const rightEarbud = createEarbud();
// Position pair: tilted outward like resting earbuds
leftEarbud.group.position.x = -0.55;
leftEarbud.group.rotation.z = 0.15;
rightEarbud.group.position.x = 0.55;
rightEarbud.group.rotation.z = -0.15;
// Parent group โ this is the `product` used by all animation code
const product = new THREE.Group();
product.add(leftEarbud.group);
product.add(rightEarbud.group);
product.position.set(0, 0.3, 0);
scene.add(product);
// โโ Fog โโ
scene.fog = new THREE.FogExp2(0x0a0a0f, 0.03);
// โโ Animation state โโ
const state = {
productScale: 0.6,
productX: 0,
productRotY: 0,
activeFeature: 0,
};
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// HERO ENTRANCE
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const heroTitle = document.querySelector(".hero-title");
const heroTagline = document.querySelector(".hero-tagline");
// SplitText for hero
const titleSplit = new SplitText(heroTitle, { type: "chars", charsClass: "char" });
gsap.set(titleSplit.chars, {
opacity: 0,
y: reduced ? 0 : 60,
rotateX: reduced ? 0 : -90,
});
const heroTl = gsap.timeline({ delay: 0.3 });
// Scale product in
heroTl
.to(state, {
productScale: 1,
duration: dur(1.4),
ease: "expo.out",
})
.to(
titleSplit.chars,
{
opacity: 1,
y: 0,
rotateX: 0,
duration: dur(0.6),
ease: "back.out(1.4)",
stagger: { each: 0.03 },
},
0.3
)
.to(
heroTagline,
{
opacity: 1,
y: 0,
duration: dur(0.8),
ease: "expo.out",
},
0.7
)
.to(
"#scroll-indicator",
{
opacity: 1,
duration: dur(0.6),
ease: "expo.out",
},
1.2
);
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SCROLL: Hero โ Features transition (product shifts right)
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
gsap.to(state, {
productX: 2.5,
productScale: 0.7,
ease: "none",
scrollTrigger: {
trigger: ".hero-section",
start: "bottom bottom",
end: "+=300",
scrub: 1.5,
},
});
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// FEATURES: Pinned section with content swap
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const featureCards = document.querySelectorAll(".feature-card");
const stepDots = document.querySelectorAll(".step-dot");
// Show first card initially
featureCards[0].classList.add("active");
// Feature stat targets
const statTargets = [
{ id: "stat-anc", target: 40, suffix: "" },
{ id: "stat-spatial", target: 360, suffix: "ยฐ" },
{ id: "stat-battery", target: 50, suffix: "" },
];
// Product rotation angles per feature
const featureRotations = [0, Math.PI * 0.6, Math.PI * 1.2];
const featureColors = [
new THREE.Color(0xf5f5f7), // White/silver
new THREE.Color(0x2997ff), // Blue
new THREE.Color(0xffd60a), // Gold
];
function setActiveFeature(index) {
if (index === state.activeFeature && featureCards[index].classList.contains("active")) return;
featureCards.forEach((c) => c.classList.remove("active"));
stepDots.forEach((d) => d.classList.remove("active"));
featureCards[index].classList.add("active");
stepDots[index].classList.add("active");
// Animate product rotation and color
if (!reduced) {
gsap.to(state, {
productRotY: featureRotations[index],
duration: 0.8,
ease: "expo.out",
});
}
// Animate counter
const stat = statTargets[index];
const el = document.getElementById(stat.id);
if (el && el.dataset.animated !== "true") {
el.dataset.animated = "true";
if (reduced) {
el.textContent = stat.target + stat.suffix;
} else {
const counter = { val: 0 };
gsap.to(counter, {
val: stat.target,
duration: 1.2,
ease: "power2.out",
onUpdate: () => {
el.textContent = Math.round(counter.val) + stat.suffix;
},
});
}
}
state.activeFeature = index;
}
// ScrollTrigger for feature track
ScrollTrigger.create({
trigger: ".feature-track",
start: "top top",
end: "bottom bottom",
scrub: 0,
onUpdate: (self) => {
const p = self.progress;
if (p < 0.33) setActiveFeature(0);
else if (p < 0.66) setActiveFeature(1);
else setActiveFeature(2);
// Color lerp
const colorProgress = p * (featureColors.length - 1);
const ci = Math.floor(colorProgress);
const ct = colorProgress - ci;
const c1 = featureColors[Math.min(ci, featureColors.length - 1)];
const c2 = featureColors[Math.min(ci + 1, featureColors.length - 1)];
productMat.color.lerpColors(c1, c2, ct);
},
});
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// SPECS GRID: Staggered entrance
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const specsHeading = document.querySelector(".specs-heading");
if (specsHeading) {
const specsSplit = new SplitText(specsHeading, { type: "chars", charsClass: "char" });
gsap.set(specsSplit.chars, { opacity: 0, y: reduced ? 0 : 30 });
gsap.to(specsSplit.chars, {
opacity: 1,
y: 0,
duration: dur(0.5),
ease: "back.out(1.2)",
stagger: { each: 0.02 },
scrollTrigger: {
trigger: ".specs-section",
start: "top 75%",
toggleActions: "play none none reverse",
},
});
}
gsap.to(".spec-card", {
opacity: 1,
y: 0,
scale: 1,
duration: dur(0.6),
ease: "expo.out",
stagger: {
each: 0.08,
from: "center",
grid: [2, 3],
},
scrollTrigger: {
trigger: ".specs-grid",
start: "top 80%",
toggleActions: "play none none reverse",
},
});
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// CTA: ScrambleText price reveal
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const ctaPrice = document.getElementById("cta-price");
if (reduced) {
ctaPrice.textContent = "$299";
gsap.set(".cta-tagline", { opacity: 1 });
gsap.set(".btn", { opacity: 1, y: 0 });
} else {
gsap.set(ctaPrice, { opacity: 0 });
ScrollTrigger.create({
trigger: ".cta-section",
start: "top 60%",
once: true,
onEnter: () => {
const ctaTl = gsap.timeline();
ctaTl
.to(ctaPrice, { opacity: 1, duration: 0.1 })
.to(ctaPrice, {
duration: 1.0,
scrambleText: {
text: "$299",
chars: "0123456789$",
speed: 0.3,
},
})
.to(
".cta-tagline",
{
opacity: 1,
duration: 0.6,
ease: "expo.out",
},
"-=0.3"
)
.to(
".btn",
{
opacity: 1,
y: 0,
duration: 0.5,
ease: "back.out(1.7)",
stagger: 0.1,
},
"-=0.2"
);
},
});
}
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// THREE.JS ANIMATION LOOP
// โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const elapsed = clock.getElapsedTime();
// Apply scale and position from scroll state
product.scale.setScalar(state.productScale);
product.position.x = state.productX;
// Rotation: base from scroll + slow auto-rotation
if (!reduced) {
product.rotation.y = state.productRotY + elapsed * 0.15;
product.rotation.x = Math.sin(elapsed * 0.1) * 0.05;
} else {
product.rotation.y = state.productRotY;
}
renderer.render(scene, camera);
}
animate();
// โโ Resize โโ
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// โโ Cleanup โโ
window.addEventListener("beforeunload", () => {
leftEarbud.bodyGeo.dispose();
leftEarbud.stemGeo.dispose();
rightEarbud.bodyGeo.dispose();
rightEarbud.stemGeo.dispose();
productMat.dispose();
renderer.dispose();
});<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Product Landing Page โ stealthisdesign</title>
<link rel="stylesheet" href="style.css">
<script type="importmap">{"imports":{"gsap":"https://esm.sh/gsap@3.13.0","gsap/ScrollTrigger":"https://esm.sh/gsap@3.13.0/ScrollTrigger","gsap/SplitText":"https://esm.sh/gsap@3.13.0/SplitText","gsap/Flip":"https://esm.sh/gsap@3.13.0/Flip","gsap/ScrambleTextPlugin":"https://esm.sh/gsap@3.13.0/ScrambleTextPlugin","gsap/TextPlugin":"https://esm.sh/gsap@3.13.0/TextPlugin","gsap/all":"https://esm.sh/gsap@3.13.0/all","gsap/":"https://esm.sh/gsap@3.13.0/","lenis":"https://esm.sh/lenis@1.1.13/dist/lenis.mjs","three":"https://esm.sh/three@0.171.0","three/addons/":"https://esm.sh/three@0.171.0/examples/jsm/"}}</script>
<style>html.lenis,
html.lenis body {
height: auto;
}
.lenis:not(.lenis-autoToggle).lenis-stopped {
overflow: clip;
}
.lenis [data-lenis-prevent],
.lenis [data-lenis-prevent-wheel],
.lenis [data-lenis-prevent-touch] {
overscroll-behavior: contain;
}
.lenis.lenis-smooth iframe {
pointer-events: none;
}
.lenis.lenis-autoToggle {
transition-property: overflow;
transition-duration: 1ms;
transition-behavior: allow-discrete;
}</style>
</head>
<body>
<!-- Three.js canvas (fixed behind everything) -->
<div id="canvas-container" aria-hidden="true"></div>
<!-- Hero -->
<section class="section hero-section" id="hero">
<div class="hero-content">
<h1 class="hero-title">Aether Pro</h1>
<p class="hero-tagline">Sound, Reimagined.</p>
<div class="scroll-indicator" id="scroll-indicator">
<span>Discover</span>
<svg width="14" height="20" viewBox="0 0 14 20" fill="none">
<path d="M7 0v16M1 10l6 6 6-6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
</div>
</section>
<!-- Feature Showcase (pinned) -->
<section class="feature-track" id="features">
<div class="feature-container">
<div class="feature-content">
<!-- Step indicators -->
<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>
<!-- Feature cards -->
<div class="feature-card" id="feature-0">
<div class="feature-icon">
<svg viewBox="0 0 48 48" fill="none" aria-hidden="true">
<path d="M6 24h4M10 16l3 2M10 32l3-2M38 24h4M35 16l-3 2M35 32l-3-2" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<path d="M18 14c-3 2-5 6-5 10s2 8 5 10M30 14c3 2 5 6 5 10s-2 8-5 10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="24" cy="24" r="4" stroke="currentColor" stroke-width="1.5"/>
</svg>
</div>
<span class="feature-label">01 โ Silence</span>
<h2 class="feature-heading">Active Noise Cancellation</h2>
<p class="feature-desc">Adaptive hybrid ANC with dual microphones eliminates up to 40dB of ambient noise. The world disappears, and only your music remains.</p>
<div class="feature-stat">
<span class="stat-value" id="stat-anc">0</span>
<span class="stat-unit">dB noise cancelled</span>
</div>
</div>
<div class="feature-card" id="feature-1">
<div class="feature-icon">
<svg viewBox="0 0 48 48" fill="none" aria-hidden="true">
<circle cx="24" cy="24" r="18" stroke="currentColor" stroke-width="1.5" stroke-dasharray="4 3"/>
<circle cx="24" cy="24" r="10" stroke="currentColor" stroke-width="1.5"/>
<circle cx="24" cy="24" r="3" fill="currentColor"/>
<path d="M24 6v4M24 38v4M6 24h4M38 24h4" stroke="currentColor" stroke-width="1" stroke-linecap="round"/>
</svg>
</div>
<span class="feature-label">02 โ Dimension</span>
<h2 class="feature-heading">Spatial Audio</h2>
<p class="feature-desc">Dynamic head tracking places sound precisely in 3D space. Turn your head and the soundstage stays anchored to the world around you.</p>
<div class="feature-stat">
<span class="stat-value" id="stat-spatial">0</span>
<span class="stat-unit">degrees of immersion</span>
</div>
</div>
<div class="feature-card" id="feature-2">
<div class="feature-icon">
<svg viewBox="0 0 48 48" fill="none" aria-hidden="true">
<rect x="14" y="10" width="20" height="28" rx="4" stroke="currentColor" stroke-width="1.5"/>
<rect x="18" y="14" width="12" height="20" rx="2" stroke="currentColor" stroke-width="1" opacity="0.5"/>
<path d="M20 18h8M20 22h8M20 26h5" stroke="currentColor" stroke-width="1" stroke-linecap="round" opacity="0.5"/>
<path d="M30 30l4 4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
</svg>
</div>
<span class="feature-label">03 โ Endurance</span>
<h2 class="feature-heading">50-Hour Battery</h2>
<p class="feature-desc">Marathon listening sessions without interruption. Quick charge delivers 5 hours of playback from just 10 minutes of charging.</p>
<div class="feature-stat">
<span class="stat-value" id="stat-battery">0</span>
<span class="stat-unit">hours total playtime</span>
</div>
</div>
</div>
</div>
</section>
<!-- Specs Grid -->
<section class="section specs-section" id="specs">
<h2 class="specs-heading">Built for the future.</h2>
<div class="specs-grid">
<div class="spec-card">
<span class="spec-icon">
<svg viewBox="0 0 24 24" fill="none"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" stroke="currentColor" stroke-width="1.5"/><path d="M8 12l3 3 5-5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<span class="spec-value">IP68</span>
<span class="spec-label">Water Resistant</span>
</div>
<div class="spec-card">
<span class="spec-icon">
<svg viewBox="0 0 24 24" fill="none"><path d="M12 18.5a6.5 6.5 0 100-13 6.5 6.5 0 000 13zM12 2v2M12 20v2M4.22 4.22l1.42 1.42M18.36 18.36l1.42 1.42M2 12h2M20 12h2M4.22 19.78l1.42-1.42M18.36 5.64l1.42-1.42" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<span class="spec-value">11mm</span>
<span class="spec-label">Custom Driver</span>
</div>
<div class="spec-card">
<span class="spec-icon">
<svg viewBox="0 0 24 24" fill="none"><path d="M5 12.55a11 11 0 0114.08 0M1.42 9a16 16 0 0121.16 0M8.53 16.11a6 6 0 016.95 0M12 20h.01" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>
</span>
<span class="spec-value">BT 5.3</span>
<span class="spec-label">Bluetooth</span>
</div>
<div class="spec-card">
<span class="spec-icon">
<svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="1.5"/><path d="M12 1v4M12 19v4M4.22 4.22L7.05 7.05M16.95 16.95l2.83 2.83M1 12h4M19 12h4M4.22 19.78l2.83-2.83M16.95 7.05l2.83-2.83" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<span class="spec-value">4.2g</span>
<span class="spec-label">Per Earbud</span>
</div>
<div class="spec-card">
<span class="spec-icon">
<svg viewBox="0 0 24 24" fill="none"><path d="M19 11H5a2 2 0 00-2 2v6a2 2 0 002 2h14a2 2 0 002-2v-6a2 2 0 00-2-2zM7 11V7a5 5 0 0110 0v4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<span class="spec-value">LDAC</span>
<span class="spec-label">Hi-Res Codec</span>
</div>
<div class="spec-card">
<span class="spec-icon">
<svg viewBox="0 0 24 24" fill="none"><rect x="2" y="6" width="20" height="12" rx="2" stroke="currentColor" stroke-width="1.5"/><path d="M6 10h4v4H6z" stroke="currentColor" stroke-width="1"/><path d="M14 10h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/><path d="M14 14h4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</span>
<span class="spec-value">USB-C</span>
<span class="spec-label">Quick Charge</span>
</div>
</div>
</section>
<!-- CTA -->
<section class="section cta-section" id="cta">
<div class="cta-content">
<div class="cta-price" id="cta-price">$299</div>
<p class="cta-tagline">Silence has never sounded so good.</p>
<div class="cta-buttons">
<button class="btn btn-primary">Buy Now</button>
<button class="btn btn-secondary">Learn More</button>
</div>
</div>
<!-- Gradient orbs -->
<div class="orb orb-1" aria-hidden="true"></div>
<div class="orb orb-2" aria-hidden="true"></div>
</section>
<script type="module" src="script.js"></script>
</body>
</html>Product Landing Page
Apple-style wireless earbuds product page with 3D hero, scroll-pinned feature reveals, and cinematic text choreography.
Source
- Repository:
libs-genclaude - Original demo id:
23-product-page
Notes
Apple-style wireless earbuds product page with 3D hero, scroll-pinned feature reveals, and cinematic text choreography.