Agent skill
layout-primitives
Builds composable layout components (Stack, Cluster, Grid, Sidebar, Center, Cover). Use when creating layout systems, spacing compositions, or Every Layout-style intrinsic design patterns that adapt without breakpoints.
Install this agent skill to your Project
npx add-skill https://github.com/dylantarre/design-system-skills/tree/main/skills/patterns/layout-primitives
SKILL.md
Layout Primitives
Overview
Build reusable layout primitives (Stack, Cluster, Sidebar, Grid) that compose to create any layout. Based on Every Layout patterns - intrinsic design that adapts without breakpoints.
When to Use
- Setting up layout components for a design system
- Creating composable layout building blocks
- Replacing repetitive flexbox/grid patterns
- Building layouts that adapt without media queries
- Implementing consistent spacing throughout an app
Quick Reference: Core Primitives
| Primitive | Purpose | Primary CSS |
|---|---|---|
| Stack | Vertical spacing | flex-direction: column + gap |
| Cluster | Horizontal wrapping groups | flex-wrap: wrap + gap |
| Sidebar | Two-column with minmax | flex-wrap: wrap + basis |
| Switcher | Stack/row based on width | flex-wrap + container query |
| Center | Centered max-width | max-width + margin-inline: auto |
| Cover | Vertically centered hero | min-height + flex |
| Grid | Auto-fill responsive grid | grid-template-columns: repeat(auto-fill) |
| Frame | Aspect ratio container | aspect-ratio |
| Reel | Horizontal scroll | overflow-x: auto + flex-wrap: nowrap |
| Box | Spacing/padding container | padding |
Stack
Vertical rhythm with consistent spacing.
CSS
.stack {
display: flex;
flex-direction: column;
}
.stack > * + * {
margin-block-start: var(--stack-space, 1.5rem);
}
/* Or with gap (modern, simpler) */
.stack {
display: flex;
flex-direction: column;
gap: var(--stack-space, 1.5rem);
}
/* Variants */
.stack[data-space="sm"] { --stack-space: 0.5rem; }
.stack[data-space="md"] { --stack-space: 1rem; }
.stack[data-space="lg"] { --stack-space: 2rem; }
.stack[data-space="xl"] { --stack-space: 4rem; }
React Component
interface StackProps {
space?: 'sm' | 'md' | 'lg' | 'xl';
as?: React.ElementType;
children: React.ReactNode;
className?: string;
}
export function Stack({
space = 'md',
as: Component = 'div',
children,
className,
}: StackProps) {
return (
<Component
className={clsx('stack', className)}
data-space={space}
>
{children}
</Component>
);
}
// Usage
<Stack space="lg">
<h1>Title</h1>
<p>Paragraph</p>
<p>Another paragraph</p>
</Stack>
Tailwind Utility
// Tailwind approach
<div className="flex flex-col gap-4">
{/* children */}
</div>
// With custom component
function Stack({ space = 4, children }) {
return (
<div className={`flex flex-col gap-${space}`}>
{children}
</div>
);
}
Cluster
Horizontal grouping that wraps naturally.
CSS
.cluster {
display: flex;
flex-wrap: wrap;
gap: var(--cluster-space, 1rem);
align-items: var(--cluster-align, center);
justify-content: var(--cluster-justify, flex-start);
}
/* Variants */
.cluster[data-justify="center"] { justify-content: center; }
.cluster[data-justify="space-between"] { justify-content: space-between; }
.cluster[data-justify="end"] { justify-content: flex-end; }
.cluster[data-align="start"] { align-items: flex-start; }
.cluster[data-align="end"] { align-items: flex-end; }
.cluster[data-align="stretch"] { align-items: stretch; }
React Component
interface ClusterProps {
space?: 'xs' | 'sm' | 'md' | 'lg';
justify?: 'start' | 'center' | 'end' | 'space-between';
align?: 'start' | 'center' | 'end' | 'stretch';
children: React.ReactNode;
className?: string;
}
export function Cluster({
space = 'md',
justify = 'start',
align = 'center',
children,
className,
}: ClusterProps) {
return (
<div
className={clsx('cluster', className)}
data-space={space}
data-justify={justify}
data-align={align}
>
{children}
</div>
);
}
// Usage
<Cluster space="sm" justify="space-between">
<Tag>React</Tag>
<Tag>TypeScript</Tag>
<Tag>CSS</Tag>
</Cluster>
Sidebar
Two-column layout with a sidebar that stacks below breakpoint.
CSS
.with-sidebar {
display: flex;
flex-wrap: wrap;
gap: var(--sidebar-gap, 1rem);
}
.with-sidebar > :first-child {
flex-basis: var(--sidebar-width, 20rem);
flex-grow: 1;
}
.with-sidebar > :last-child {
flex-basis: 0;
flex-grow: 999;
min-inline-size: var(--sidebar-content-min, 50%);
}
/* Sidebar on right */
.with-sidebar[data-side="right"] {
flex-direction: row-reverse;
}
React Component
interface SidebarProps {
sideWidth?: string;
contentMin?: string;
gap?: string;
side?: 'left' | 'right';
children: React.ReactNode;
}
export function Sidebar({
sideWidth = '20rem',
contentMin = '50%',
gap = '1rem',
side = 'left',
children,
}: SidebarProps) {
return (
<div
className="with-sidebar"
style={{
'--sidebar-width': sideWidth,
'--sidebar-content-min': contentMin,
'--sidebar-gap': gap,
} as React.CSSProperties}
data-side={side}
>
{children}
</div>
);
}
// Usage
<Sidebar sideWidth="250px" contentMin="60%">
<nav>Sidebar content</nav>
<main>Main content</main>
</Sidebar>
Switcher
Switches between horizontal and vertical based on available width.
CSS
.switcher {
display: flex;
flex-wrap: wrap;
gap: var(--switcher-gap, 1rem);
}
.switcher > * {
flex-grow: 1;
flex-basis: calc((var(--switcher-threshold, 30rem) - 100%) * 999);
}
/* Limit number of items per row */
.switcher > :nth-last-child(n+4),
.switcher > :nth-last-child(n+4) ~ * {
flex-basis: 100%;
}
React Component
interface SwitcherProps {
threshold?: string;
gap?: string;
limit?: number;
children: React.ReactNode;
}
export function Switcher({
threshold = '30rem',
gap = '1rem',
limit = 4,
children,
}: SwitcherProps) {
return (
<div
className="switcher"
style={{
'--switcher-threshold': threshold,
'--switcher-gap': gap,
} as React.CSSProperties}
>
{children}
</div>
);
}
// Usage - items stay horizontal until container < 30rem
<Switcher threshold="500px">
<Card>First</Card>
<Card>Second</Card>
<Card>Third</Card>
</Switcher>
Center
Horizontally center with max-width.
CSS
.center {
box-sizing: content-box;
max-inline-size: var(--center-max, 60ch);
margin-inline: auto;
padding-inline: var(--center-gutter, 1rem);
}
/* Text centering variant */
.center[data-text] {
text-align: center;
}
/* Intrinsic centering (shrink-wrap) */
.center[data-intrinsic] {
display: flex;
flex-direction: column;
align-items: center;
}
React Component
interface CenterProps {
max?: string;
gutter?: string;
text?: boolean;
intrinsic?: boolean;
children: React.ReactNode;
}
export function Center({
max = '60ch',
gutter = '1rem',
text = false,
intrinsic = false,
children,
}: CenterProps) {
return (
<div
className="center"
style={{
'--center-max': max,
'--center-gutter': gutter,
} as React.CSSProperties}
data-text={text || undefined}
data-intrinsic={intrinsic || undefined}
>
{children}
</div>
);
}
// Usage
<Center max="800px">
<article>Centered content with max width</article>
</Center>
Cover
Full-height section with vertically centered content.
CSS
.cover {
display: flex;
flex-direction: column;
min-block-size: var(--cover-min-height, 100vh);
padding: var(--cover-padding, 1rem);
}
.cover > * {
margin-block: var(--cover-space, 1rem);
}
.cover > :first-child:not([data-centered]) {
margin-block-start: 0;
}
.cover > :last-child:not([data-centered]) {
margin-block-end: 0;
}
.cover > [data-centered] {
margin-block: auto;
}
React Component
interface CoverProps {
minHeight?: string;
padding?: string;
children: React.ReactNode;
}
export function Cover({ minHeight = '100vh', padding = '1rem', children }: CoverProps) {
return (
<div
className="cover"
style={{
'--cover-min-height': minHeight,
'--cover-padding': padding,
} as React.CSSProperties}
>
{children}
</div>
);
}
export function CoverCenter({ children }: { children: React.ReactNode }) {
return <div data-centered>{children}</div>;
}
// Usage - header at top, content centered, footer at bottom
<Cover>
<header>Logo</header>
<CoverCenter>
<h1>Hero Content</h1>
</CoverCenter>
<footer>Footer</footer>
</Cover>
Grid
Responsive grid without media queries.
CSS
.grid {
display: grid;
gap: var(--grid-gap, 1rem);
grid-template-columns: repeat(
auto-fill,
minmax(min(var(--grid-min, 250px), 100%), 1fr)
);
}
/* Fixed columns variant */
.grid[data-columns="2"] {
grid-template-columns: repeat(2, 1fr);
}
.grid[data-columns="3"] {
grid-template-columns: repeat(3, 1fr);
}
.grid[data-columns="4"] {
grid-template-columns: repeat(4, 1fr);
}
React Component
interface GridProps {
min?: string;
gap?: string;
children: React.ReactNode;
}
export function Grid({ min = '250px', gap = '1rem', children }: GridProps) {
return (
<div
className="grid"
style={{
'--grid-min': min,
'--grid-gap': gap,
} as React.CSSProperties}
>
{children}
</div>
);
}
// Usage
<Grid min="300px" gap="1.5rem">
<Card>1</Card>
<Card>2</Card>
<Card>3</Card>
<Card>4</Card>
</Grid>
Frame
Aspect ratio container for media.
CSS
.frame {
aspect-ratio: var(--frame-ratio, 16 / 9);
overflow: hidden;
}
.frame > img,
.frame > video,
.frame > iframe {
inline-size: 100%;
block-size: 100%;
object-fit: cover;
}
/* Common ratios */
.frame[data-ratio="1:1"] { --frame-ratio: 1; }
.frame[data-ratio="4:3"] { --frame-ratio: 4 / 3; }
.frame[data-ratio="16:9"] { --frame-ratio: 16 / 9; }
.frame[data-ratio="21:9"] { --frame-ratio: 21 / 9; }
React Component
type FrameRatio = '1:1' | '4:3' | '16:9' | '21:9' | string;
interface FrameProps {
ratio?: FrameRatio;
children: React.ReactNode;
}
const ratioMap: Record<string, string> = {
'1:1': '1',
'4:3': '4 / 3',
'16:9': '16 / 9',
'21:9': '21 / 9',
};
export function Frame({ ratio = '16:9', children }: FrameProps) {
return (
<div
className="frame"
style={{
'--frame-ratio': ratioMap[ratio] || ratio,
} as React.CSSProperties}
>
{children}
</div>
);
}
// Usage
<Frame ratio="16:9">
<img src="/hero.jpg" alt="Hero" />
</Frame>
Reel
Horizontal scrolling container.
CSS
.reel {
display: flex;
gap: var(--reel-gap, 1rem);
overflow-x: auto;
overflow-y: hidden;
scrollbar-color: var(--color-border-primary) transparent;
}
.reel > * {
flex: 0 0 var(--reel-item-width, auto);
}
/* Hide scrollbar (optional) */
.reel[data-hide-scrollbar] {
scrollbar-width: none;
}
.reel[data-hide-scrollbar]::-webkit-scrollbar {
display: none;
}
/* Snap scrolling */
.reel[data-snap] {
scroll-snap-type: x mandatory;
scroll-padding-inline: var(--reel-gap, 1rem);
}
.reel[data-snap] > * {
scroll-snap-align: start;
}
React Component
interface ReelProps {
gap?: string;
itemWidth?: string;
snap?: boolean;
hideScrollbar?: boolean;
children: React.ReactNode;
}
export function Reel({
gap = '1rem',
itemWidth,
snap = false,
hideScrollbar = false,
children,
}: ReelProps) {
return (
<div
className="reel"
style={{
'--reel-gap': gap,
'--reel-item-width': itemWidth,
} as React.CSSProperties}
data-snap={snap || undefined}
data-hide-scrollbar={hideScrollbar || undefined}
>
{children}
</div>
);
}
// Usage
<Reel itemWidth="300px" snap>
<Card>1</Card>
<Card>2</Card>
<Card>3</Card>
<Card>4</Card>
</Reel>
Box
Padding and background container.
CSS
.box {
padding: var(--box-padding, 1rem);
border: var(--box-border, 0);
background-color: var(--box-bg, transparent);
border-radius: var(--box-radius, 0);
}
/* Invert colors */
.box[data-invert] {
background-color: var(--color-bg-inverse);
color: var(--color-text-inverse);
}
React Component
interface BoxProps {
padding?: string;
bg?: string;
invert?: boolean;
children: React.ReactNode;
}
export function Box({ padding = '1rem', bg, invert, children }: BoxProps) {
return (
<div
className="box"
style={{
'--box-padding': padding,
'--box-bg': bg,
} as React.CSSProperties}
data-invert={invert || undefined}
>
{children}
</div>
);
}
Composition Examples
Page Layout
<Stack space="xl">
<Center max="1200px">
<header>
<Cluster justify="space-between">
<Logo />
<nav>
<Cluster space="md">
<a href="#">Home</a>
<a href="#">About</a>
<a href="#">Contact</a>
</Cluster>
</nav>
</Cluster>
</header>
</Center>
<Cover minHeight="80vh">
<CoverCenter>
<Center text>
<Stack space="lg">
<h1>Welcome</h1>
<p>Hero content here</p>
</Stack>
</Center>
</CoverCenter>
</Cover>
<Center max="1200px">
<Sidebar sideWidth="300px">
<aside>
<Stack space="md">
<h2>Sidebar</h2>
{/* sidebar content */}
</Stack>
</aside>
<main>
<Stack space="lg">
<Grid min="300px">
<Card>1</Card>
<Card>2</Card>
<Card>3</Card>
</Grid>
</Stack>
</main>
</Sidebar>
</Center>
</Stack>
Card Grid
<Grid min="280px" gap="1.5rem">
{items.map(item => (
<Box key={item.id} padding="1.5rem">
<Stack space="md">
<Frame ratio="16:9">
<img src={item.image} alt="" />
</Frame>
<h3>{item.title}</h3>
<p>{item.description}</p>
<Cluster justify="space-between">
<span>{item.date}</span>
<a href={item.url}>Read more</a>
</Cluster>
</Stack>
</Box>
))}
</Grid>
Tailwind Implementation
// tailwind.config.js - custom utilities
module.exports = {
theme: {
extend: {
// Custom layout utilities can use @apply or plugins
},
},
plugins: [
function({ addComponents }) {
addComponents({
'.stack': {
display: 'flex',
flexDirection: 'column',
gap: 'var(--stack-space, 1rem)',
},
'.cluster': {
display: 'flex',
flexWrap: 'wrap',
gap: 'var(--cluster-space, 1rem)',
alignItems: 'center',
},
// ... etc
});
},
],
};
Or use utility classes directly:
<!-- Stack -->
<div class="flex flex-col gap-4">
<!-- Cluster -->
<div class="flex flex-wrap gap-2 items-center">
<!-- Center -->
<div class="max-w-prose mx-auto px-4">
<!-- Grid -->
<div class="grid gap-4 grid-cols-[repeat(auto-fill,minmax(250px,1fr))]">
Resources
- Every Layout: https://every-layout.dev
- Intrinsic Web Design: https://www.youtube.com/watch?v=AMPKmh98XLY
- Layout Land: https://www.youtube.com/c/LayoutLand
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
getting-started
Introduction to the design system skills plugin. Use when first installing the plugin, exploring available skills, or planning a design system implementation.
responsive-typography
Implements fluid typography using CSS clamp() that scales with viewport. Use when creating responsive font sizes, viewport-aware headings, or type that adapts smoothly without breakpoints.
spacing-scale
Generates consistent spacing tokens using base values and ratios. Use when creating margin/padding systems, gap tokens, or layout spacing. Outputs CSS custom properties, Tailwind config, or JSON tokens.
color-scale
Generates perceptually uniform OKLCH color palettes from brand colors. Use when creating color systems, theme palettes, or converting hex values to 11-step scales. Outputs CSS custom properties, Tailwind config, or JSON tokens.
shadow-scale
Generates layered box-shadow elevation tokens for depth hierarchy. Use when creating elevation systems, card shadows, modal depth, or dark mode shadow variants. Outputs CSS, Tailwind, or JSON.
breakpoints
Generates responsive breakpoint tokens for media queries and container queries. Use when setting up responsive design, mobile-first layouts, or viewport-based styling. Outputs CSS, Tailwind, or JSON.
Didn't find tool you were looking for?