For the complete documentation index, see 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.

Layout

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

Installation

$ pnpm dlx shadcn@latest add https://lab.pratikthapw.dev/r/layout.json

Usage

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 for the full contract.

Composition

Use the following composition to build a page:

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

Dashboard regions

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

Overview

A responsive three-region summary.

Live
Revenue
Orders
Customers
import {
  Box,
  Cluster,
  Container,
  Grid,
  Section,
  Stack,
} from "@/components/ui/layout";

const cards = ["Revenue", "Orders", "Customers"] as const;

export const LayoutDashboardExample = () => (
  <Section
    className="border-border overflow-hidden rounded-xl border"
    spacing="10"
    surface="muted"
  >
    <Container size="lg">
      <Stack gap="6">
        <Cluster align="center" gap="2" justify="between">
          <Stack gap="1">
            <h2 className="text-xl font-semibold tracking-tight">Overview</h2>
            <p className="text-muted-foreground text-sm">
              A responsive three-region summary.
            </p>
          </Stack>
          <span className="bg-primary text-primary-foreground rounded-md px-3 py-1.5 text-xs font-medium">
            Live
          </span>
        </Cluster>
        <Grid cols={3} gap="4">
          {cards.map((card) => (
            <Box
              key={card}
              padding="6"
              radius="lg"
              surface="card"
              className="border-border min-h-28 border"
            >
              <span className="text-sm font-medium">{card}</span>
            </Box>
          ))}
        </Grid>
      </Stack>
    </Container>
  </Section>
);

Page shell

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

Product header

Readable content width

Sections own vertical rhythm. Containers own width and gutters. Boxes own bounded surfaces.
import {
  Box,
  Container,
  Section,
  Shell,
  Stack,
} from "@/components/ui/layout";

export const LayoutPageExample = () => (
  <Shell className="border-border min-h-0 overflow-hidden rounded-xl border">
    <Section spacing="4" surface="inverted">
      <Container size="lg">
        <span className="text-sm font-medium">Product header</span>
      </Container>
    </Section>
    <Section spacing="14">
      <Container size="md">
        <Stack gap="6">
          <h2 className="text-2xl font-semibold tracking-tight">
            Readable content width
          </h2>
          <Box
            padding="6"
            radius="lg"
            surface="card"
            className="border-border border"
          >
            Sections own vertical rhythm. Containers own width and gutters.
            Boxes own bounded surfaces.
          </Box>
        </Stack>
      </Container>
    </Section>
  </Shell>
);

Marketing block

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

Why this family

One block, every marketing page

A Stack header over a Grid body — the shape repeats down the whole page.

ComposableNamed relationships replace one-off flex and grid arrangements.
Token-drivenSpacing and radius flow through numeric token steps, not raw utilities.
PortableOne family covers marketing pages, dashboards, and apps.
import {
  Box,
  Container,
  Grid,
  Section,
  Stack,
} from "@/components/ui/layout";

const benefits = [
  {
    description:
      "Named relationships replace one-off flex and grid arrangements.",
    title: "Composable",
  },
  {
    description:
      "Spacing and radius flow through numeric token steps, not raw utilities.",
    title: "Token-driven",
  },
  {
    description: "One family covers marketing pages, dashboards, and apps.",
    title: "Portable",
  },
] as const;

export const LayoutMarketingExample = () => (
  <Section
    className="border-border overflow-hidden rounded-xl border"
    spacing="16"
    surface="muted"
  >
    <Container size="xl">
      <Stack align="center" className="mx-auto max-w-xl text-center" gap="3">
        <span className="text-primary text-xs font-semibold tracking-widest uppercase">
          Why this family
        </span>
        <h2 className="text-2xl font-semibold tracking-tight sm:text-3xl">
          One block, every marketing page
        </h2>
        <p className="text-muted-foreground text-sm">
          A Stack header over a Grid body — the shape repeats down the whole
          page.
        </p>
      </Stack>
      <Grid className="mt-10" cols={3} gap="6">
        {benefits.map((benefit) => (
          <Box
            key={benefit.title}
            padding="6"
            radius="xl"
            surface="card"
            className="flex flex-col gap-2"
          >
            <span className="text-sm font-semibold">{benefit.title}</span>
            <span className="text-muted-foreground text-sm leading-6">
              {benefit.description}
            </span>
          </Box>
        ))}
      </Grid>
    </Container>
  </Section>
);

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.

Welcome back
The fluid half keeps gutters on mobile and defers to the split on desktop.
import {
  Center,
  Cluster,
  Section,
  Shell,
  Stack,
} from "@/components/ui/layout";

const stats = [
  { label: "Stories", value: "120+" },
  { label: "Rating", value: "4.9" },
  { label: "Readers", value: "12k" },
] as const;

export const LayoutAuthSplitExample = () => (
  <Section
    className="border-border overflow-hidden rounded-xl border"
    spacing="0"
  >
    <Cluster align="stretch" className="min-h-96" gap="0">
      <Section className="hidden w-2/5 lg:block" spacing="0" surface="primary">
        <Center className="min-h-full">
          <Stack align="center" className="px-10 text-center" gap="4">
            <span className="text-2xl font-semibold tracking-tight">
              StoryWonder
            </span>
            <p className="text-primary-foreground/80 max-w-xs text-sm leading-6">
              Personalized storybooks where the layout never competes with the
              story.
            </p>
            <Cluster className="pt-6" gap="8">
              {stats.map((stat) => (
                <Stack align="center" key={stat.label} gap="0">
                  <span className="text-xl font-semibold">{stat.value}</span>
                  <span className="text-primary-foreground/70 text-xs">
                    {stat.label}
                  </span>
                </Stack>
              ))}
            </Cluster>
          </Stack>
        </Center>
      </Section>
      <Shell as="div" className="flex-1" pageX="mobile-only">
        <Center className="min-h-full">
          <Stack className="w-full max-w-sm px-6 py-12 lg:px-0" gap="4">
            <span className="text-lg font-semibold tracking-tight">
              Welcome back
            </span>
            <div className="bg-muted h-9 rounded-md" />
            <div className="bg-muted h-9 rounded-md" />
            <button
              className="bg-primary text-primary-foreground hover:bg-primary/90 h-9 rounded-md text-sm font-medium"
              type="button"
            >
              Sign in
            </button>
            <span className="text-muted-foreground text-center text-xs">
              The fluid half keeps gutters on mobile and defers to the split on
              desktop.
            </span>
          </Stack>
        </Center>
      </Shell>
    </Cluster>
  </Section>
);

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

Page content
import {
  Cluster,
  Container,
  Section,
  Spacer,
} from "@/components/ui/layout";

const links = ["Product", "Stories", "Pricing", "About"] as const;

export const LayoutSiteHeaderExample = () => (
  <div className="border-border overflow-hidden rounded-xl border">
    <Section as="nav" className="border-border border-b" spacing="4">
      <Container size="xl">
        <Cluster align="center" className="flex-nowrap" gap="6">
          <span className="flex items-center gap-2 text-sm font-semibold">
            <span className="bg-primary size-4 rounded-sm" />
            Acme
          </span>
          <Cluster as="ul" className="flex-nowrap" gap="5">
            {links.map((link) => (
              <li className="text-muted-foreground text-sm" key={link}>
                {link}
              </li>
            ))}
          </Cluster>
          <Spacer />
          <Cluster className="flex-nowrap" gap="3">
            <button
              className="hover:text-foreground text-muted-foreground text-sm font-medium"
              type="button"
            >
              Sign in
            </button>
            <button
              className="bg-primary text-primary-foreground hover:bg-primary/90 rounded-md px-3 py-1.5 text-sm font-medium"
              type="button"
            >
              Get started
            </button>
          </Cluster>
        </Cluster>
      </Container>
    </Section>
    <Section className="text-muted-foreground py-20 text-center text-sm">
      Page content
    </Section>
  </div>
);

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

PropTypeDefault
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

PropTypeDefault
surface"default" | "inverted" | "muted" | "primary" | "secondary""default"
spacingSpacingKey"0"

Container

PropTypeDefault
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

PropTypeDefault
direction"col" | "row" (Flex only)"col"
align"start" | "center" | "end" | "baseline" | "stretch""stretch"
justify"start" | "center" | "end" | "between" | "around" | "evenly""start"
wrapbooleanfalse
gapSpacingKey"4"

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

Center

PropTypeDefault
variant"both" | "horizontal" | "vertical""both"

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

Spacer

PropTypeDefault
growbooleantrue

Spacer is aria-hidden and never receives content.

Grid

PropTypeDefault
cols163
gapSpacingKey"4"
gapXSpacingKey
gapYSpacingKey

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

PropTypeDefault
paddingSpacingKey"0"
radius"none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl""none"
surface"none" | "background" | "card" | "muted" | "primary" | "secondary""none"