A course sales page in Next.js 16 with zero UI dependencies
A good course sales page is mostly conversion mechanics: a sticky call-to-action, a pricing toggle, and accordions for the curriculum and FAQ. I wanted to build all of that with no UI libraries — just Next.js 16, TypeScript and CSS Modules. The result is Scholar, a 3-page template. Here are the techniques.
The stack
-
Next.js 16 (App Router) —
/,/curriculum,/checkout - TypeScript + CSS Modules
-
next/font— Fraunces (display) + Inter (body) - No other runtime dependencies.
1. A sticky CTA bar that appears after the hero
A simple scroll listener toggles a class once you've scrolled past the hero; CSS handles the slide-in with a transform.
useEffect(() => {
const onScroll = () => setShow(window.scrollY > 680);
window.addEventListener("scroll", onScroll, { passive: true });
return () => window.removeEventListener("scroll", onScroll);
}, []);
.bar { transform: translateY(110%); transition: transform .35s cubic-bezier(.22,1,.36,1); }
.show { transform: translateY(0); }
It lives in app/layout.tsx, so it's present on every page without re-mounting.
2. A live pricing toggle
One piece of state drives the whole pricing section. Each tier stores both prices, and the toggle just picks the key:
price: { oneTime: "$149", installments: "$55" }
const [mode, setMode] = useState<"oneTime" | "installments">("oneTime");
// ...
<strong>{tier.price[mode]}</strong>
The same pattern powers the checkout's plan selector — change the plan or billing mode and the order summary recalculates instantly. No library, no context.
3. Accordions with CSS grid-rows
Animating to height: auto is the classic pain. Animating CSS grid rows from 0fr to 1fr solves it cleanly, and I reused the exact pattern for both the FAQ and the curriculum modules:
.bodyWrap { display: grid; grid-template-rows: 0fr; transition: grid-template-rows .3s ease; }
.open .bodyWrap { grid-template-rows: 1fr; }
The inner element only needs overflow: hidden.
4. A checkout that's ready to wire up
The checkout is a self-contained React form: plan radios, billing toggle, a sticky live order summary, and a success state on submit. Because the pricing data lives in constants.ts, mapping tiers to Stripe/Gumroad/Lemon Squeezy product IDs is a one-line change.
5. Everything in one file
src/lib/constants.ts holds the copy, the 6-module / 48-lesson curriculum, pricing, and FAQ. Components stay presentational. Rebranding is editing data, not JSX.
Wrapping up
You don't need a component library to build a polished, converting sales page — App Router, a little state and CSS grid go a long way. I packaged this as Scholar (link below) if you'd rather start from a finished base.
Get Scholar → https://devmaya.gumroad.com/l/scholar
Top comments (0)