MKT UI

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>
  • Section owns the background and the vertical rhythm. It runs edge to edge.
  • Container owns 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

OrderSectionWhat it answers
1site-headerWhere am I, and how do I get in touch fast?
2hero-splitWhat do you do, for whom, and what should I do next?
3services-icon-gridWhat specifically can you help me with?
4ProofWhy should I believe you?
5quote-request-formHow do I get started?
6cta-bandFinal nudge for anyone who scrolled past
7site-footerContact 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.

examples/data/home-service.ts
export const homeServices = [
  { id: "emergency", title: "Emergency repairs", description: "…" },
];
examples/recipes/home-service-home.tsx
<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.

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.

app/page.tsx
// 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.

On this page