Installation
$ pnpm dlx shadcn@latest add https://lab.pratikthapw.dev/r/layout.jsonUsage
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.
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.
Readable content width
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.
One block, every marketing page
A Stack header over a Grid body — the shape repeats down the whole page.
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.
Personalized storybooks where the layout never competes with the story.
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>
);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.
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
| 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
| Prop | Type | Default |
|---|---|---|
surface | "default" | "inverted" | "muted" | "primary" | "secondary" | "default" |
spacing | SpacingKey | "0" |
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
| 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
| Prop | Type | Default |
|---|---|---|
variant | "both" | "horizontal" | "vertical" | "both" |
Center works at any depth; it is not limited to page-level centering.
Spacer
| Prop | Type | Default |
|---|---|---|
grow | boolean | true |
Spacer is aria-hidden and never receives content.
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
| Prop | Type | Default |
|---|---|---|
padding | SpacingKey | "0" |
radius | "none" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "none" |
surface | "none" | "background" | "card" | "muted" | "primary" | "secondary" | "none" |