DEV Community

Dev Maya
Dev Maya

Posted on

A course sales page in Next.js 16 with zero UI dependencies (sticky CTA, pricing toggle, accordions)

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);
}, []);
Enter fullscreen mode Exit fullscreen mode
.bar { transform: translateY(110%); transition: transform .35s cubic-bezier(.22,1,.36,1); }
.show { transform: translateY(0); }
Enter fullscreen mode Exit fullscreen mode

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" }
Enter fullscreen mode Exit fullscreen mode
const [mode, setMode] = useState<"oneTime" | "installments">("oneTime");
// ...
<strong>{tier.price[mode]}</strong>
Enter fullscreen mode Exit fullscreen mode

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; }
Enter fullscreen mode Exit fullscreen mode

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)