Claude-skill-registry 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.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/layout-primitives" ~/.claude/skills/majiayu000-claude-skill-registry-layout-primitives && rm -rf "$T"
skills/data/layout-primitives/SKILL.mdLayout 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 | + gap |
| Cluster | Horizontal wrapping groups | + gap |
| Sidebar | Two-column with minmax | + basis |
| Switcher | Stack/row based on width | + container query |
| Center | Centered max-width | + |
| Cover | Vertically centered hero | + flex |
| Grid | Auto-fill responsive grid | |
| Frame | Aspect ratio container | |
| Reel | Horizontal scroll | + |
| Box | Spacing/padding container | |
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