Composition
Assembling sections into a page that reads as one website.
Sections are designed to be stacked. The rules below are what keep a page assembled from six of them looking designed rather than collaged.
Section and Container do different jobs
<Section surface="muted">
<Container>
<SectionHeading title="What we do" />
</Container>
</Section>Sectionowns the background and the vertical rhythm. It runs edge to edge.Containerowns the max width and the horizontal gutter.
Keeping them apart is what lets a tinted band span the full viewport while its text
still lines up with the section above it. A section that sets its own horizontal
padding instead of using a Container will drift out of alignment with everything
else on the page — this is the single most common way a marketing page starts to look
slightly wrong without anyone being able to say why.
Heading hierarchy
Every page needs exactly one h1 and no skipped levels. SectionHeading separates
the two concerns:
{/* Hero: the page's h1 */}
<HeroSplit title="Plumbing problems fixed properly" />
{/* Later sections: h2, whatever size they render at */}
<SectionHeading as="h2" size="default" title="What we do" />
{/* A sub-heading inside a section */}
<SectionHeading as="h3" size="compact" title="Emergency call-outs" />as controls the document outline; size controls how big it looks. They are separate
because a hero on a sub-page often needs to look like a display headline while still
being the page's h1, and a large mid-page heading must stay an h2 however big it
renders.
Alternating surfaces
Alternate default and muted to give a long page a rhythm, and save brand or
inverted for the moment you want someone to act:
<HeroSplit … /> {/* default */}
<ServicesIconGrid surface="muted" … /> {/* muted */}
<Section>…</Section> {/* default */}
<CtaBand surface="inverted" … /> {/* inverted */}
<SiteFooter … />Two inverted bands on one page cancel each other out — if everything is emphasized,
nothing is.
A typical service-business page
| Order | Section | What it answers |
|---|---|---|
| 1 | site-header | Where am I, and how do I get in touch fast? |
| 2 | hero-split | What do you do, for whom, and what should I do next? |
| 3 | services-icon-grid | What specifically can you help me with? |
| 4 | Proof | Why should I believe you? |
| 5 | quote-request-form | How do I get started? |
| 6 | cta-band | Final nudge for anyone who scrolled past |
| 7 | site-footer | Contact details, hours, and everything else |
This is a useful default, not a law. A business whose customers already know the category can lead with proof; one selling something unfamiliar may need explanation before anything else.
Keeping copy out of components
The example websites keep every string in examples/data/ and every composition in
examples/recipes/. Route files stay thin.
export const homeServices = [
{ id: "emergency", title: "Emergency repairs", description: "…" },
];<ServicesIconGrid services={homeServices.map((s) => ({ ...s, icon: icons[s.icon] }))} />Icons stay out of the data file so the content remains plain serialisable data. It can then come from a CMS without changing the section at all.
Anchor links and sticky headers
site-header is 4rem tall and matches --mkt-header-height, which
scroll-padding-top uses. That is what stops an in-page anchor from landing with its
heading hidden behind the header. If you change the header height, update the token:
:root {
--mkt-header-height: 5rem;
}Server and client boundaries
Most sections render on the server and ship no JavaScript. site-header and the forms
carry their own "use client" directive, so you can place them in a server component
without wrapping anything.
// No "use client" needed here.
export default function Page() {
return (
<>
<SiteHeader … /> {/* client boundary is inside the section */}
<HeroSplit … /> {/* server-rendered */}
</>
);
}One caveat: onSubmit is a function, and functions cannot cross the server/client
boundary. A page that renders a form must either be a client component itself or pass
the handler from one.
Testing what you compose
Before shipping a page, check the things that actually break:
- View it at 375px. Any horizontal scrollbar is a bug.
- Tab through it. Every interactive element should be reachable, with visible focus.
- Replace the copy with something twice as long. Headings should wrap, not overflow.
- Remove the optional images. Sections should still look deliberate.