# Layout

Portable primitives for page width, rhythm, alignment, grids, and surfaces.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Installation [#installation]

<CodeTabs>
  <TabsList>
    <TabsTrigger value="cli">
      Command
    </TabsTrigger>

    <TabsTrigger value="manual">
      Manual
    </TabsTrigger>
  </TabsList>

  <TabsContent value="cli">
    <CodeBlockCommand __bun__="bunx --bun shadcn@latest add https://lab.pratikthapw.dev/r/layout.json" __npm__="npx shadcn@latest add https://lab.pratikthapw.dev/r/layout.json" __pnpm__="pnpm dlx shadcn@latest add https://lab.pratikthapw.dev/r/layout.json" __yarn__="npx shadcn@latest add https://lab.pratikthapw.dev/r/layout.json" />
  </TabsContent>

  <TabsContent value="manual">
    <Steps className="mb-0 pt-2">
      <Step>
        Install the following dependencies:
      </Step>

      ```bash
      npm install class-variance-authority clsx tailwind-merge
      ```

      <Step>
        Copy and paste the following code into your project.
      </Step>

      <ComponentSource name="layout" title="components/ui/layout.tsx" />

      <ComponentSource name="tokens" title="lib/tokens.ts" />

      <Step>
        Update the import paths to match your project setup.
      </Step>
    </Steps>
  </TabsContent>
</CodeTabs>

## Usage [#usage]

```tsx
import { Box, Container, Grid, Section, Stack } from "@/components/ui/layout";

export function FeatureSection() {
  return (
    <Section spacing="12">
      <Container size="lg">
        <Stack gap="6">
          <h2 className="text-3xl font-semibold tracking-tight">Features</h2>
          <Grid cols={3} gap="4">
            <Box padding="6" radius="lg" surface="card">
              One clear region
            </Box>
          </Grid>
        </Stack>
      </Container>
    </Section>
  );
}
```

Spacing props take numeric token steps (`"0"` to `"24"`, including halves like `"2.5"`); each step compiles to its matching Tailwind utility, so `gap="12"` renders `gap-12`. The scale ships in `lib/tokens.ts` — see [Theming](/docs/theming) for the full contract.

## Composition [#composition]

Use the following composition to build a page:

```text
Shell
└── Section
    └── Container
        └── Flex (engine)
            ├── Stack (direction="col")
            ├── Cluster (direction="row", wraps)
            ├── Center (both | horizontal | vertical)
            ├── Spacer (grow)
            └── Grid (2D)
                └── Box (surface)
```

Section owns the surface and vertical rhythm. Container owns width. Flex owns one-dimensional distribution; Stack and Cluster are its semantic aliases. Box owns padding, radius, and surface color. The page owns content, semantics (`as`), and one-off overrides (`className`).

## Examples [#examples]

### Dashboard regions [#dashboard-regions]

Compose named relationships instead of repeating one-off flex and grid arrangements throughout a product.

<ComponentPreview name="layout-dashboard">
  <LayoutDashboardExample />
</ComponentPreview>

### Page shell [#page-shell]

Sections own vertical rhythm, containers own width and gutters, and boxes own bounded surfaces.

<ComponentPreview name="layout-page">
  <LayoutPageExample />
</ComponentPreview>

### Marketing block [#marketing-block]

The canonical marketing shape: a centered Stack header over a Grid body, repeated down the page with alternating surfaces.

<ComponentPreview name="layout-marketing">
  <LayoutMarketingExample />
</ComponentPreview>

### Split-screen auth [#split-screen-auth]

A Cluster with `gap="0"` and `align="stretch"` splits the viewport; the fluid half uses `pageX="mobile-only"` to keep gutters on mobile and defer to the split on desktop.

<ComponentPreview name="layout-auth-split">
  <LayoutAuthSplitExample />
</ComponentPreview>

### Site header [#site-header]

`Section as="nav"` with a Spacer between the two ends of a Cluster. The Spacer creates the non-uniform distribution a gap cannot.

<ComponentPreview name="layout-site-header">
  <LayoutSiteHeaderExample />
</ComponentPreview>

## API [#api]

Spacing props (`gap`, `gapX`, `gapY`, `spacing`, `padding`) accept a `SpacingKey`: the token steps `"0"`, `"0.5"`, `"1"`, `"2"`, `"2.5"`, `"3"`, `"4"`, `"5"`, `"6"`, `"7"`, `"8"`, `"9"`, `"10"`, `"12"`, `"14"`, `"16"`, `"20"`, `"24"`.

All polymorphic primitives accept `as` to render a different element.

### Shell [#shell]

| Prop    | Type                                   | Default  |
| ------- | -------------------------------------- | -------- |
| `pageX` | `"default" \| "mobile-only" \| "none"` | `"none"` |
| `pageY` | `"default" \| "small" \| "none"`       | —        |

`Shell` renders an element owning the minimum page height (`min-h-dvh`) and the base surface. It stays unpadded by default because `Container` owns gutters; use `pageX`/`pageY` when a pane needs its own page-level padding. Use `Center` to center content inside it — there is no centered variant.

### Section [#section]

| Prop      | Type                                                             | Default     |
| --------- | ---------------------------------------------------------------- | ----------- |
| `surface` | `"default" \| "inverted" \| "muted" \| "primary" \| "secondary"` | `"default"` |
| `spacing` | `SpacingKey`                                                     | `"0"`       |

### Container [#container]

| Prop    | Type                                     | Default    |
| ------- | ---------------------------------------- | ---------- |
| `size`  | `"sm" \| "md" \| "lg" \| "xl" \| "full"` | `"xl"`     |
| `align` | `"center" \| "left" \| "right"`          | `"center"` |

`Container` owns responsive gutters (`px-4 sm:px-6 lg:px-8`) in addition to width.

### Flex, Stack, Cluster [#flex-stack-cluster]

| Prop        | Type                                                                | Default     |
| ----------- | ------------------------------------------------------------------- | ----------- |
| `direction` | `"col" \| "row"` (Flex only)                                        | `"col"`     |
| `align`     | `"start" \| "center" \| "end" \| "baseline" \| "stretch"`           | `"stretch"` |
| `justify`   | `"start" \| "center" \| "end" \| "between" \| "around" \| "evenly"` | `"start"`   |
| `wrap`      | `boolean`                                                           | `false`     |
| `gap`       | `SpacingKey`                                                        | `"4"`       |

`Stack` is `Flex` with `direction="col"`; `Cluster` is `Flex` with `direction="row"` and `wrap` on.

### Center [#center]

| Prop      | Type                                   | Default  |
| --------- | -------------------------------------- | -------- |
| `variant` | `"both" \| "horizontal" \| "vertical"` | `"both"` |

Center works at any depth; it is not limited to page-level centering.

### Spacer [#spacer]

| Prop   | Type      | Default |
| ------ | --------- | ------- |
| `grow` | `boolean` | `true`  |

Spacer is `aria-hidden` and never receives content.

### Grid [#grid]

| Prop   | Type         | Default |
| ------ | ------------ | ------- |
| `cols` | `1`–`6`      | `3`     |
| `gap`  | `SpacingKey` | `"4"`   |
| `gapX` | `SpacingKey` | —       |
| `gapY` | `SpacingKey` | —       |

Column counts are responsive presets: `cols={3}` compiles to `grid-cols-1 sm:grid-cols-2 lg:grid-cols-3`. Grid does not own its children; the children decide their own sizing.

### Box [#box]

| Prop      | Type                                                                      | Default  |
| --------- | ------------------------------------------------------------------------- | -------- |
| `padding` | `SpacingKey`                                                              | `"0"`    |
| `radius`  | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "3xl" \| "4xl"`       | `"none"` |
| `surface` | `"none" \| "background" \| "card" \| "muted" \| "primary" \| "secondary"` | `"none"` |
