Stride
Bold gym page with a masked headline reveal, bookable class schedule by day, coach cards and monthly/yearly membership.
app/page.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import { ArrowRight, ArrowUpRight, Check, Dumbbell, Flame, HeartPulse, Timer } from "lucide-react";
import { useState } from "react";
const ease = [0.22, 1, 0.36, 1] as const;
const up = {
initial: { opacity: 0, y: 28 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.3 },
transition: { duration: 0.7, ease },
};
const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] as const;
type Session = { time: string; name: string; coach: string; spots: number; level: "All levels" | "Intermediate" | "Advanced" };
const SCHEDULE: Record<(typeof DAYS)[number], Session[]> = {
Mon: [
{ time: "06:00", name: "Sunrise HIIT", coach: "Arjun", spots: 4, level: "All levels" },
{ time: "18:30", name: "Strength 101", coach: "Meera", spots: 9, level: "All levels" },
{ time: "20:00", name: "Boxing rounds", coach: "Kabir", spots: 0, level: "Intermediate" },
],
Tue: [
{ time: "07:00", name: "Mobility flow", coach: "Isha", spots: 12, level: "All levels" },
{ time: "19:00", name: "Barbell club", coach: "Meera", spots: 3, level: "Advanced" },
],
Wed: [
{ time: "06:00", name: "Sunrise HIIT", coach: "Arjun", spots: 7, level: "All levels" },
{ time: "18:30", name: "Kettlebell engine", coach: "Kabir", spots: 5, level: "Intermediate" },
],
Thu: [
{ time: "07:00", name: "Run club 5K", coach: "Isha", spots: 15, level: "All levels" },
{ time: "19:00", name: "Strength 101", coach: "Meera", spots: 2, level: "All levels" },
],
Fri: [
{ time: "06:00", name: "Friday burner", coach: "Arjun", spots: 0, level: "Advanced" },
{ time: "18:00", name: "Boxing rounds", coach: "Kabir", spots: 6, level: "Intermediate" },
],
Sat: [
{ time: "08:00", name: "Community workout", coach: "All coaches", spots: 20, level: "All levels" },
{ time: "10:00", name: "Mobility flow", coach: "Isha", spots: 8, level: "All levels" },
],
};
const coaches = [
{ name: "Arjun Rao", focus: "HIIT and conditioning", from: "#a3e635", to: "#22c55e" },
{ name: "Meera Iyer", focus: "Strength and lifting", from: "#facc15", to: "#f97316" },
{ name: "Kabir Khan", focus: "Boxing", from: "#fb7185", to: "#e11d48" },
{ name: "Isha Nair", focus: "Mobility and running", from: "#38bdf8", to: "#6366f1" },
];
const plans = [
{ name: "Drop-in", monthly: 499, unit: "per class", perks: ["Any single class", "Towel and locker", "No commitment"] },
{ name: "Unlimited", monthly: 2999, unit: "per month", perks: ["Every class, every day", "Open gym access", "1 coach check-in a month", "Bring a friend on Saturdays"], featured: true },
{ name: "Personal", monthly: 7999, unit: "per month", perks: ["Unlimited classes", "8 one-to-one sessions", "Custom nutrition plan"] },
];
function Marquee() {
const words = ["Strength", "Stamina", "Mobility", "Boxing", "Run club", "Community"];
return (
<div className="overflow-hidden border-y border-white/10 bg-lime-400 py-3 text-zinc-950" aria-hidden>
<motion.div className="flex w-max gap-10 text-sm font-bold uppercase tracking-[0.25em]" animate={{ x: ["0%", "-50%"] }} transition={{ duration: 22, repeat: Infinity, ease: "linear" }}>
{[...words, ...words, ...words, ...words].map((w, i) => (
<span key={i} className="flex items-center gap-10">
{w} <Flame className="size-4" />
</span>
))}
</motion.div>
</div>
);
}
export default function StrideTemplate() {
const [day, setDay] = useState<(typeof DAYS)[number]>("Mon");
const [yearly, setYearly] = useState(false);
const [booked, setBooked] = useState<string[]>([]);
const price = (monthly: number, name: string) => (name === "Drop-in" || !yearly ? monthly : Math.round((monthly * 0.8) / 10) * 10);
return (
<div className="min-h-screen bg-zinc-950 font-sans text-zinc-300 antialiased">
<header className="sticky top-0 z-40 border-b border-white/10 bg-zinc-950/85 backdrop-blur-xl">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<span className="flex items-center gap-2 text-lg font-black uppercase tracking-tight text-white">
<Dumbbell className="size-5 text-lime-400" /> Stride
</span>
<nav className="hidden gap-8 text-sm font-medium md:flex">
{["Classes", "Coaches", "Membership", "Location"].map((l) => (
<a key={l} href="#" className="transition-colors hover:text-lime-400">
{l}
</a>
))}
</nav>
<a href="#plans" className="rounded-full bg-lime-400 px-5 py-2 text-sm font-bold text-zinc-950 transition hover:bg-lime-300">
Free trial
</a>
</div>
</header>
<section className="relative overflow-hidden">
<motion.div aria-hidden className="absolute -right-40 -top-40 size-[34rem] rounded-full bg-lime-400/20 blur-3xl" animate={{ scale: [1, 1.2, 1] }} transition={{ duration: 8, repeat: Infinity, ease: "easeInOut" }} />
<div className="relative mx-auto max-w-6xl px-6 pb-20 pt-20 lg:pt-28">
<motion.p initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="text-sm font-bold uppercase tracking-[0.3em] text-lime-400">
Indiranagar · Open 5 am to 11 pm
</motion.p>
<h1 className="mt-6 text-6xl font-black uppercase leading-[0.9] tracking-tight text-white sm:text-8xl lg:text-9xl">
{["Stronger", "every", "single day."].map((line, i) => (
<span key={line} className="block overflow-hidden">
<motion.span className={`block ${i === 2 ? "text-lime-400" : ""}`} initial={{ y: "110%" }} animate={{ y: 0 }} transition={{ duration: 0.9, delay: 0.1 + i * 0.12, ease }}>
{line}
</motion.span>
</span>
))}
</h1>
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.6, duration: 0.7 }} className="mt-10 flex flex-wrap items-end justify-between gap-8">
<p className="max-w-md text-lg leading-relaxed text-zinc-400">Small-group training with coaches who know your name. Forty classes a week, and your first week is on us.</p>
<div className="flex flex-wrap gap-3">
<a href="#plans" className="group inline-flex items-center gap-2 rounded-full bg-lime-400 px-7 py-4 text-sm font-bold uppercase tracking-wide text-zinc-950 transition hover:-translate-y-0.5">
Start free week <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
</a>
<a href="#schedule" className="inline-flex items-center rounded-full border border-white/20 px-7 py-4 text-sm font-bold uppercase tracking-wide text-white hover:bg-white/5">
See schedule
</a>
</div>
</motion.div>
<div className="mt-16 grid gap-px overflow-hidden rounded-2xl bg-white/10 sm:grid-cols-3">
{[
{ icon: Timer, value: "45 min", label: "classes that fit a lunch break" },
{ icon: HeartPulse, value: "12 max", label: "people per class" },
{ icon: Flame, value: "40+", label: "classes every week" },
].map((s) => (
<motion.div key={s.label} {...up} className="flex items-center gap-4 bg-zinc-950 p-6">
<s.icon className="size-7 text-lime-400" />
<span>
<span className="block text-2xl font-black text-white">{s.value}</span>
<span className="text-sm text-zinc-500">{s.label}</span>
</span>
</motion.div>
))}
</div>
</div>
</section>
<Marquee />
<section id="schedule" className="mx-auto max-w-6xl px-6 py-24">
<motion.div {...up} className="flex flex-wrap items-end justify-between gap-6">
<h2 className="text-5xl font-black uppercase tracking-tight text-white">This week</h2>
<div role="tablist" aria-label="Day" className="flex w-full justify-between gap-1 rounded-full bg-white/5 p-1 sm:w-auto">
{DAYS.map((d) => (
<button key={d} role="tab" aria-selected={day === d} onClick={() => setDay(d)} className="relative flex-1 rounded-full px-2 py-2 text-xs font-bold uppercase sm:flex-none sm:px-4 sm:text-sm">
{day === d && <motion.span layoutId="stride-day" className="absolute inset-0 rounded-full bg-lime-400" transition={{ type: "spring", stiffness: 400, damping: 32 }} />}
<span className={`relative ${day === d ? "text-zinc-950" : "text-zinc-400"}`}>{d}</span>
</button>
))}
</div>
</motion.div>
<AnimatePresence mode="wait">
<motion.ul key={day} initial={{ opacity: 0, y: 12 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -12 }} transition={{ duration: 0.25 }} className="mt-10 divide-y divide-white/10 border-y border-white/10">
{SCHEDULE[day].map((s, i) => {
const id = `${day}-${s.time}`;
const isBooked = booked.includes(id);
const full = s.spots === 0;
return (
<motion.li key={id} initial={{ opacity: 0, x: -16 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: i * 0.06 }} className="flex flex-wrap items-center gap-x-5 gap-y-3 py-6 sm:gap-x-8">
<span className="w-16 text-2xl font-black tabular-nums text-white sm:w-20 sm:text-3xl">{s.time}</span>
<span className="min-w-[9rem] flex-1">
<span className="block text-xl font-bold text-white">{s.name}</span>
<span className="text-sm text-zinc-500">
with {s.coach} · {s.level}
</span>
</span>
<span className={`text-sm ${full ? "text-rose-400" : s.spots <= 4 ? "text-amber-400" : "text-zinc-500"}`}>{full ? "Class full" : `${s.spots} spots left`}</span>
<button
disabled={full && !isBooked}
onClick={() => setBooked((b) => (isBooked ? b.filter((x) => x !== id) : [...b, id]))}
aria-pressed={isBooked}
className={`inline-flex w-32 items-center justify-center gap-1.5 rounded-full py-2.5 text-sm font-bold uppercase transition ${isBooked ? "bg-white text-zinc-950" : full ? "cursor-not-allowed bg-white/5 text-zinc-600" : "bg-lime-400 text-zinc-950 hover:bg-lime-300"}`}
>
{isBooked ? (
<>
<Check className="size-4" /> Booked
</>
) : full ? (
"Waitlist"
) : (
"Book"
)}
</button>
</motion.li>
);
})}
</motion.ul>
</AnimatePresence>
</section>
<section className="mx-auto max-w-6xl px-6 pb-24">
<motion.h2 {...up} className="text-5xl font-black uppercase tracking-tight text-white">
Your coaches
</motion.h2>
<div className="mt-10 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{coaches.map((c, i) => (
<motion.a key={c.name} href="#" {...up} transition={{ duration: 0.6, delay: i * 0.08, ease }} className="group relative block h-72 overflow-hidden rounded-3xl" style={{ background: `linear-gradient(160deg, ${c.from}, ${c.to})` }}>
<span aria-hidden className="absolute -bottom-6 -right-2 text-[9rem] font-black leading-none text-black/15 transition-transform duration-500 group-hover:-translate-y-3">
{c.name[0]}
</span>
<span className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/70 to-transparent p-5">
<span className="flex items-center justify-between text-lg font-bold text-white">
{c.name} <ArrowUpRight className="size-5 transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5" />
</span>
<span className="text-sm text-white/80">{c.focus}</span>
</span>
</motion.a>
))}
</div>
</section>
<section id="plans" className="border-t border-white/10 bg-zinc-900 py-24">
<div className="mx-auto max-w-6xl px-6">
<motion.div {...up} className="flex flex-wrap items-end justify-between gap-6">
<h2 className="text-5xl font-black uppercase tracking-tight text-white">Membership</h2>
<button role="switch" aria-checked={yearly} onClick={() => setYearly((y) => !y)} className="flex items-center gap-3 text-sm font-medium text-zinc-300">
Monthly
<span className={`relative h-7 w-12 rounded-full transition-colors ${yearly ? "bg-lime-400" : "bg-white/15"}`}>
<motion.span layout className={`absolute top-1 size-5 rounded-full bg-white ${yearly ? "right-1" : "left-1"}`} transition={{ type: "spring", stiffness: 500, damping: 32 }} />
</span>
Yearly <span className="rounded-full bg-lime-400/15 px-2 py-0.5 text-xs font-bold text-lime-400">Save 20%</span>
</button>
</motion.div>
<div className="mt-10 grid gap-5 md:grid-cols-3">
{plans.map((p, i) => (
<motion.div key={p.name} {...up} transition={{ duration: 0.6, delay: i * 0.1, ease }} className={`rounded-3xl p-8 ${p.featured ? "bg-lime-400 text-zinc-950" : "bg-zinc-950 ring-1 ring-white/10"}`}>
<p className={`text-sm font-bold uppercase tracking-widest ${p.featured ? "text-zinc-800" : "text-lime-400"}`}>{p.name}</p>
<p className={`mt-5 flex items-baseline gap-2 ${p.featured ? "text-zinc-950" : "text-white"}`}>
<span className="relative inline-flex h-14 overflow-hidden text-5xl font-black tabular-nums">
<AnimatePresence mode="popLayout" initial={false}>
<motion.span key={price(p.monthly, p.name)} initial={{ y: 40, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: -40, opacity: 0 }} transition={{ type: "spring", stiffness: 300, damping: 28 }}>
₹{price(p.monthly, p.name).toLocaleString("en-IN")}
</motion.span>
</AnimatePresence>
</span>
<span className={`text-sm ${p.featured ? "text-zinc-700" : "text-zinc-500"}`}>{p.unit}</span>
</p>
<ul className="mt-7 space-y-3 text-sm">
{p.perks.map((x) => (
<li key={x} className="flex items-center gap-2.5">
<Check className={`size-4 ${p.featured ? "text-zinc-950" : "text-lime-400"}`} strokeWidth={3} /> {x}
</li>
))}
</ul>
<a href="#" className={`mt-8 block rounded-full py-3 text-center text-sm font-bold uppercase transition ${p.featured ? "bg-zinc-950 text-white hover:bg-zinc-800" : "bg-white/10 text-white hover:bg-white/15"}`}>
{p.featured ? "Start free week" : "Choose plan"}
</a>
</motion.div>
))}
</div>
</div>
</section>
<footer className="border-t border-white/10 py-10">
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-4 px-6 text-sm text-zinc-500">
<span className="flex items-center gap-2 font-black uppercase text-white">
<Dumbbell className="size-4 text-lime-400" /> Stride
</span>
<span>© 2026 Stride Training Club · 100 Feet Road, Indiranagar</span>
</div>
</footer>
</div>
);
}
What's included
- Navbar
- Hero
- Marquee
- Class schedule
- Coaches
- Membership pricing
- Footer
Use it in 3 steps
1. Install dependencies in your Next.js + Tailwind project:
$ npm i motion lucide-react- 2. Copy the code into
app/page.tsx. - 3. Replace the copy, colors and links with your own — then deploy.