Nimbus
Analytics SaaS landing page with live dashboard hero, product tabs, pricing and FAQ.
app/page.tsx
"use client";
import { AnimatePresence, motion } from "motion/react";
import {
ArrowRight,
BarChart3,
Bell,
Check,
Cloud,
Database,
Filter,
Layers,
LineChart,
Lock,
Menu,
Plus,
Sparkles,
Users,
X,
Zap,
} from "lucide-react";
import { useState } from "react";
const ease = [0.22, 1, 0.36, 1] as const;
const fadeUp = {
initial: { opacity: 0, y: 24 },
whileInView: { opacity: 1, y: 0 },
viewport: { once: true, amount: 0.3 },
transition: { duration: 0.7, ease },
};
function Nav() {
const [open, setOpen] = useState(false);
const links = ["Product", "Features", "Pricing", "Customers", "FAQ"];
return (
<header className="sticky top-0 z-50 border-b border-zinc-100 bg-white/80 backdrop-blur-lg">
<div className="mx-auto flex h-16 max-w-6xl items-center justify-between px-6">
<a href="#" className="flex items-center gap-2 font-semibold text-zinc-900">
<span className="grid size-8 place-items-center rounded-lg bg-blue-600 text-white">
<Cloud className="size-4" />
</span>
Nimbus
</a>
<nav className="hidden items-center gap-8 text-sm text-zinc-600 md:flex">
{links.map((l) => (
<a key={l} href={`#${l.toLowerCase()}`} className="transition-colors hover:text-zinc-900">
{l}
</a>
))}
</nav>
<div className="hidden items-center gap-3 md:flex">
<a href="#" className="text-sm text-zinc-600 hover:text-zinc-900">Log in</a>
<a href="#pricing" className="rounded-full bg-zinc-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-zinc-800">
Start free
</a>
</div>
<button aria-label="Menu" onClick={() => setOpen(!open)} className="md:hidden">
{open ? <X className="size-5" /> : <Menu className="size-5" />}
</button>
</div>
<AnimatePresence>
{open && (
<motion.nav
initial={{ height: 0 }}
animate={{ height: "auto" }}
exit={{ height: 0 }}
className="overflow-hidden border-t border-zinc-100 md:hidden"
>
<div className="space-y-1 px-6 py-4">
{links.map((l) => (
<a key={l} href={`#${l.toLowerCase()}`} onClick={() => setOpen(false)} className="block py-2 text-sm text-zinc-700">
{l}
</a>
))}
</div>
</motion.nav>
)}
</AnimatePresence>
</header>
);
}
function Dashboard() {
const bars = [42, 58, 50, 72, 64, 86, 78, 94, 70, 88, 96, 82];
return (
<div className="rounded-2xl border border-zinc-200 bg-white p-2 shadow-[0_40px_100px_-30px_rgba(37,99,235,0.35)]">
<div className="rounded-xl border border-zinc-100 bg-zinc-50/60 p-5">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<p className="text-xs text-zinc-500">Monthly recurring revenue</p>
<p className="text-2xl font-semibold tracking-tight text-zinc-900">$184,320</p>
</div>
<div className="flex gap-2">
{["7D", "30D", "12M"].map((p, i) => (
<span key={p} className={`rounded-md px-2.5 py-1 text-xs ${i === 2 ? "bg-zinc-900 text-white" : "bg-white text-zinc-500 ring-1 ring-zinc-200"}`}>
{p}
</span>
))}
</div>
</div>
<div className="mt-6 flex h-40 items-end gap-2">
{bars.map((h, i) => (
<motion.div
key={i}
initial={{ height: 0 }}
animate={{ height: `${h}%` }}
transition={{ duration: 0.9, delay: 0.6 + i * 0.05, ease }}
className={`flex-1 rounded-t-md ${i === bars.length - 2 ? "bg-blue-600" : "bg-blue-200"}`}
/>
))}
</div>
<div className="mt-5 grid grid-cols-3 gap-3">
{[
{ label: "Active users", value: "24.8k", delta: "+8.2%" },
{ label: "Churn", value: "1.9%", delta: "-0.4%" },
{ label: "ARPU", value: "$74", delta: "+3.1%" },
].map((s) => (
<div key={s.label} className="rounded-lg bg-white p-3 ring-1 ring-zinc-200">
<p className="text-[11px] text-zinc-500">{s.label}</p>
<p className="mt-0.5 font-semibold text-zinc-900">{s.value}</p>
<p className="text-[11px] font-medium text-emerald-600">{s.delta}</p>
</div>
))}
</div>
</div>
</div>
);
}
function Hero() {
return (
<section className="relative isolate overflow-hidden px-6 pb-24 pt-20">
<div aria-hidden className="absolute inset-x-0 top-0 -z-10 h-[600px] bg-gradient-to-b from-blue-50 via-white to-white" />
<div
aria-hidden
className="absolute inset-0 -z-10 [background-image:linear-gradient(to_right,#f1f5f9_1px,transparent_1px),linear-gradient(to_bottom,#f1f5f9_1px,transparent_1px)] [background-size:56px_56px] [mask-image:radial-gradient(ellipse_at_top,black,transparent_65%)]"
/>
<div className="mx-auto max-w-6xl text-center">
<motion.div initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.6, ease }} className="inline-flex items-center gap-2 rounded-full border border-blue-100 bg-white px-3 py-1 text-xs font-medium text-blue-700 shadow-sm">
<Sparkles className="size-3.5" /> AI forecasting is now live
</motion.div>
<motion.h1 initial={{ opacity: 0, y: 24, filter: "blur(8px)" }} animate={{ opacity: 1, y: 0, filter: "blur(0px)" }} transition={{ duration: 0.9, delay: 0.1, ease }} className="mx-auto mt-6 max-w-4xl text-balance text-5xl font-semibold tracking-tight text-zinc-900 sm:text-7xl">
Analytics that explain <span className="text-blue-600">why</span>, not just what
</motion.h1>
<motion.p initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, delay: 0.25, ease }} className="mx-auto mt-6 max-w-2xl text-lg text-zinc-500">
Nimbus connects your product, billing and support data so every team can see what drives growth — and act on it the same day.
</motion.p>
<motion.div initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.8, delay: 0.4, ease }} className="mt-9 flex flex-wrap justify-center gap-3">
<a href="#pricing" className="group inline-flex items-center gap-2 rounded-full bg-blue-600 px-6 py-3 text-sm font-medium text-white shadow-lg shadow-blue-600/25 transition hover:-translate-y-0.5 hover:bg-blue-700">
Start 14-day free trial <ArrowRight className="size-4 transition-transform group-hover:translate-x-0.5" />
</a>
<a href="#features" className="rounded-full border border-zinc-200 bg-white px-6 py-3 text-sm font-medium text-zinc-900 transition hover:bg-zinc-50">
Explore features
</a>
</motion.div>
<p className="mt-4 text-xs text-zinc-400">No credit card required · Setup in 5 minutes</p>
<motion.div initial={{ opacity: 0, y: 60, rotateX: 15 }} animate={{ opacity: 1, y: 0, rotateX: 0 }} transition={{ duration: 1.1, delay: 0.5, ease }} style={{ transformPerspective: 1400 }} className="mx-auto mt-16 max-w-4xl text-left">
<Dashboard />
</motion.div>
</div>
</section>
);
}
function Logos() {
const names = ["Lumora", "Vertexa", "Orbital", "Stackline", "Northwave", "Cubeform", "Launchly", "Keystone"];
return (
<section className="border-y border-zinc-100 py-10">
<p className="text-center text-sm text-zinc-500">Powering decisions at 3,000+ companies</p>
<div className="mt-6 flex overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_15%,black_85%,transparent)]">
<motion.div className="flex shrink-0 gap-14 pr-14" animate={{ x: ["0%", "-50%"] }} transition={{ duration: 28, repeat: Infinity, ease: "linear" }}>
{[...names, ...names].map((n, i) => (
<span key={i} className="flex items-center gap-2 text-xl font-semibold tracking-tight text-zinc-300">
<Layers className="size-5" /> {n}
</span>
))}
</motion.div>
</div>
</section>
);
}
const features = [
{ icon: LineChart, title: "Revenue intelligence", text: "MRR, expansion and churn broken down by plan, cohort and channel." },
{ icon: Users, title: "Product analytics", text: "Funnels, retention and paths built automatically from your events." },
{ icon: Sparkles, title: "AI forecasting", text: "Predict next quarter with models trained on your own history." },
{ icon: Bell, title: "Smart alerts", text: "Get notified in Slack the moment a metric moves unexpectedly." },
{ icon: Filter, title: "Segments", text: "Slice any chart by any property without writing a line of SQL." },
{ icon: Lock, title: "Enterprise security", text: "SOC 2 Type II, SSO, SCIM and granular role-based permissions." },
];
function Features() {
return (
<section id="features" className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<motion.div {...fadeUp} className="max-w-2xl">
<p className="text-sm font-semibold text-blue-600">Features</p>
<h2 className="mt-3 text-4xl font-semibold tracking-tight text-zinc-900 sm:text-5xl">Everything you need to understand growth</h2>
</motion.div>
<div className="mt-14 grid gap-px overflow-hidden rounded-3xl border border-zinc-200 bg-zinc-200 sm:grid-cols-2 lg:grid-cols-3">
{features.map((f, i) => (
<motion.div key={f.title} {...fadeUp} transition={{ ...fadeUp.transition, delay: i * 0.06 }} className="group bg-white p-8 transition-colors hover:bg-zinc-50/80">
<span className="grid size-11 place-items-center rounded-xl bg-blue-50 text-blue-600 transition-transform group-hover:scale-110 group-hover:-rotate-6">
<f.icon className="size-5" />
</span>
<h3 className="mt-6 font-semibold text-zinc-900">{f.title}</h3>
<p className="mt-2 text-sm leading-relaxed text-zinc-500">{f.text}</p>
</motion.div>
))}
</div>
</div>
</section>
);
}
const tabs = [
{ id: "connect", icon: Database, label: "Connect", title: "Every source in one place", text: "Stripe, Postgres, Segment, HubSpot, Snowflake and 80+ more. Data stays fresh with sync every 5 minutes.", points: ["One-click OAuth connectors", "Automatic schema mapping", "Historical backfill"] },
{ id: "analyze", icon: BarChart3, label: "Analyze", title: "Answers without SQL", text: "Ask questions in plain English or build charts with drag-and-drop. Share live dashboards with anyone.", points: ["Natural-language queries", "100+ chart types", "Live, shareable boards"] },
{ id: "act", icon: Zap, label: "Act", title: "Turn insight into action", text: "Trigger workflows, sync audiences back to your tools and keep every team aligned on the same numbers.", points: ["Reverse ETL to 30+ tools", "Slack & email digests", "Goal tracking"] },
];
function Product() {
const [active, setActive] = useState(tabs[0].id);
const tab = tabs.find((t) => t.id === active)!;
return (
<section id="product" className="bg-zinc-50 px-6 py-24">
<div className="mx-auto max-w-6xl">
<motion.h2 {...fadeUp} className="text-center text-4xl font-semibold tracking-tight text-zinc-900 sm:text-5xl">
From raw data to decisions
</motion.h2>
<div className="mt-10 flex justify-center">
<div className="inline-flex rounded-full border border-zinc-200 bg-white p-1">
{tabs.map((t) => (
<button key={t.id} onClick={() => setActive(t.id)} className={`relative flex items-center gap-2 rounded-full px-5 py-2 text-sm font-medium transition-colors ${active === t.id ? "text-white" : "text-zinc-600 hover:text-zinc-900"}`}>
{active === t.id && <motion.span layoutId="nimbus-tab" className="absolute inset-0 rounded-full bg-blue-600" transition={{ type: "spring", stiffness: 380, damping: 30 }} />}
<t.icon className="relative size-4" />
<span className="relative">{t.label}</span>
</button>
))}
</div>
</div>
<AnimatePresence mode="wait">
<motion.div key={tab.id} initial={{ opacity: 0, y: 16 }} animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -16 }} transition={{ duration: 0.35 }} className="mt-12 grid items-center gap-10 rounded-3xl border border-zinc-200 bg-white p-8 md:grid-cols-2 md:p-12">
<div>
<h3 className="text-3xl font-semibold tracking-tight text-zinc-900">{tab.title}</h3>
<p className="mt-4 text-zinc-500">{tab.text}</p>
<ul className="mt-6 space-y-3">
{tab.points.map((p) => (
<li key={p} className="flex items-center gap-3 text-sm text-zinc-700">
<span className="grid size-5 place-items-center rounded-full bg-blue-600 text-white"><Check className="size-3" /></span>
{p}
</li>
))}
</ul>
</div>
<div className="grid aspect-[4/3] place-items-center rounded-2xl bg-gradient-to-br from-blue-50 to-indigo-100">
<motion.div initial={{ scale: 0.8, rotate: -8 }} animate={{ scale: 1, rotate: 0 }} transition={{ type: "spring", stiffness: 200, damping: 16 }} className="grid size-28 place-items-center rounded-3xl bg-white text-blue-600 shadow-xl">
<tab.icon className="size-12" strokeWidth={1.5} />
</motion.div>
</div>
</motion.div>
</AnimatePresence>
</div>
</section>
);
}
const quotes = [
{ quote: "Nimbus replaced three tools and a weekly spreadsheet ritual. Our board deck now builds itself.", name: "Hannah Weber", role: "CFO, Stackline" },
{ quote: "The AI forecast was within 2% of actuals last quarter. It's now the number our whole company plans around.", name: "Rahul Mehta", role: "VP Growth, Orbital" },
{ quote: "Setup took 20 minutes. By lunch, our product team was answering questions they'd been stuck on for months.", name: "Grace Liu", role: "Head of Product, Lumora" },
];
function Testimonials() {
return (
<section id="customers" className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<motion.h2 {...fadeUp} className="max-w-xl text-4xl font-semibold tracking-tight text-zinc-900">Teams that run on Nimbus</motion.h2>
<div className="mt-12 grid gap-5 md:grid-cols-3">
{quotes.map((q, i) => (
<motion.figure key={q.name} {...fadeUp} transition={{ ...fadeUp.transition, delay: i * 0.1 }} className="flex flex-col justify-between rounded-3xl border border-zinc-200 p-8">
<blockquote className="text-zinc-700">“{q.quote}”</blockquote>
<figcaption className="mt-8 flex items-center gap-3">
<span className="grid size-10 place-items-center rounded-full bg-gradient-to-br from-blue-500 to-indigo-600 text-sm font-semibold text-white">
{q.name.split(" ").map((n) => n[0]).join("")}
</span>
<span>
<span className="block text-sm font-medium text-zinc-900">{q.name}</span>
<span className="block text-xs text-zinc-500">{q.role}</span>
</span>
</figcaption>
</motion.figure>
))}
</div>
</div>
</section>
);
}
const plans = [
{ name: "Starter", price: 0, text: "For early-stage teams", features: ["Up to 10k events / mo", "3 dashboards", "Community support"] },
{ name: "Growth", price: 49, text: "For scaling companies", features: ["Up to 5M events / mo", "Unlimited dashboards", "AI forecasting", "Slack alerts"], featured: true },
{ name: "Scale", price: 199, text: "For data-driven orgs", features: ["Unlimited events", "SSO & SCIM", "Dedicated success manager", "99.99% SLA"] },
];
function Pricing() {
return (
<section id="pricing" className="bg-zinc-50 px-6 py-24">
<div className="mx-auto max-w-6xl">
<motion.div {...fadeUp} className="text-center">
<h2 className="text-4xl font-semibold tracking-tight text-zinc-900 sm:text-5xl">Pricing that scales with you</h2>
<p className="mt-4 text-zinc-500">Start free. Upgrade when you need more.</p>
</motion.div>
<div className="mt-14 grid gap-5 lg:grid-cols-3">
{plans.map((p, i) => (
<motion.div key={p.name} {...fadeUp} transition={{ ...fadeUp.transition, delay: i * 0.1 }} className={`flex flex-col rounded-3xl p-8 ${p.featured ? "bg-blue-600 text-white shadow-2xl shadow-blue-600/30" : "border border-zinc-200 bg-white"}`}>
<h3 className="font-semibold">{p.name}</h3>
<p className={`text-sm ${p.featured ? "text-blue-100" : "text-zinc-500"}`}>{p.text}</p>
<p className="mt-6 text-5xl font-semibold tracking-tight">
${p.price}
<span className={`text-base font-normal ${p.featured ? "text-blue-100" : "text-zinc-500"}`}>/mo</span>
</p>
<ul className="mt-8 flex-1 space-y-3 text-sm">
{p.features.map((f) => (
<li key={f} className="flex items-center gap-2.5">
<Check className={`size-4 ${p.featured ? "text-white" : "text-blue-600"}`} />
{f}
</li>
))}
</ul>
<button className={`mt-8 rounded-full py-3 text-sm font-medium transition hover:-translate-y-0.5 ${p.featured ? "bg-white text-blue-700" : "bg-zinc-900 text-white"}`}>
{p.price === 0 ? "Get started" : "Start free trial"}
</button>
</motion.div>
))}
</div>
</div>
</section>
);
}
const faqs = [
{ q: "How long does setup take?", a: "Most teams connect their first source and see dashboards within 10 minutes." },
{ q: "Is my data secure?", a: "Yes. We're SOC 2 Type II certified, encrypt data at rest and in transit, and never sell data." },
{ q: "Can I cancel anytime?", a: "Absolutely. Plans are month-to-month and you can export all your data whenever you like." },
{ q: "Do you offer discounts for startups?", a: "Yes — eligible startups get 50% off the Growth plan for their first year." },
];
function Faq() {
const [open, setOpen] = useState<number | null>(0);
return (
<section id="faq" className="px-6 py-24">
<div className="mx-auto max-w-3xl">
<h2 className="text-center text-4xl font-semibold tracking-tight text-zinc-900">Questions & answers</h2>
<div className="mt-12 divide-y divide-zinc-200 border-y border-zinc-200">
{faqs.map((f, i) => (
<div key={f.q}>
<button onClick={() => setOpen(open === i ? null : i)} className="flex w-full items-center justify-between py-6 text-left font-medium text-zinc-900">
{f.q}
<motion.span animate={{ rotate: open === i ? 45 : 0 }}>
<Plus className="size-5 text-zinc-400" />
</motion.span>
</button>
<AnimatePresence initial={false}>
{open === i && (
<motion.p initial={{ height: 0, opacity: 0 }} animate={{ height: "auto", opacity: 1 }} exit={{ height: 0, opacity: 0 }} className="overflow-hidden pb-6 text-zinc-500">
{f.a}
</motion.p>
)}
</AnimatePresence>
</div>
))}
</div>
</div>
</section>
);
}
function Cta() {
return (
<section className="px-6 pb-24">
<motion.div {...fadeUp} className="relative isolate mx-auto max-w-6xl overflow-hidden rounded-[2rem] bg-zinc-950 px-8 py-20 text-center">
<div aria-hidden className="absolute left-1/2 top-0 -z-10 h-64 w-[40rem] -translate-x-1/2 -translate-y-1/2 rounded-full bg-blue-600/50 blur-3xl" />
<h2 className="mx-auto max-w-2xl text-4xl font-semibold tracking-tight text-white sm:text-5xl">See your growth story in 5 minutes</h2>
<p className="mx-auto mt-5 max-w-lg text-zinc-400">Connect a data source and Nimbus builds your first dashboards automatically.</p>
<a href="#pricing" className="mt-10 inline-flex items-center gap-2 rounded-full bg-white px-7 py-3.5 text-sm font-medium text-zinc-900 transition hover:-translate-y-0.5">
Start free trial <ArrowRight className="size-4" />
</a>
</motion.div>
</section>
);
}
function Footer() {
const cols = {
Product: ["Features", "Integrations", "Pricing", "Changelog"],
Company: ["About", "Careers", "Blog", "Contact"],
Resources: ["Docs", "API reference", "Guides", "Status"],
};
return (
<footer className="border-t border-zinc-100 px-6 py-16">
<div className="mx-auto grid max-w-6xl gap-10 md:grid-cols-[2fr_1fr_1fr_1fr]">
<div>
<span className="flex items-center gap-2 font-semibold text-zinc-900">
<span className="grid size-7 place-items-center rounded-lg bg-blue-600 text-white"><Cloud className="size-3.5" /></span>
Nimbus
</span>
<p className="mt-4 max-w-xs text-sm text-zinc-500">Analytics that explain why. Built for teams that move fast.</p>
</div>
{Object.entries(cols).map(([title, items]) => (
<div key={title}>
<p className="text-sm font-semibold text-zinc-900">{title}</p>
<ul className="mt-4 space-y-2.5 text-sm text-zinc-500">
{items.map((i) => (
<li key={i}><a href="#" className="hover:text-zinc-900">{i}</a></li>
))}
</ul>
</div>
))}
</div>
<p className="mx-auto mt-14 max-w-6xl text-xs text-zinc-400">© {new Date().getFullYear()} Nimbus Analytics, Inc. All rights reserved.</p>
</footer>
);
}
export default function NimbusTemplate() {
return (
<div className="min-h-screen bg-white font-sans text-zinc-900 antialiased">
<Nav />
<main>
<Hero />
<Logos />
<Features />
<Product />
<Testimonials />
<Pricing />
<Faq />
<Cta />
</main>
<Footer />
</div>
);
}
What's included
- Navbar
- Hero
- Logos
- Features
- Product tabs
- Testimonials
- Pricing
- FAQ
- CTA
- 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.