Composition

Guide

How layout primitives nest to build every page and section.

The design system has a strict layout hierarchy. Using the right component at each level gives you correct spacing, responsive behaviour, and semantic HTML — without arbitrary classes.

The Hierarchy

Six layers, each with a single responsibility. Each layer wraps the next. Never skip layers or substitute raw divs.

Pagemin-h-screen · bg-background · once per route
Sectionfull-width band · vertical padding (py) · optional background
Containermax-width · centered · horizontal gutters (px)
Stack / Gridtoken gaps · vertical or grid layout for children
Surfacecard or panel · tone · radius · border · padding

Your content

Heading, Text, Button, Badge, Inline…

Section

Provides vertical padding only. Never sets max-width or horizontal padding. Full-width by design.

Container

Provides horizontal constraint only. Never adds vertical padding. Never nest Container inside Container.

Surface

A card or panel, not a page band. For full-width backgrounds use Section, not Surface.

Stack

Does not wrap. Single-axis flex layout with token gaps. Use direction="horizontal" for rows.

Inline

Does wrap. Horizontal flex-wrap for buttons, badges, and chips. Contrast with Stack.

Grid

Multi-column layout with responsive column collapse. Use instead of Stack horizontal when mobile layout matters.

Decision Tree

Start here before writing any JSX. Match your need to the right component.

Layout

Full-width page shell (once per route)

Page

<main>

Full-width band with vertical padding + background

Section

<div className='py-16 bg-muted'>

Max-width constraint + horizontal gutters

Container

<div className='max-w-6xl mx-auto px-6'>

Vertical list of elements with gaps

Stack

<div className='flex flex-col gap-4'>

Horizontal non-wrapping row

Stack direction='horizontal'

<div className='flex gap-4'>

Wrapping horizontal items (buttons, tags)

Inline

<div className='flex flex-wrap gap-2'>

Responsive multi-column grid

Grid

<div className='grid grid-cols-3'>

60/40 or 40/60 split layout

Grid cols='2-asymmetric'

Card, panel, or boxed container

Surface

<div className='rounded-xl bg-muted p-6'>

Typography

Page or section title

Heading as='h1|h2|h3' size='...'

<h2 className='text-2xl'>

Body text

Text

<p className='text-sm'>

Secondary/supporting text

Text variant='muted'

<p className='text-muted-foreground'>

Small or caption text

Text size='s' or size='caption'

Interactive

Primary action

Button

<button className='bg-primary'>

Status label or tag

Badge

<span className='rounded-full bg-muted px-2'>

Text input

Input + Label

Glass button on imagery

Button variant='glass'

Live Examples

Common patterns built from the hierarchy. Copy and adapt.

Standard Content Section

The most common pattern. Section → Container → Stack → content.

Better Air. Everyday.

An intelligent home climate system that delivers heating, cooling, and purification.

Alternating Section Backgrounds

Use Section background prop — never raw bg-* on a div — to create visual rhythm between sections.

Default background

Muted background (Section background="muted")

Default again

Feature Grid

Section → Container → Stack (for header) + Grid → Surface → Stack → content.

Why Everyday

Air Quality

Medical-grade HEPA filtration for clean air.

Smart Control

Adaptive scheduling with real-time sensors.

Sustainability

Up to 70% less energy than fossil-fuel systems.

Inline vs Stack — The Wrapping Rule

Stack never wraps. Inline always wraps. This is the key distinction.

Inline — wraps
Air Quality
HVAC
Smart Home
Sustainability
Energy
Design
Stack direction="horizontal" — no wrap

Anti-Patterns

Common shortcuts that bypass the system. Each one has a correct equivalent.

avoid

<div className="py-16 bg-muted">
  <div className="max-w-6xl mx-auto px-6">
    <h2 className="text-2xl font-bold">Title</h2>
  </div>
</div>

prefer

<Section spacing="lg" background="muted">
  <Container size="xl">
    <Heading as="h2" size="l">Title</Heading>
  </Container>
</Section>

Section + Container replace manual py-*, bg-*, max-w-*, mx-auto, and px-* classes entirely.

avoid

<div className="flex flex-col gap-6">
  <h2>Title</h2>
  <p>Body text</p>
</div>

prefer

<Stack spacing="lg">
  <Heading as="h2" size="l">Title</Heading>
  <Text variant="muted">Body text</Text>
</Stack>

Stack replaces div flex-col gap-*. It also ensures token-backed gaps instead of arbitrary values.

avoid

<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
  <div className="rounded-xl bg-muted p-6">…</div>
  <div className="rounded-xl bg-muted p-6">…</div>
</div>

prefer

<Grid cols="3" gap="md">
  <Surface tone="muted" radius="sm" padding="md">…</Surface>
  <Surface tone="muted" radius="sm" padding="md">…</Surface>
</Grid>

Grid + Surface replace manual grid-cols-*, gap-*, rounded-*, bg-*, and p-* classes.

avoid

<div className="flex flex-wrap gap-2">
  <span className="px-3 py-1 rounded-full bg-muted text-sm">Tag 1</span>
  <span className="px-3 py-1 rounded-full bg-muted text-sm">Tag 2</span>
</div>

prefer

<Inline spacing="xs">
  <Badge variant="secondary">Tag 1</Badge>
  <Badge variant="secondary">Tag 2</Badge>
</Inline>

Inline + Badge replace flex-wrap divs with manual pill styling.

Block Composition

How marketing "blocks" map to the layout hierarchy. Each block is a Section with Container inside — not a standalone styled component.

BlockRenderer pattern (marketing)

// BlockRenderer maps CMS sections → layout hierarchy
<Page>
  {sections.map((section) => (
    <Section key={section._key} spacing={section.spacing}>
      {section.fullBleed ? (
        // Full-bleed: no Container — content touches edges
        <ContentBlock section={section} />
      ) : (
        // Constrained: Container inside Section
        <Container size="xl">
          <ContentBlock section={section} />
        </Container>
      )}
    </Section>
  ))}
</Page>

// ContentBlock arranges content using Grid + Stack + Surface
function ContentBlock({ section }) {
  return (
    <Grid cols={section.cols ?? "2"} gap="lg">
      {section.cells.map((cell) => (
        <Stack key={cell._key} spacing="md">
          <Heading as="h2" size="l">{cell.title}</Heading>
          <Text variant="muted">{cell.body}</Text>
        </Stack>
      ))}
    </Grid>
  );
}

Every block is Page → Section → Container → Grid/Stack → content. The CMS controls content; the code controls structure.