Skip to content
Chapter 28Lesson 1

Project overview

Every B2B SaaS ships the same page before anyone logs in: a header with a logo and nav, a hero with a headline and two call-to-action buttons, a three-column feature grid, a pricing table, a footer, and — below the md breakpoint — a nav drawer that slides in from the side. It is the most-viewed page the company owns and the first thing its design system is judged against. You will build it from empty component files up, cashing out the React, JSX, and Tailwind work at once on a single page where one wrong token or one keyboard trap is visible to anyone who opens it.

The scope is deliberately narrow. It is a static page: no real auth behind the “Get started” buttons, no CMS, no analytics, and no animation beyond what shadcn and tw-animate-css already give you. The copy and pricing numbers live in one typed file, src/lib/data.ts, so components stay about layout and tokens rather than hard-coded strings. Nothing on this page talks to a server.

Three commitments run through the chapter:

  • Accessibility is verified, not promised. The bar is a Lighthouse accessibility score of 100 and keyboard-only traversal that reaches every control. A 99, or one place where focus gets stuck, is a failure.
  • Motion comes for free. tw-animate-css and shadcn’s defaults already animate the drawer and respect prefers-reduced-motion. You do not hand-build transitions here.
  • The drawer is shadcn’s Sheet . The Sheet primitive owns the focus trap; the only custom code it needs is one project-owned hook, useLockBodyScroll.

This is the from-scratch toolchain project. The starter is a Next.js 16 app, already scaffolded and installed, but unlike later project chapters it was not cloned from a previous repo. Because it lays the foundation those chapters carry forward, the next four lessons walk through the provided toolchain — pnpm, AGENTS.md, tsconfig, and Biome — before you write a line of UI.

The finished surface at desktop width: sticky header, hero with two CTAs, three-column feature grid, pricing table with the featured Pro tier, and footer.

This is where the React, JSX, and Tailwind skills stop being separate exercises and meet on one page: shadcn primitives instead of raw elements, a card’s tone driven from data through a cva table, semantic tokens instead of literal colors, flex and Grid layout across breakpoints, a next-themes toggle that paints the right theme on the first frame, and an accessibility bar you verify with the keyboard and Lighthouse.

The other half is reading a real toolchain critically. By the end you will know why this project pins pnpm and commits its lockfile, what earns a place in an AGENTS.md, the two halves of a tsconfig, and the floor a single Biome config sets for formatting and linting, all read off the config files the starter already ships.

The shape is small because the page is static. Read as roles:

  • The route — a single App Router page at src/app/page.tsx. It composes the sections in order and owns the <main> landmark wrapping the three middle ones.
  • The sectionsSiteHeader, Hero, FeatureGrid, PricingTable, and SiteFooter in src/components/. Header and footer carry their own <header> and <footer> landmarks; the page supplies the <main> between them.
  • The primitives — shadcn’s Button, Badge, Card, Sheet, Separator, and Skeleton in src/components/ui/, composed rather than reinstalled.
  • The content — typed copy and pricing in src/lib/data.ts: nav links, features, pricing tiers, footer groups, and social links, all as typed arrays.
  • The one hookuseLockBodyScroll in src/hooks/, the only custom behavior the project owns.
  • The themingnext-themes flips a .dark class on <html> from a <ThemeProvider> in src/app/_components/providers.tsx. The OKLCH tokens and the @theme inline block that maps them to Tailwind utilities live in src/app/globals.css.

What is not here is as telling as what is: no data fetching, no auth, no server state, no database. Every bit of complexity on this page is layout, tokens, and one client-side interaction.

The starter is fully scaffolded: every config file, shadcn primitive, the stylesheet, layout, page, theme provider, and typed data are in place. You write only the eleven highlighted files — the section components and one hook — each shipped as a stub with the right signature and a TODO(Ln) comment naming the lesson that fills it in. The annotations below mark only files a later lesson walks through or that your components import; anything unannotated is plumbing you can take for granted.

  • .mise.toml pins Node 24 and pnpm 11.3.0 — next lesson
  • .npmrc pnpm settings — next lesson
  • pnpm-workspace.yaml allows the sharp native build — next lesson
  • pnpm-lock.yaml the committed lockfile — next lesson
  • package.json scripts, pinned package manager and Node engine — next lesson
  • AGENTS.md the repo’s onboarding briefing — its own lesson
  • tsconfig.json strictness floor plus Next.js compatibility — its own lesson
  • biome.json formatter and linter config — its own lesson
  • next.config.ts
  • postcss.config.mjs
  • components.json
  • vitest.config.ts
  • .editorconfig
  • next-env.d.ts
  • Directoryscripts/
    • test-lesson.mjs
  • Directorytests/
    • Directorylessons/
  • Directorypublic/
    • hero-light.png the light-mode hero image
    • hero-dark.png the dark-mode hero image
    • logo.svg
  • Directorysrc/
    • Directoryapp/
      • layout.tsx root shell — lang, fonts, and the <Providers> wrapper
      • page.tsx composes the sections inside min-h-dvh with the <main> landmark
      • globals.css OKLCH tokens (light and .dark) and the @theme inline map
      • Directory_components/
        • providers.tsx the next-themes <ThemeProvider>
    • Directorycomponents/
      • Directoryui/ provided shadcn primitives — button, badge, card, sheet, separator, skeleton
      • site-header.tsx
      • hero.tsx
      • theme-aware-image.tsx
      • feature-card.tsx
      • feature-grid.tsx
      • pricing-card.tsx
      • pricing-table.tsx
      • site-footer.tsx
      • theme-toggle.tsx
      • mobile-nav.tsx
    • Directoryhooks/
      • use-lock-body-scroll.ts
    • Directorylib/
      • data.ts typed copy and pricing fixtures
      • utils.ts the cn() class-merge helper

Two structural facts to note. There is no (marketing) route group — the page is src/app/page.tsx directly. And there is no standalone dialog.tsx: the mobile drawer’s Sheet wraps Radix’s Dialog internally, so the dialog behavior is already there.

Eleven lessons turn the empty scaffold into the running page. The first four read the toolchain; the rest each build one confirmable piece of the surface.

pnpm and the lockfile contract

The pnpm toolchain: the version pinned through mise, the committed lockfile as a deterministic contract, and the guard against mixing package managers.

AGENTS.md as the next contributor's briefing

What earns a place in the onboarding file — thesis, pinned stack, layout, commands, conventions — and what does not.

Configuring tsconfig

tsconfig.json in two halves: the project-owned strictness floor and the Next.js-owned compatibility surface.

Biome, the single-binary linter and formatter

Why Biome replaces ESLint plus Prettier: the biome.json, the daily check and verify scripts, and safe versus unsafe fixes.

Site header with desktop navigation

The semantic sticky <header> with logo, desktop nav, and slots for the theme toggle and mobile drawer.

Hero with a flicker-free theme-aware image

The single-<h1> hero with two CTAs and a light/dark image swapped by CSS, with no flash on load.

Feature grid with CVA card variants

A responsive three-column grid whose card tone and emphasis come from data through a cva table.

Pricing table with a featured tier

A data-driven pricing row with one promoted tier and a lift that respects reduced motion.

Site footer

The footer landmark: brand block, three link-group navs, and labelled icon buttons for social links.

Flicker-free theme toggle

The next-themes sun/moon toggle with a CSS-only icon swap and no mount gate.

Mobile nav drawer

The shadcn Sheet drawer with its focus trap, the useLockBodyScroll hook, and Esc-to-close.

This project starts from the toolchain, not a feature, so setup is short: the starter ships scaffolded and installed, and the dev server serves the page shell as soon as it boots.

  1. Get the starter codebase from the project repository, under Chapter 028/start/.

  2. You already have mise from earlier in the course. The starter’s .mise.toml pins both Node 24 and pnpm 11.3.0, so when you cd into the project, mise activates the right versions automatically — nothing global to install.

  3. Install dependencies. This resolves against the committed lockfile and populates node_modules.

    Terminal window
    pnpm install
  4. Start the dev server.

    Terminal window
    pnpm dev

There are no environment variables to set; the page is fully static.

Open the printed local URL. The layout, global styles, and page frame render, but the eleven section components are still empty stubs, so the page is mostly bare scaffolding. That is the expected starting state, and the lessons ahead fill the stubs in one confirmable piece at a time.