Pulse
Health app launch page with an animated phone mockup, activity rings and store badges.
app/page.tsx
"use client";
import { motion, useScroll, useTransform } from "motion/react";
import { Apple, Bell, Flame, Footprints, Heart, Moon, Play, Smartphone, Star, Trophy, Users } from "lucide-react";
import { useRef } from "react";
const ease = [0.22, 1, 0.36, 1] as const;
const up = {
initial: { opacity: 0, y: 24 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.3 },
transition: { duration: 0.7, ease },
};
function Ring({ value, color, size, stroke = 12, delay = 0 }: { value: number; color: string; size: number; stroke?: number; delay?: number }) {
const r = (size - stroke) / 2;
return (
<svg width={size} height={size} className="absolute -rotate-90">
<circle cx={size / 2} cy={size / 2} r={r} fill="none" stroke={color} strokeOpacity={0.15} strokeWidth={stroke} />
<motion.circle
cx={size / 2}
cy={size / 2}
r={r}
fill="none"
stroke={color}
strokeWidth={stroke}
strokeLinecap="round"
initial={{ pathLength: 0 }}
whileInView={{ pathLength: value }}
viewport={{ once: true }}
transition={{ duration: 1.6, delay, ease }}
/>
</svg>
);
}
function Phone() {
return (
<div className="relative mx-auto h-[580px] w-[290px] rounded-[3rem] border-[10px] border-zinc-900 bg-white shadow-[0_50px_100px_-20px_rgba(225,29,72,0.35)]">
<div className="absolute left-1/2 top-2 h-6 w-24 -translate-x-1/2 rounded-full bg-zinc-900" />
<div className="flex h-full flex-col px-5 pb-6 pt-12">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-zinc-400">Thursday</p>
<p className="text-lg font-semibold text-zinc-900">Good morning, Sam</p>
</div>
<Bell className="size-5 text-zinc-400" />
</div>
<div className="relative mx-auto mt-6 grid size-44 place-items-center">
<Ring value={0.82} color="#e11d48" size={176} delay={0.6} />
<div className="absolute grid size-[136px] place-items-center">
<Ring value={0.64} color="#f97316" size={136} delay={0.8} />
</div>
<div className="absolute grid size-24 place-items-center">
<Ring value={0.9} color="#eab308" size={96} delay={1} />
</div>
<Flame className="relative size-6 text-rose-600" />
</div>
<div className="mt-6 grid grid-cols-3 gap-2 text-center">
{[
{ l: "Move", v: "482", c: "text-rose-600" },
{ l: "Steps", v: "8.2k", c: "text-orange-500" },
{ l: "Stand", v: "10h", c: "text-yellow-500" },
].map((s) => (
<div key={s.l} className="rounded-xl bg-zinc-50 py-2">
<p className={`text-base font-semibold ${s.c}`}>{s.v}</p>
<p className="text-[10px] text-zinc-500">{s.l}</p>
</div>
))}
</div>
<div className="mt-4 space-y-2">
{[
{ icon: Footprints, t: "Morning run", s: "5.2 km · 28 min" },
{ icon: Moon, t: "Sleep", s: "7h 42m · 92 score" },
].map((a, i) => (
<motion.div key={a.t} initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} transition={{ delay: 1.2 + i * 0.15 }} className="flex items-center gap-3 rounded-xl border border-zinc-100 p-2.5">
<span className="grid size-8 place-items-center rounded-lg bg-rose-50 text-rose-600"><a.icon className="size-4" /></span>
<div>
<p className="text-xs font-medium text-zinc-900">{a.t}</p>
<p className="text-[10px] text-zinc-500">{a.s}</p>
</div>
</motion.div>
))}
</div>
</div>
</div>
);
}
function StoreButtons() {
return (
<div className="flex flex-wrap gap-3">
<a href="#" className="flex items-center gap-3 rounded-xl bg-zinc-900 px-5 py-2.5 text-white transition hover:-translate-y-0.5">
<Apple className="size-6 fill-current" />
<span className="text-left leading-tight">
<span className="block text-[10px] text-zinc-400">Download on the</span>
<span className="block text-sm font-semibold">App Store</span>
</span>
</a>
<a href="#" className="flex items-center gap-3 rounded-xl bg-zinc-900 px-5 py-2.5 text-white transition hover:-translate-y-0.5">
<Play className="size-5 fill-current" />
<span className="text-left leading-tight">
<span className="block text-[10px] text-zinc-400">Get it on</span>
<span className="block text-sm font-semibold">Google Play</span>
</span>
</a>
</div>
);
}
function Hero() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({ target: ref, offset: ["start start", "end start"] });
const phoneY = useTransform(scrollYProgress, [0, 1], [0, 120]);
const phoneRotate = useTransform(scrollYProgress, [0, 1], [0, -6]);
return (
<section ref={ref} className="relative isolate overflow-hidden px-6 pb-24 pt-16">
<div aria-hidden className="absolute right-0 top-0 -z-10 size-[40rem] translate-x-1/3 -translate-y-1/4 rounded-full bg-gradient-to-br from-rose-100 to-orange-100 blur-3xl" />
<div className="mx-auto grid max-w-6xl items-center gap-16 lg:grid-cols-2">
<div>
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} className="flex items-center gap-2 text-sm text-zinc-600">
<span className="flex text-amber-400">{Array.from({ length: 5 }).map((_, i) => <Star key={i} className="size-4 fill-current" />)}</span>
4.9 · 120k ratings
</motion.div>
<motion.h1 initial={{ opacity: 0, y: 24 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.9, ease }} className="mt-5 text-balance text-5xl font-semibold leading-[1.05] tracking-tight text-zinc-900 sm:text-6xl">
Small habits.<br />
<span className="bg-gradient-to-r from-rose-600 to-orange-500 bg-clip-text text-transparent">Serious results.</span>
</motion.h1>
<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.2, duration: 0.8, ease }} className="mt-6 max-w-md text-lg text-zinc-500">
Pulse turns your movement, sleep and recovery into one daily score — with coaching that adapts to you.
</motion.p>
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ delay: 0.35, duration: 0.8, ease }} className="mt-9">
<StoreButtons />
</motion.div>
</div>
<motion.div style={{ y: phoneY, rotate: phoneRotate }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 1, ease }}>
<Phone />
</motion.div>
</div>
</section>
);
}
const features = [
{ icon: Heart, title: "Daily readiness", text: "A single score built from heart-rate variability, sleep and strain tells you when to push and when to rest." },
{ icon: Trophy, title: "Adaptive goals", text: "Targets that rise as you improve and ease off when life gets busy — so streaks feel achievable." },
{ icon: Users, title: "Train together", text: "Challenges with friends, shared workouts and gentle nudges keep motivation high." },
];
function Features() {
return (
<section className="px-6 py-24">
<div className="mx-auto max-w-6xl space-y-24">
{features.map((f, i) => (
<motion.div key={f.title} {...up} className={`grid items-center gap-12 md:grid-cols-2 ${i % 2 ? "md:[&>*:first-child]:order-2" : ""}`}>
<div className="grid aspect-[4/3] place-items-center rounded-[2rem] bg-gradient-to-br from-rose-50 to-orange-50">
<motion.div initial={{ scale: 0.6, rotate: -12, opacity: 0 }} whileInView={{ scale: 1, rotate: 0, opacity: 1 }} viewport={{ once: true }} transition={{ type: "spring", stiffness: 160, damping: 14, delay: 0.2 }} className="grid size-32 place-items-center rounded-[2rem] bg-white text-rose-600 shadow-2xl shadow-rose-900/10">
<f.icon className="size-14" strokeWidth={1.5} />
</motion.div>
</div>
<div>
<span className="text-sm font-semibold text-rose-600">0{i + 1}</span>
<h3 className="mt-3 text-4xl font-semibold tracking-tight text-zinc-900">{f.title}</h3>
<p className="mt-4 max-w-md text-lg text-zinc-500">{f.text}</p>
</div>
</motion.div>
))}
</div>
</section>
);
}
function Reviews() {
const reviews = [
{ n: "Jordan P.", t: "The only fitness app I've stuck with for more than a month. The readiness score changed how I train." },
{ n: "Aisha K.", t: "Beautiful design and genuinely useful insights. My sleep has improved a full hour on average." },
{ n: "Tom R.", t: "Challenges with my brothers keep us all moving. We're 200 days into our streak." },
];
return (
<section className="bg-zinc-50 px-6 py-24">
<div className="mx-auto max-w-6xl">
<motion.h2 {...up} className="text-center text-4xl font-semibold tracking-tight text-zinc-900">People feel the difference</motion.h2>
<div className="mt-12 grid gap-5 md:grid-cols-3">
{reviews.map((r, i) => (
<motion.div key={r.n} {...up} transition={{ ...up.transition, delay: i * 0.1 }} className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-zinc-200">
<div className="flex text-amber-400">{Array.from({ length: 5 }).map((_, k) => <Star key={k} className="size-4 fill-current" />)}</div>
<p className="mt-4 text-zinc-700">{r.t}</p>
<p className="mt-6 text-sm font-medium text-zinc-900">{r.n}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}
function Download() {
return (
<section className="px-6 py-24">
<motion.div {...up} className="relative isolate mx-auto max-w-6xl overflow-hidden rounded-[2rem] bg-gradient-to-br from-rose-600 to-orange-500 px-8 py-20 text-center text-white">
<Smartphone className="mx-auto size-10 opacity-80" />
<h2 className="mx-auto mt-6 max-w-xl text-4xl font-semibold tracking-tight sm:text-5xl">Start your first week free</h2>
<p className="mx-auto mt-4 max-w-md text-rose-50">Available on iPhone, Android and Apple Watch.</p>
<div className="mt-10 flex justify-center"><StoreButtons /></div>
</motion.div>
</section>
);
}
export default function PulseTemplate() {
return (
<div className="min-h-screen bg-white text-zinc-900 antialiased">
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur-lg">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<span className="flex items-center gap-2 font-semibold">
<span className="grid size-8 place-items-center rounded-xl bg-gradient-to-br from-rose-600 to-orange-500 text-white"><Flame className="size-4" /></span>
Pulse
</span>
<nav className="hidden gap-8 text-sm text-zinc-600 md:flex">
<a href="#" className="hover:text-zinc-900">Features</a>
<a href="#" className="hover:text-zinc-900">Science</a>
<a href="#" className="hover:text-zinc-900">Reviews</a>
</nav>
<a href="#" className="rounded-full bg-zinc-900 px-4 py-2 text-sm font-medium text-white">Download</a>
</div>
</header>
<main>
<Hero />
<Features />
<Reviews />
<Download />
</main>
<footer className="px-6 py-10 text-center text-sm text-zinc-400">© {new Date().getFullYear()} Pulse Health Inc.</footer>
</div>
);
}
What's included
- Navbar
- Hero + phone
- Feature rows
- Reviews
- Download CTA
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.