
Designing Layouts
- 82 installs
- 426 repo stars
- Updated December 11, 2025
- ancoleman/ai-design-components
designing-layouts is a Claude skill that builds responsive CSS layout systems using grid, flexbox, container queries, breakpoints, and spacing tokens.
About
This skill provides guidance for building responsive layout systems using modern CSS: grid systems, flexbox patterns, container queries, spacing scales, and mobile-first breakpoints. A developer uses it when structuring app shells, dashboards with sidebars, card grids, or split-pane interfaces. It includes design-token integration and scripts to generate breakpoints and validate layout accessibility.
- CSS Grid, Flexbox, and container-query layout patterns
- Mobile-first breakpoint and spacing-token systems
- Scripts for breakpoints, fluid typography, and accessibility validation
Designing Layouts by the numbers
- 82 all-time installs (skills.sh)
- Ranked #1,104 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
designing-layouts capabilities & compatibility
- Capabilities
- responsive layout · css grid layout · flexbox layout · ui design
- Use cases
- frontend · ui design · web design
- IDEs
- vscode · cursor ide
What designing-layouts says it does
This skill provides comprehensive guidance for creating responsive layout systems using modern CSS techniques.
Container queries are production-ready in all modern browsers (2025).
npx skills add https://github.com/ancoleman/ai-design-components --skill designing-layoutsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 82 |
|---|---|
| repo stars | ★ 426 |
| Last updated | December 11, 2025 |
| Repository | ancoleman/ai-design-components ↗ |
What it does
Building responsive layouts (grids, flexbox, container queries, breakpoints) for app shells, dashboards, and card galleries.
Who is it for?
Structuring responsive app layouts, dashboards, and card grids with modern CSS.
Skip if: Backend or data work with no UI/CSS layout involved.
When should I use this skill?
Building responsive dashboards, grid layouts, split-pane interfaces, or breakpoint systems.
What you get
A responsive, token-driven layout with the right grid/flexbox/container-query method and validated accessibility.
- Responsive layout
- Breakpoint system
- Spacing token scale
By the numbers
- 12-column grid pattern
- 5 mobile-first breakpoints (640/768/1024/1280/1536px)
- 3 bundled layout scripts
Files
Layout Systems & Responsive Design
Purpose
This skill provides comprehensive guidance for creating responsive layout systems using modern CSS techniques. It covers grid systems, flexbox patterns, container queries, spacing systems, and mobile-first design strategies to build flexible, accessible interfaces that adapt seamlessly across devices.
When to Use
Invoke this skill when:
- Building responsive admin dashboards with sidebars and headers
- Creating grid-based layouts for content cards or galleries
- Implementing masonry or Pinterest-style layouts
- Designing split-pane interfaces with resizable panels
- Establishing responsive breakpoint systems
- Structuring application shells with navigation and content areas
- Building mobile-first responsive designs
- Creating flexible spacing and container systems
Layout Patterns
Grid Systems
For structured, two-dimensional layouts, use CSS Grid with design tokens.
12-Column Grid:
.grid-container {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: var(--grid-gap);
max-width: var(--container-max-width);
margin: 0 auto;
padding: 0 var(--container-padding-x);
}
.col-span-6 { grid-column: span 6; }
.col-span-4 { grid-column: span 4; }
.col-span-3 { grid-column: span 3; }Auto-Fit Responsive Grid:
.auto-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(280px, 100%), 1fr));
gap: var(--grid-gap);
}For complex grid layouts and advanced patterns, see references/layout-patterns.md.
Flexbox Patterns
For one-dimensional layouts and alignment control.
Holy Grail Layout:
.holy-grail {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.holy-grail__body {
flex: 1;
display: flex;
}
.holy-grail__nav {
width: var(--sidebar-width);
flex-shrink: 0;
}
.holy-grail__main {
flex: 1;
min-width: 0; /* Prevent overflow */
}
.holy-grail__aside {
width: var(--sidebar-width);
flex-shrink: 0;
}For additional flexbox patterns including sticky footer and centering, see references/css-techniques.md.
Container Queries
For component-responsive design that adapts based on container size, not viewport.
.card-container {
container-type: inline-size;
container-name: card;
}
@container card (min-width: 400px) {
.card {
grid-template-columns: auto 1fr;
gap: var(--spacing-lg);
}
}
@container card (min-width: 600px) {
.card {
grid-template-columns: 200px 1fr auto;
}
}Container queries are production-ready in all modern browsers (2025). For detailed usage and fallback strategies, see references/responsive-strategies.md.
Responsive Breakpoints
Use mobile-first approach with semantic breakpoints.
/* Mobile-first breakpoints using design tokens */
@media (min-width: 640px) { /* sm: Tablet portrait */
.container { max-width: 640px; }
}
@media (min-width: 768px) { /* md: Tablet landscape */
.container { max-width: 768px; }
}
@media (min-width: 1024px) { /* lg: Desktop */
.container { max-width: 1024px; }
}
@media (min-width: 1280px) { /* xl: Wide desktop */
.container { max-width: 1280px; }
}
@media (min-width: 1536px) { /* 2xl: Ultra-wide */
.container { max-width: 1536px; }
}For fluid typography and advanced responsive techniques, see references/responsive-strategies.md.
Spacing Systems
Implement consistent spacing using design tokens.
/* Base unit: 4px or 8px */
:root {
--spacing-xs: 4px;
--spacing-sm: 8px;
--spacing-md: 16px;
--spacing-lg: 24px;
--spacing-xl: 32px;
--spacing-2xl: 48px;
--spacing-3xl: 64px;
}
/* Apply systematically */
.section { padding: var(--section-spacing) 0; }
.container { padding: 0 var(--container-padding-x); }
.card { padding: var(--spacing-lg); }
.stack > * + * { margin-top: var(--spacing-md); }CSS Framework Integration
Tailwind CSS
For utility-first approach with custom configuration:
// tailwind.config.js
module.exports = {
theme: {
extend: {
spacing: {
'xs': 'var(--spacing-xs)',
'sm': 'var(--spacing-sm)',
'md': 'var(--spacing-md)',
'lg': 'var(--spacing-lg)',
'xl': 'var(--spacing-xl)',
},
screens: {
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
}
}
}
}For Tailwind patterns and optimization, see references/library-guide.md.
How to Use
1. Define Layout Requirements
Determine layout type and responsive behavior needed.
2. Choose Layout Method
- CSS Grid: Two-dimensional layouts, complex grids
- Flexbox: One-dimensional layouts, alignment
- Container Queries: Component-responsive designs
3. Implement with Design Tokens
Use design tokens from skills/design-tokens/ for consistent spacing, breakpoints, and sizing.
4. Generate Configurations
For responsive breakpoints:
node scripts/generate_breakpoints.js --approach mobile-firstFor fluid typography scale:
node scripts/calculate_fluid_typography.js --min-vw 320 --max-vw 19205. Validate Accessibility
Check semantic HTML and landmark regions:
node scripts/validate_layout_accessibility.js path/to/component.tsx6. Test Responsiveness
Test across device sizes using responsive preview tools and actual devices.
Scripts
scripts/generate_breakpoints.js- Generate responsive breakpoint systemscripts/calculate_fluid_typography.js- Calculate fluid typography scalesscripts/validate_layout_accessibility.js- Validate semantic HTML and ARIA landmarks
References
references/layout-patterns.md- Common layout patterns (sidebar, masonry, split-pane)references/responsive-strategies.md- Mobile-first design and responsive techniquesreferences/css-techniques.md- Modern CSS features (Grid, Flexbox, Container Queries)references/accessibility-layouts.md- Semantic HTML and ARIA landmarksreferences/library-guide.md- Framework integration (Tailwind, styled-components)references/performance-optimization.md- CSS performance and layout thrashing
Examples
examples/admin-layout.tsx- Complete admin dashboard with sidebarexamples/responsive-grid.tsx- Auto-responsive grid systemexamples/masonry-layout.tsx- Pinterest-style masonry gridexamples/split-pane.tsx- Resizable split-pane interface
Assets
assets/breakpoint-config.json- Standard breakpoint configurationsassets/layout-templates.json- Common layout templatesassets/spacing-scale.json- Spacing system configurations
{
"breakpoints": {
"xs": {
"min": 0,
"max": 639,
"label": "Extra Small",
"description": "Small phones (portrait)",
"columns": 4,
"margin": "16px",
"gutter": "16px"
},
"sm": {
"min": 640,
"max": 767,
"label": "Small",
"description": "Large phones, small tablets",
"columns": 8,
"margin": "24px",
"gutter": "16px"
},
"md": {
"min": 768,
"max": 1023,
"label": "Medium",
"description": "Tablets (portrait and landscape)",
"columns": 8,
"margin": "32px",
"gutter": "24px"
},
"lg": {
"min": 1024,
"max": 1279,
"label": "Large",
"description": "Desktop, laptop",
"columns": 12,
"margin": "32px",
"gutter": "24px"
},
"xl": {
"min": 1280,
"max": 1535,
"label": "Extra Large",
"description": "Wide desktop",
"columns": 12,
"margin": "auto",
"maxWidth": "1280px",
"gutter": "32px"
},
"2xl": {
"min": 1536,
"max": null,
"label": "2X Large",
"description": "Ultra-wide displays",
"columns": 12,
"margin": "auto",
"maxWidth": "1536px",
"gutter": "32px"
}
},
"devices": {
"iphone-se": {
"width": 375,
"height": 667,
"dpr": 2
},
"iphone-12": {
"width": 390,
"height": 844,
"dpr": 3
},
"iphone-14-pro": {
"width": 393,
"height": 852,
"dpr": 3
},
"iphone-14-pro-max": {
"width": 430,
"height": 932,
"dpr": 3
},
"pixel-5": {
"width": 393,
"height": 851,
"dpr": 2.75
},
"samsung-s20": {
"width": 412,
"height": 915,
"dpr": 3.5
},
"ipad-mini": {
"width": 768,
"height": 1024,
"dpr": 2
},
"ipad-air": {
"width": 820,
"height": 1180,
"dpr": 2
},
"ipad-pro-11": {
"width": 834,
"height": 1194,
"dpr": 2
},
"ipad-pro-12": {
"width": 1024,
"height": 1366,
"dpr": 2
},
"surface-pro": {
"width": 912,
"height": 1368,
"dpr": 2
},
"macbook-air": {
"width": 1440,
"height": 900,
"dpr": 2
},
"macbook-pro-14": {
"width": 1512,
"height": 982,
"dpr": 2
},
"macbook-pro-16": {
"width": 1728,
"height": 1117,
"dpr": 2
},
"imac-24": {
"width": 1920,
"height": 1080,
"dpr": 2
},
"desktop-4k": {
"width": 3840,
"height": 2160,
"dpr": 1
}
},
"containerSizes": {
"xs": "100%",
"sm": "640px",
"md": "768px",
"lg": "1024px",
"xl": "1280px",
"2xl": "1536px",
"full": "100%"
},
"gridSystems": {
"12-column": {
"columns": 12,
"gutterSizes": {
"xs": "16px",
"sm": "16px",
"md": "24px",
"lg": "24px",
"xl": "32px"
}
},
"8-column": {
"columns": 8,
"gutterSizes": {
"xs": "16px",
"sm": "16px",
"md": "24px",
"lg": "24px"
}
},
"4-column": {
"columns": 4,
"gutterSizes": {
"xs": "16px",
"sm": "16px"
}
}
}
}{
"templates": {
"admin-dashboard": {
"name": "Admin Dashboard",
"description": "Classic admin layout with sidebar navigation",
"structure": {
"header": {
"height": "64px",
"position": "fixed",
"zIndex": 40
},
"sidebar": {
"width": "280px",
"collapsedWidth": "64px",
"position": "fixed",
"breakpoint": "lg"
},
"main": {
"marginLeft": "280px",
"marginTop": "64px",
"padding": "24px"
}
},
"responsive": {
"mobile": {
"sidebar": "off-canvas",
"header": "sticky"
}
}
},
"holy-grail": {
"name": "Holy Grail Layout",
"description": "Header, footer, and three-column body",
"structure": {
"header": {
"height": "auto",
"flexShrink": 0
},
"body": {
"display": "flex",
"flex": 1
},
"nav": {
"width": "200px",
"order": -1
},
"main": {
"flex": 1,
"minWidth": 0
},
"aside": {
"width": "200px"
},
"footer": {
"height": "auto",
"flexShrink": 0
}
}
},
"split-view": {
"name": "Split View",
"description": "Two-panel resizable layout",
"structure": {
"container": {
"display": "flex",
"height": "100vh"
},
"panel1": {
"defaultWidth": "50%",
"minWidth": "200px"
},
"resizer": {
"width": "4px",
"cursor": "col-resize"
},
"panel2": {
"flex": 1,
"minWidth": "200px"
}
}
},
"card-grid": {
"name": "Card Grid",
"description": "Responsive card layout",
"structure": {
"container": {
"display": "grid",
"gap": "24px"
},
"columns": {
"xs": "1",
"sm": "2",
"md": "3",
"lg": "4",
"xl": "4"
},
"cardAspectRatio": "4/3"
}
},
"masonry": {
"name": "Masonry Grid",
"description": "Pinterest-style variable height grid",
"structure": {
"columns": {
"xs": "1",
"sm": "2",
"md": "3",
"lg": "4",
"xl": "5"
},
"gap": "16px",
"method": "css-columns"
}
},
"landing-page": {
"name": "Landing Page",
"description": "Marketing page with hero and sections",
"structure": {
"hero": {
"minHeight": "80vh",
"display": "flex",
"alignItems": "center"
},
"section": {
"padding": "80px 0",
"maxWidth": "1200px",
"margin": "0 auto"
}
}
},
"blog": {
"name": "Blog Layout",
"description": "Article with optional sidebar",
"structure": {
"container": {
"maxWidth": "1200px",
"display": "grid",
"gap": "32px"
},
"article": {
"maxWidth": "65ch",
"lineHeight": 1.6
},
"sidebar": {
"width": "300px",
"position": "sticky",
"top": "80px"
}
}
},
"kanban": {
"name": "Kanban Board",
"description": "Horizontal scrolling columns",
"structure": {
"board": {
"display": "flex",
"overflowX": "auto",
"gap": "16px",
"padding": "16px"
},
"column": {
"width": "300px",
"flexShrink": 0,
"maxHeight": "calc(100vh - 100px)"
}
}
},
"comparison": {
"name": "Comparison View",
"description": "Side-by-side comparison",
"structure": {
"container": {
"display": "grid",
"gridTemplateColumns": "1fr 1fr",
"gap": "2px",
"height": "100%"
},
"panel": {
"overflow": "auto",
"padding": "16px"
}
}
},
"dashboard-grid": {
"name": "Dashboard Grid",
"description": "Widget-based dashboard layout",
"structure": {
"grid": {
"display": "grid",
"gridTemplateColumns": "repeat(12, 1fr)",
"gridAutoRows": "minmax(100px, auto)",
"gap": "16px"
},
"widgets": {
"small": "span 3",
"medium": "span 6",
"large": "span 9",
"full": "span 12"
}
}
}
},
"patterns": {
"sticky-header": {
"position": "sticky",
"top": 0,
"zIndex": 40,
"background": "var(--color-surface)",
"borderBottom": "1px solid var(--border-color)"
},
"sticky-footer": {
"marginTop": "auto",
"flexShrink": 0
},
"centered-content": {
"maxWidth": "var(--container-max-width)",
"margin": "0 auto",
"padding": "0 var(--container-padding-x)"
},
"full-bleed": {
"width": "100vw",
"marginLeft": "calc(50% - 50vw)",
"marginRight": "calc(50% - 50vw)"
},
"aspect-ratio-box": {
"position": "relative",
"paddingBottom": "56.25%",
"child": {
"position": "absolute",
"top": 0,
"left": 0,
"width": "100%",
"height": "100%"
}
}
},
"responsive_patterns": {
"stack-on-mobile": {
"desktop": {
"display": "grid",
"gridTemplateColumns": "repeat(auto-fit, minmax(250px, 1fr))"
},
"mobile": {
"display": "block"
}
},
"hide-on-mobile": {
"@media (max-width: 768px)": {
"display": "none"
}
},
"show-on-mobile": {
"display": "none",
"@media (max-width: 768px)": {
"display": "block"
}
},
"collapse-padding": {
"desktop": {
"padding": "32px"
},
"tablet": {
"padding": "24px"
},
"mobile": {
"padding": "16px"
}
}
}
}{
"spacingScale": {
"base": 4,
"unit": "px",
"scale": {
"0": "0px",
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px",
"2xl": "48px",
"3xl": "64px",
"4xl": "96px",
"5xl": "128px",
"6xl": "192px"
},
"aliases": {
"none": "0",
"extra-small": "xs",
"small": "sm",
"medium": "md",
"large": "lg",
"extra-large": "xl"
}
},
"layoutSpacing": {
"container": {
"padding": {
"xs": "16px",
"sm": "24px",
"md": "32px",
"lg": "48px",
"xl": "64px"
},
"maxWidth": {
"sm": "640px",
"md": "768px",
"lg": "1024px",
"xl": "1280px",
"2xl": "1536px",
"full": "100%"
}
},
"section": {
"padding": {
"xs": "32px 0",
"sm": "48px 0",
"md": "64px 0",
"lg": "80px 0",
"xl": "96px 0"
}
},
"grid": {
"gap": {
"xs": "8px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"xl": "32px"
}
},
"stack": {
"gap": {
"xs": "8px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"xl": "32px"
}
},
"card": {
"padding": {
"xs": "12px",
"sm": "16px",
"md": "20px",
"lg": "24px",
"xl": "32px"
}
},
"form": {
"fieldGap": "24px",
"labelGap": "8px",
"inputPadding": "12px 16px",
"buttonPadding": "12px 24px"
}
},
"componentSpacing": {
"header": {
"height": {
"mobile": "56px",
"desktop": "64px"
},
"padding": "0 16px"
},
"sidebar": {
"width": {
"collapsed": "64px",
"narrow": "200px",
"normal": "280px",
"wide": "320px"
},
"padding": "16px",
"itemGap": "4px"
},
"modal": {
"padding": {
"sm": "16px",
"md": "24px",
"lg": "32px"
},
"headerPadding": "16px 24px",
"bodyPadding": "24px",
"footerPadding": "16px 24px"
},
"table": {
"cellPadding": "12px 16px",
"headerPadding": "16px",
"rowGap": "0"
},
"list": {
"itemPadding": "12px 16px",
"itemGap": "4px",
"groupGap": "16px"
}
},
"rhythmScale": {
"description": "Vertical rhythm based on line height",
"baseLineHeight": 24,
"unit": "px",
"scale": {
"0.25": "6px",
"0.5": "12px",
"0.75": "18px",
"1": "24px",
"1.5": "36px",
"2": "48px",
"3": "72px",
"4": "96px"
}
},
"customProperties": {
"css": [
"--spacing-xs: 4px",
"--spacing-sm: 8px",
"--spacing-md: 16px",
"--spacing-lg: 24px",
"--spacing-xl: 32px",
"--spacing-2xl: 48px",
"--spacing-3xl: 64px",
"--spacing-4xl: 96px",
"--container-padding-x: var(--spacing-lg)",
"--container-padding-y: var(--spacing-md)",
"--container-max-width: 1280px",
"--section-spacing: var(--spacing-3xl)",
"--content-spacing: var(--spacing-lg)",
"--grid-gap: var(--spacing-md)",
"--stack-gap: var(--spacing-md)",
"--header-height: 64px",
"--sidebar-width: 280px",
"--sidebar-collapsed-width: 64px"
]
},
"utilities": {
"margin": {
"m-0": "0",
"m-auto": "auto",
"m-xs": "4px",
"m-sm": "8px",
"m-md": "16px",
"m-lg": "24px",
"m-xl": "32px",
"mx-auto": "0 auto",
"my-xs": "4px 0",
"my-sm": "8px 0",
"my-md": "16px 0",
"my-lg": "24px 0",
"mx-xs": "0 4px",
"mx-sm": "0 8px",
"mx-md": "0 16px",
"mx-lg": "0 24px"
},
"padding": {
"p-0": "0",
"p-xs": "4px",
"p-sm": "8px",
"p-md": "16px",
"p-lg": "24px",
"p-xl": "32px",
"py-xs": "4px 0",
"py-sm": "8px 0",
"py-md": "16px 0",
"py-lg": "24px 0",
"px-xs": "0 4px",
"px-sm": "0 8px",
"px-md": "0 16px",
"px-lg": "0 24px"
},
"gap": {
"gap-xs": "4px",
"gap-sm": "8px",
"gap-md": "16px",
"gap-lg": "24px",
"gap-xl": "32px"
}
}
}import React, { useState, useEffect } from 'react';
/**
* Complete Admin Dashboard Layout with Sidebar
* Features: Collapsible sidebar, responsive design, accessible navigation
*/
interface AdminLayoutProps {
children: React.ReactNode;
user?: {
name: string;
avatar?: string;
};
}
interface NavItem {
id: string;
label: string;
icon?: React.ReactNode;
href: string;
badge?: number;
children?: NavItem[];
}
const navigationItems: NavItem[] = [
{ id: 'dashboard', label: 'Dashboard', href: '/', icon: '📊' },
{ id: 'users', label: 'Users', href: '/users', icon: '👥', badge: 3 },
{
id: 'content',
label: 'Content',
href: '/content',
icon: '📝',
children: [
{ id: 'posts', label: 'Posts', href: '/content/posts' },
{ id: 'media', label: 'Media', href: '/content/media' },
{ id: 'pages', label: 'Pages', href: '/content/pages' },
]
},
{ id: 'analytics', label: 'Analytics', href: '/analytics', icon: '📈' },
{ id: 'settings', label: 'Settings', href: '/settings', icon: '⚙️' },
];
export function AdminLayout({ children, user }: AdminLayoutProps) {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const [currentPath, setCurrentPath] = useState('/');
// Handle responsive behavior
useEffect(() => {
const handleResize = () => {
if (window.innerWidth < 1024) {
setSidebarOpen(false);
} else {
setMobileSidebarOpen(false);
}
};
handleResize();
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
// Toggle expanded state for nav items with children
const toggleExpanded = (itemId: string) => {
setExpandedItems(prev => {
const next = new Set(prev);
if (next.has(itemId)) {
next.delete(itemId);
} else {
next.add(itemId);
}
return next;
});
};
// Render navigation item
const renderNavItem = (item: NavItem, depth = 0) => {
const isActive = currentPath === item.href;
const hasChildren = item.children && item.children.length > 0;
const isExpanded = expandedItems.has(item.id);
return (
<li key={item.id} role="none">
<a
href={item.href}
onClick={(e) => {
if (hasChildren) {
e.preventDefault();
toggleExpanded(item.id);
} else {
setCurrentPath(item.href);
}
}}
className={`
nav-item
${isActive ? 'nav-item--active' : ''}
${depth > 0 ? 'nav-item--nested' : ''}
`}
aria-current={isActive ? 'page' : undefined}
aria-expanded={hasChildren ? isExpanded : undefined}
>
{item.icon && (
<span className="nav-item__icon" aria-hidden="true">
{item.icon}
</span>
)}
<span className={`nav-item__label ${!sidebarOpen && depth === 0 ? 'sr-only' : ''}`}>
{item.label}
</span>
{item.badge && (
<span className="nav-item__badge" aria-label={`${item.badge} new items`}>
{item.badge}
</span>
)}
{hasChildren && (
<span
className="nav-item__chevron"
aria-hidden="true"
style={{ transform: isExpanded ? 'rotate(90deg)' : 'rotate(0)' }}
>
›
</span>
)}
</a>
{hasChildren && isExpanded && (
<ul className="nav-submenu" role="group">
{item.children!.map(child => renderNavItem(child, depth + 1))}
</ul>
)}
</li>
);
};
return (
<div className="admin-layout">
{/* Skip Links */}
<a href="#main" className="skip-link">Skip to main content</a>
<a href="#nav" className="skip-link">Skip to navigation</a>
{/* Header */}
<header className="admin-header" role="banner">
<div className="admin-header__left">
<button
className="menu-toggle"
onClick={() => {
if (window.innerWidth < 1024) {
setMobileSidebarOpen(!mobileSidebarOpen);
} else {
setSidebarOpen(!sidebarOpen);
}
}}
aria-label={sidebarOpen ? 'Close sidebar' : 'Open sidebar'}
>
<span className="menu-toggle__line" />
<span className="menu-toggle__line" />
<span className="menu-toggle__line" />
</button>
<h1 className="admin-header__title">Admin Dashboard</h1>
</div>
<div className="admin-header__right">
{user && (
<div className="user-menu">
<button className="user-menu__trigger" aria-label="User menu">
{user.avatar ? (
<img src={user.avatar} alt="" className="user-menu__avatar" />
) : (
<span className="user-menu__avatar">👤</span>
)}
<span className="user-menu__name">{user.name}</span>
</button>
</div>
)}
</div>
</header>
{/* Mobile Overlay */}
{mobileSidebarOpen && (
<div
className="mobile-overlay"
onClick={() => setMobileSidebarOpen(false)}
aria-hidden="true"
/>
)}
{/* Sidebar */}
<aside
id="nav"
className={`
admin-sidebar
${sidebarOpen ? 'admin-sidebar--open' : 'admin-sidebar--collapsed'}
${mobileSidebarOpen ? 'admin-sidebar--mobile-open' : ''}
`}
role="navigation"
aria-label="Main navigation"
>
<nav className="admin-nav">
<ul role="list" className="nav-list">
{navigationItems.map(item => renderNavItem(item))}
</ul>
</nav>
</aside>
{/* Main Content */}
<main
id="main"
className={`
admin-main
${sidebarOpen ? 'admin-main--sidebar-open' : 'admin-main--sidebar-collapsed'}
`}
role="main"
>
<div className="admin-content">
{children}
</div>
</main>
</div>
);
}
// CSS (in a separate file or CSS-in-JS)
const styles = `
.admin-layout {
display: flex;
flex-direction: column;
min-height: 100vh;
background: var(--color-background);
}
/* Skip Links */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--color-primary);
color: white;
padding: 8px 16px;
text-decoration: none;
z-index: 100;
border-radius: 0 0 4px 0;
}
.skip-link:focus {
top: 0;
}
/* Header */
.admin-header {
position: sticky;
top: 0;
z-index: 40;
height: var(--header-height, 64px);
background: var(--color-surface);
border-bottom: 1px solid var(--border-color);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 var(--spacing-lg);
box-shadow: var(--shadow-sm);
}
.admin-header__left {
display: flex;
align-items: center;
gap: var(--spacing-md);
}
.admin-header__title {
font-size: var(--text-xl);
font-weight: 600;
margin: 0;
}
/* Menu Toggle */
.menu-toggle {
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
width: 44px;
height: 44px;
padding: 10px;
background: transparent;
border: none;
cursor: pointer;
border-radius: var(--radius-md);
transition: background-color 0.2s;
}
.menu-toggle:hover {
background: var(--color-hover);
}
.menu-toggle__line {
width: 24px;
height: 2px;
background: currentColor;
transition: transform 0.2s;
}
/* Sidebar */
.admin-sidebar {
position: fixed;
left: 0;
top: var(--header-height, 64px);
bottom: 0;
width: var(--sidebar-width, 280px);
background: var(--color-surface);
border-right: 1px solid var(--border-color);
transition: width 0.3s ease, transform 0.3s ease;
overflow-y: auto;
overflow-x: hidden;
z-index: 30;
}
.admin-sidebar--collapsed {
width: var(--sidebar-collapsed-width, 64px);
}
.admin-sidebar--collapsed .nav-item__label {
opacity: 0;
width: 0;
}
.admin-sidebar--collapsed .nav-item__badge,
.admin-sidebar--collapsed .nav-item__chevron,
.admin-sidebar--collapsed .nav-submenu {
display: none;
}
/* Mobile Sidebar */
@media (max-width: 1023px) {
.admin-sidebar {
transform: translateX(-100%);
}
.admin-sidebar--mobile-open {
transform: translateX(0);
}
.mobile-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 25;
}
}
/* Navigation */
.nav-list {
list-style: none;
padding: var(--spacing-sm);
margin: 0;
}
.nav-item {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm) var(--spacing-md);
color: var(--color-text);
text-decoration: none;
border-radius: var(--radius-md);
transition: background-color 0.2s, color 0.2s;
position: relative;
min-height: 44px;
}
.nav-item:hover {
background: var(--color-hover);
}
.nav-item--active {
background: var(--color-primary-light);
color: var(--color-primary);
font-weight: 500;
}
.nav-item--nested {
padding-left: calc(var(--spacing-md) + 24px);
}
.nav-item__icon {
flex-shrink: 0;
font-size: 20px;
width: 24px;
text-align: center;
}
.nav-item__label {
flex: 1;
transition: opacity 0.3s, width 0.3s;
}
.nav-item__badge {
background: var(--color-error);
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
min-width: 20px;
text-align: center;
}
.nav-item__chevron {
transition: transform 0.2s;
}
.nav-submenu {
list-style: none;
margin: 0;
padding: 0;
}
/* Main Content */
.admin-main {
flex: 1;
margin-left: var(--sidebar-width, 280px);
margin-top: var(--header-height, 64px);
transition: margin-left 0.3s ease;
min-height: calc(100vh - var(--header-height, 64px));
}
.admin-main--sidebar-collapsed {
margin-left: var(--sidebar-collapsed-width, 64px);
}
@media (max-width: 1023px) {
.admin-main {
margin-left: 0;
}
}
.admin-content {
padding: var(--spacing-xl);
max-width: var(--container-max-width, 1440px);
margin: 0 auto;
}
/* User Menu */
.user-menu__trigger {
display: flex;
align-items: center;
gap: var(--spacing-sm);
padding: var(--spacing-sm);
background: transparent;
border: none;
cursor: pointer;
border-radius: var(--radius-md);
min-height: 44px;
}
.user-menu__trigger:hover {
background: var(--color-hover);
}
.user-menu__avatar {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.user-menu__name {
font-weight: 500;
}
/* Screen reader only */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
`;import React, { useState, useEffect, useRef } from 'react';
/**
* Pinterest-style Masonry Layout
* Features: Variable height items, responsive columns, smooth transitions
*/
interface MasonryItem {
id: string;
content: React.ReactNode;
height?: number; // Optional predefined height
}
interface MasonryLayoutProps {
items: MasonryItem[];
columns?: number; // Fixed columns or auto-calculate
gap?: number;
minColumnWidth?: number;
}
export function MasonryLayout({
items,
columns,
gap = 16,
minColumnWidth = 250
}: MasonryLayoutProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [columnCount, setColumnCount] = useState(columns || 3);
const [columnHeights, setColumnHeights] = useState<number[]>([]);
const [itemPositions, setItemPositions] = useState<Map<string, { column: number; top: number }>>(new Map());
// Calculate number of columns based on container width
useEffect(() => {
if (columns) {
setColumnCount(columns);
return;
}
const updateColumns = () => {
if (containerRef.current) {
const containerWidth = containerRef.current.offsetWidth;
const calculatedColumns = Math.max(1, Math.floor(containerWidth / minColumnWidth));
setColumnCount(calculatedColumns);
}
};
updateColumns();
const resizeObserver = new ResizeObserver(updateColumns);
if (containerRef.current) {
resizeObserver.observe(containerRef.current);
}
return () => resizeObserver.disconnect();
}, [columns, minColumnWidth]);
// Calculate item positions
useEffect(() => {
const heights = new Array(columnCount).fill(0);
const positions = new Map<string, { column: number; top: number }>();
items.forEach((item) => {
// Find the shortest column
const shortestColumn = heights.indexOf(Math.min(...heights));
// Position item in shortest column
positions.set(item.id, {
column: shortestColumn,
top: heights[shortestColumn]
});
// Update column height (use predefined height or estimate)
const itemHeight = item.height || 200; // Default estimate
heights[shortestColumn] += itemHeight + gap;
});
setColumnHeights(heights);
setItemPositions(positions);
}, [items, columnCount, gap]);
const getColumnWidth = () => {
if (!containerRef.current) return '100%';
const containerWidth = containerRef.current.offsetWidth;
const totalGaps = (columnCount - 1) * gap;
const columnWidth = (containerWidth - totalGaps) / columnCount;
return `${columnWidth}px`;
};
return (
<div
ref={containerRef}
className="masonry-container"
style={{
position: 'relative',
height: Math.max(...columnHeights) + 'px'
}}
>
{items.map((item) => {
const position = itemPositions.get(item.id);
if (!position) return null;
const columnWidth = getColumnWidth();
const left = `calc(${position.column} * (${columnWidth} + ${gap}px))`;
return (
<div
key={item.id}
className="masonry-item"
style={{
position: 'absolute',
top: `${position.top}px`,
left: left,
width: columnWidth,
transition: 'all 0.3s ease'
}}
>
{item.content}
</div>
);
})}
</div>
);
}
/**
* CSS-Only Masonry Layout (Modern Browsers)
* Uses CSS Grid with experimental masonry support
*/
export function CSSMasonryLayout({ items, columns = 4, gap = '1rem' }: {
items: React.ReactNode[];
columns?: number;
gap?: string;
}) {
return (
<>
<div className="css-masonry-modern">
{items.map((item, index) => (
<div key={index} className="css-masonry-item">
{item}
</div>
))}
</div>
<style>{`
/* Modern CSS Grid Masonry (Experimental - Check browser support) */
@supports (grid-template-rows: masonry) {
.css-masonry-modern {
display: grid;
grid-template-columns: repeat(${columns}, 1fr);
grid-template-rows: masonry;
gap: ${gap};
}
}
/* Fallback to columns */
@supports not (grid-template-rows: masonry) {
.css-masonry-modern {
column-count: ${columns};
column-gap: ${gap};
}
.css-masonry-item {
break-inside: avoid;
margin-bottom: ${gap};
}
}
/* Responsive columns */
@media (max-width: 1200px) {
.css-masonry-modern {
column-count: 3;
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.css-masonry-modern {
column-count: 2;
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 600px) {
.css-masonry-modern {
column-count: 1;
grid-template-columns: 1fr;
}
}
`}</style>
</>
);
}
/**
* Pinterest-Style Cards Example
*/
interface PinterestCard {
id: string;
image: string;
title: string;
description?: string;
author?: {
name: string;
avatar?: string;
};
stats?: {
likes: number;
comments: number;
};
}
export function PinterestGrid({ cards }: { cards: PinterestCard[] }) {
const masonryItems: MasonryItem[] = cards.map(card => ({
id: card.id,
content: (
<article className="pinterest-card">
<div className="pinterest-card__image">
<img src={card.image} alt={card.title} loading="lazy" />
<button className="pinterest-card__save" aria-label="Save">
📌 Save
</button>
</div>
<div className="pinterest-card__content">
<h3 className="pinterest-card__title">{card.title}</h3>
{card.description && (
<p className="pinterest-card__description">{card.description}</p>
)}
{card.author && (
<div className="pinterest-card__author">
{card.author.avatar && (
<img src={card.author.avatar} alt="" className="author-avatar" />
)}
<span className="author-name">{card.author.name}</span>
</div>
)}
{card.stats && (
<div className="pinterest-card__stats">
<span>❤️ {card.stats.likes}</span>
<span>💬 {card.stats.comments}</span>
</div>
)}
</div>
</article>
),
// Estimate height based on content
height: card.description ? 350 : 280
}));
return <MasonryLayout items={masonryItems} minColumnWidth={236} gap={20} />;
}
/**
* Photo Gallery Example
*/
export function PhotoGallery({ photos }: {
photos: Array<{
id: string;
src: string;
alt: string;
width: number;
height: number;
caption?: string;
}>
}) {
const masonryItems: MasonryItem[] = photos.map(photo => ({
id: photo.id,
content: (
<figure className="photo-gallery-item">
<img
src={photo.src}
alt={photo.alt}
loading="lazy"
style={{ aspectRatio: `${photo.width}/${photo.height}` }}
/>
{photo.caption && (
<figcaption className="photo-caption">{photo.caption}</figcaption>
)}
</figure>
),
// Calculate height based on aspect ratio
height: (250 * photo.height) / photo.width
}));
return <MasonryLayout items={masonryItems} minColumnWidth={250} gap={16} />;
}
// CSS Styles
const styles = `
/* Masonry Container */
.masonry-container {
width: 100%;
position: relative;
transition: height 0.3s ease;
}
.masonry-item {
position: absolute;
transition: all 0.3s ease;
}
/* Pinterest Card Styles */
.pinterest-card {
background: var(--color-surface);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
cursor: pointer;
}
.pinterest-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-lg);
}
.pinterest-card__image {
position: relative;
overflow: hidden;
background: var(--color-background);
}
.pinterest-card__image img {
width: 100%;
height: auto;
display: block;
}
.pinterest-card__save {
position: absolute;
top: 8px;
right: 8px;
background: var(--color-error);
color: white;
border: none;
padding: 8px 12px;
border-radius: var(--radius-full);
font-weight: 600;
opacity: 0;
transform: translateY(-4px);
transition: opacity 0.2s, transform 0.2s;
cursor: pointer;
}
.pinterest-card:hover .pinterest-card__save {
opacity: 1;
transform: translateY(0);
}
.pinterest-card__content {
padding: var(--spacing-md);
}
.pinterest-card__title {
font-size: var(--text-base);
font-weight: 600;
margin: 0 0 var(--spacing-xs) 0;
line-height: 1.3;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.pinterest-card__description {
font-size: var(--text-sm);
color: var(--color-text-secondary);
margin: 0 0 var(--spacing-sm) 0;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
.pinterest-card__author {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
}
.author-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.author-name {
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
.pinterest-card__stats {
display: flex;
gap: var(--spacing-md);
font-size: var(--text-sm);
color: var(--color-text-secondary);
}
/* Photo Gallery Styles */
.photo-gallery-item {
margin: 0;
border-radius: var(--radius-md);
overflow: hidden;
background: var(--color-surface);
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
}
.photo-gallery-item:hover {
transform: scale(1.02);
box-shadow: var(--shadow-md);
}
.photo-gallery-item img {
width: 100%;
height: auto;
display: block;
}
.photo-caption {
padding: var(--spacing-sm);
font-size: var(--text-sm);
color: var(--color-text-secondary);
background: var(--color-surface);
}
/* Loading State */
.masonry-loading {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
}
.skeleton-card {
background: linear-gradient(90deg, var(--color-background) 25%, var(--color-surface) 50%, var(--color-background) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--radius-lg);
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
/* Responsive */
@media (max-width: 640px) {
.pinterest-card__content {
padding: var(--spacing-sm);
}
.pinterest-card__title {
font-size: var(--text-sm);
}
}
/* Dark Mode Support */
@media (prefers-color-scheme: dark) {
.pinterest-card {
background: var(--color-surface-dark);
}
.photo-gallery-item {
background: var(--color-surface-dark);
}
}
`;import React, { useState, useEffect } from 'react';
/**
* Auto-Responsive Grid System
* Features: Auto-fit/fill, container queries, customizable gaps and columns
*/
interface GridItem {
id: string;
content: React.ReactNode;
span?: number; // How many columns this item should span
aspectRatio?: string; // Aspect ratio for the item
}
interface ResponsiveGridProps {
items: GridItem[];
minItemWidth?: number;
maxColumns?: number;
gap?: string;
variant?: 'auto-fit' | 'auto-fill' | 'fixed';
useContainerQueries?: boolean;
}
export function ResponsiveGrid({
items,
minItemWidth = 250,
maxColumns = 12,
gap = 'var(--spacing-md)',
variant = 'auto-fit',
useContainerQueries = true
}: ResponsiveGridProps) {
const [containerWidth, setContainerWidth] = useState<number>(0);
const [columns, setColumns] = useState<number>(1);
// Calculate optimal columns based on container width
useEffect(() => {
if (variant === 'fixed') return;
const updateColumns = () => {
const container = document.querySelector('.grid-container');
if (container) {
const width = container.clientWidth;
setContainerWidth(width);
const gapSize = parseInt(gap) || 16;
const availableWidth = width - gapSize;
const possibleColumns = Math.floor(availableWidth / (minItemWidth + gapSize));
setColumns(Math.min(possibleColumns, maxColumns));
}
};
updateColumns();
const observer = new ResizeObserver(updateColumns);
const container = document.querySelector('.grid-container');
if (container) {
observer.observe(container);
}
return () => observer.disconnect();
}, [minItemWidth, maxColumns, gap, variant]);
const getGridStyles = () => {
const baseStyles = {
display: 'grid',
gap: gap,
width: '100%'
};
switch (variant) {
case 'auto-fit':
return {
...baseStyles,
gridTemplateColumns: `repeat(auto-fit, minmax(min(${minItemWidth}px, 100%), 1fr))`
};
case 'auto-fill':
return {
...baseStyles,
gridTemplateColumns: `repeat(auto-fill, minmax(${minItemWidth}px, 1fr))`
};
case 'fixed':
return {
...baseStyles,
gridTemplateColumns: `repeat(${maxColumns}, 1fr)`
};
default:
return baseStyles;
}
};
return (
<div
className={`grid-container ${useContainerQueries ? 'grid-container--cq' : ''}`}
style={useContainerQueries ? { containerType: 'inline-size' } : {}}
>
<div className="grid" style={getGridStyles()}>
{items.map(item => (
<div
key={item.id}
className="grid-item"
style={{
gridColumn: item.span ? `span ${item.span}` : undefined,
aspectRatio: item.aspectRatio || undefined
}}
>
{item.content}
</div>
))}
</div>
{/* Grid info for debugging/demo */}
{process.env.NODE_ENV === 'development' && (
<div className="grid-info">
<span>Container: {containerWidth}px</span>
<span>Columns: {columns}</span>
<span>Min width: {minItemWidth}px</span>
</div>
)}
</div>
);
}
/**
* Card Grid Example
* Common use case for product cards, blog posts, etc.
*/
interface CardGridProps {
cards: Array<{
id: string;
title: string;
description: string;
image?: string;
tags?: string[];
}>;
}
export function CardGrid({ cards }: CardGridProps) {
const gridItems: GridItem[] = cards.map(card => ({
id: card.id,
content: (
<article className="card">
{card.image && (
<div className="card__image">
<img src={card.image} alt="" loading="lazy" />
</div>
)}
<div className="card__content">
<h3 className="card__title">{card.title}</h3>
<p className="card__description">{card.description}</p>
{card.tags && (
<div className="card__tags">
{card.tags.map(tag => (
<span key={tag} className="tag">{tag}</span>
))}
</div>
)}
</div>
</article>
)
}));
return (
<ResponsiveGrid
items={gridItems}
minItemWidth={300}
gap="var(--spacing-lg)"
variant="auto-fit"
useContainerQueries={true}
/>
);
}
/**
* Feature Grid Example
* For feature sections with icons and descriptions
*/
interface FeatureGridProps {
features: Array<{
id: string;
icon: React.ReactNode;
title: string;
description: string;
}>;
columns?: number;
}
export function FeatureGrid({ features, columns = 3 }: FeatureGridProps) {
const gridItems: GridItem[] = features.map(feature => ({
id: feature.id,
content: (
<div className="feature">
<div className="feature__icon">{feature.icon}</div>
<h3 className="feature__title">{feature.title}</h3>
<p className="feature__description">{feature.description}</p>
</div>
)
}));
return (
<ResponsiveGrid
items={gridItems}
minItemWidth={280}
maxColumns={columns}
gap="var(--spacing-xl)"
variant="auto-fit"
/>
);
}
/**
* Image Gallery Grid
* Responsive image gallery with different aspect ratios
*/
interface GalleryImage {
id: string;
src: string;
alt: string;
span?: number;
aspectRatio?: string;
}
export function ImageGallery({ images }: { images: GalleryImage[] }) {
const gridItems: GridItem[] = images.map(img => ({
id: img.id,
span: img.span,
aspectRatio: img.aspectRatio || '1',
content: (
<figure className="gallery-item">
<img
src={img.src}
alt={img.alt}
loading="lazy"
decoding="async"
/>
</figure>
)
}));
return (
<ResponsiveGrid
items={gridItems}
minItemWidth={200}
gap="var(--spacing-sm)"
variant="auto-fit"
/>
);
}
// CSS Styles
const styles = `
/* Grid Container */
.grid-container {
width: 100%;
position: relative;
}
.grid-container--cq {
container-type: inline-size;
container-name: grid;
}
/* Grid */
.grid {
display: grid;
width: 100%;
}
.grid-item {
min-width: 0; /* Prevent overflow */
position: relative;
}
/* Container Queries for Grid Items */
@container grid (min-width: 600px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
gap: var(--spacing-md);
}
.card__image {
aspect-ratio: 1;
}
}
@container grid (min-width: 900px) {
.feature {
display: grid;
grid-template-columns: auto 1fr;
gap: var(--spacing-md);
align-items: start;
}
}
/* Card Styles */
.card {
background: var(--color-surface);
border-radius: var(--radius-lg);
overflow: hidden;
box-shadow: var(--shadow-sm);
transition: transform 0.2s, box-shadow 0.2s;
height: 100%;
display: flex;
flex-direction: column;
}
.card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.card__image {
aspect-ratio: 16/9;
overflow: hidden;
background: var(--color-background);
}
.card__image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.card__content {
padding: var(--spacing-md);
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.card__title {
font-size: var(--text-lg);
font-weight: 600;
margin: 0;
line-height: 1.3;
}
.card__description {
color: var(--color-text-secondary);
margin: 0;
flex: 1;
}
.card__tags {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-xs);
margin-top: auto;
}
.tag {
padding: 2px 8px;
background: var(--color-primary-light);
color: var(--color-primary);
border-radius: var(--radius-sm);
font-size: var(--text-sm);
}
/* Feature Styles */
.feature {
text-align: center;
padding: var(--spacing-lg);
}
.feature__icon {
font-size: 48px;
margin-bottom: var(--spacing-md);
color: var(--color-primary);
}
.feature__title {
font-size: var(--text-xl);
font-weight: 600;
margin: 0 0 var(--spacing-sm) 0;
}
.feature__description {
color: var(--color-text-secondary);
margin: 0;
}
/* Gallery Styles */
.gallery-item {
margin: 0;
overflow: hidden;
border-radius: var(--radius-md);
background: var(--color-background);
}
.gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
transition: transform 0.3s;
}
.gallery-item:hover img {
transform: scale(1.05);
}
/* Grid Info (Development) */
.grid-info {
position: fixed;
bottom: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 12px;
display: flex;
gap: 16px;
z-index: 9999;
font-family: monospace;
}
/* Responsive Breakpoints */
@media (max-width: 640px) {
.card__content {
padding: var(--spacing-sm);
}
.feature {
padding: var(--spacing-md);
}
.feature__icon {
font-size: 36px;
}
}
@media (min-width: 1280px) {
.card__content {
padding: var(--spacing-lg);
}
}
/* Print Styles */
@media print {
.grid {
display: block;
}
.grid-item {
page-break-inside: avoid;
margin-bottom: var(--spacing-md);
}
}
`;import React, { useState, useRef, useEffect, useCallback } from 'react';
/**
* Resizable Split-Pane Interface
* Features: Mouse/touch dragging, keyboard control, min/max constraints
*/
interface SplitPaneProps {
left: React.ReactNode;
right: React.ReactNode;
orientation?: 'horizontal' | 'vertical';
defaultPosition?: number; // Percentage
minPosition?: number; // Percentage
maxPosition?: number; // Percentage
resizerWidth?: number; // Pixels
onResize?: (position: number) => void;
storageKey?: string; // Save position to localStorage
}
export function SplitPane({
left,
right,
orientation = 'horizontal',
defaultPosition = 50,
minPosition = 20,
maxPosition = 80,
resizerWidth = 4,
onResize,
storageKey
}: SplitPaneProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [position, setPosition] = useState(() => {
if (storageKey) {
const saved = localStorage.getItem(`splitpane-${storageKey}`);
return saved ? parseFloat(saved) : defaultPosition;
}
return defaultPosition;
});
const [isDragging, setIsDragging] = useState(false);
// Save position to localStorage
useEffect(() => {
if (storageKey) {
localStorage.setItem(`splitpane-${storageKey}`, position.toString());
}
}, [position, storageKey]);
// Handle mouse/touch move
const handleMove = useCallback((clientX: number, clientY: number) => {
if (!isDragging || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
let newPosition: number;
if (orientation === 'horizontal') {
const x = clientX - rect.left;
newPosition = (x / rect.width) * 100;
} else {
const y = clientY - rect.top;
newPosition = (y / rect.height) * 100;
}
// Apply constraints
newPosition = Math.max(minPosition, Math.min(maxPosition, newPosition));
setPosition(newPosition);
onResize?.(newPosition);
}, [isDragging, orientation, minPosition, maxPosition, onResize]);
// Mouse events
useEffect(() => {
const handleMouseMove = (e: MouseEvent) => handleMove(e.clientX, e.clientY);
const handleMouseUp = () => setIsDragging(false);
if (isDragging) {
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.body.style.cursor = orientation === 'horizontal' ? 'col-resize' : 'row-resize';
document.body.style.userSelect = 'none';
}
return () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
}, [isDragging, handleMove, orientation]);
// Touch events
const handleTouchMove = (e: React.TouchEvent) => {
if (e.touches.length === 1) {
handleMove(e.touches[0].clientX, e.touches[0].clientY);
}
};
// Keyboard control
const handleKeyDown = (e: React.KeyboardEvent) => {
const step = 5; // 5% per key press
let newPosition = position;
switch (e.key) {
case 'ArrowLeft':
case 'ArrowUp':
e.preventDefault();
newPosition = Math.max(minPosition, position - step);
break;
case 'ArrowRight':
case 'ArrowDown':
e.preventDefault();
newPosition = Math.min(maxPosition, position + step);
break;
case 'Home':
e.preventDefault();
newPosition = minPosition;
break;
case 'End':
e.preventDefault();
newPosition = maxPosition;
break;
default:
return;
}
setPosition(newPosition);
onResize?.(newPosition);
};
const isHorizontal = orientation === 'horizontal';
return (
<div
ref={containerRef}
className={`split-pane split-pane--${orientation}`}
style={{
display: 'flex',
flexDirection: isHorizontal ? 'row' : 'column',
height: '100%',
width: '100%',
position: 'relative'
}}
>
{/* First Pane */}
<div
className="split-pane__first"
style={{
[isHorizontal ? 'width' : 'height']: `${position}%`,
overflow: 'auto',
flexShrink: 0
}}
>
{left}
</div>
{/* Resizer */}
<div
className={`split-pane__resizer ${isDragging ? 'dragging' : ''}`}
style={{
[isHorizontal ? 'width' : 'height']: `${resizerWidth}px`,
cursor: isHorizontal ? 'col-resize' : 'row-resize',
flexShrink: 0
}}
role="separator"
tabIndex={0}
aria-valuenow={Math.round(position)}
aria-valuemin={minPosition}
aria-valuemax={maxPosition}
aria-label={`Resize panes, current position ${Math.round(position)}%`}
onMouseDown={() => setIsDragging(true)}
onTouchStart={() => setIsDragging(true)}
onTouchMove={handleTouchMove}
onTouchEnd={() => setIsDragging(false)}
onKeyDown={handleKeyDown}
/>
{/* Second Pane */}
<div
className="split-pane__second"
style={{
flex: 1,
overflow: 'auto',
minWidth: 0,
minHeight: 0
}}
>
{right}
</div>
</div>
);
}
/**
* Code Editor Split Pane Example
*/
export function CodeEditorLayout() {
const [activeFile, setActiveFile] = useState('index.tsx');
const fileTree = (
<div className="file-tree">
<h3>Files</h3>
<ul>
<li>
<button
className={activeFile === 'index.tsx' ? 'active' : ''}
onClick={() => setActiveFile('index.tsx')}
>
📄 index.tsx
</button>
</li>
<li>
<button
className={activeFile === 'styles.css' ? 'active' : ''}
onClick={() => setActiveFile('styles.css')}
>
🎨 styles.css
</button>
</li>
<li>
<button
className={activeFile === 'README.md' ? 'active' : ''}
onClick={() => setActiveFile('README.md')}
>
📝 README.md
</button>
</li>
</ul>
</div>
);
const editor = (
<div className="code-editor">
<div className="editor-header">
<span className="editor-title">{activeFile}</span>
<div className="editor-actions">
<button aria-label="Format code">Format</button>
<button aria-label="Save file">Save</button>
</div>
</div>
<div className="editor-content">
<pre>
<code>{`// ${activeFile} content would go here
export function Component() {
return <div>Hello World</div>;
}`}</code>
</pre>
</div>
</div>
);
const preview = (
<div className="preview-pane">
<h3>Preview</h3>
<div className="preview-content">
<p>Live preview would render here</p>
</div>
</div>
);
return (
<div style={{ height: '600px' }}>
<SplitPane
left={
<SplitPane
left={fileTree}
right={editor}
defaultPosition={25}
minPosition={15}
maxPosition={40}
storageKey="file-tree"
/>
}
right={preview}
defaultPosition={60}
minPosition={40}
maxPosition={80}
storageKey="editor-preview"
/>
</div>
);
}
/**
* Comparison View Example
*/
export function ComparisonView({
before,
after
}: {
before: React.ReactNode;
after: React.ReactNode;
}) {
const [syncScroll, setSyncScroll] = useState(true);
const leftRef = useRef<HTMLDivElement>(null);
const rightRef = useRef<HTMLDivElement>(null);
// Sync scrolling between panes
const handleScroll = (source: 'left' | 'right') => {
if (!syncScroll) return;
const sourceRef = source === 'left' ? leftRef : rightRef;
const targetRef = source === 'left' ? rightRef : leftRef;
if (sourceRef.current && targetRef.current) {
targetRef.current.scrollTop = sourceRef.current.scrollTop;
targetRef.current.scrollLeft = sourceRef.current.scrollLeft;
}
};
return (
<div className="comparison-view">
<div className="comparison-header">
<button
className={`sync-button ${syncScroll ? 'active' : ''}`}
onClick={() => setSyncScroll(!syncScroll)}
aria-pressed={syncScroll}
>
🔗 Sync Scroll
</button>
</div>
<SplitPane
left={
<div className="comparison-pane">
<h3 className="comparison-title">Before</h3>
<div
ref={leftRef}
className="comparison-content"
onScroll={() => handleScroll('left')}
>
{before}
</div>
</div>
}
right={
<div className="comparison-pane">
<h3 className="comparison-title">After</h3>
<div
ref={rightRef}
className="comparison-content"
onScroll={() => handleScroll('right')}
>
{after}
</div>
</div>
}
defaultPosition={50}
minPosition={30}
maxPosition={70}
/>
</div>
);
}
// CSS Styles
const styles = `
/* Split Pane Base Styles */
.split-pane {
display: flex;
height: 100%;
width: 100%;
position: relative;
}
.split-pane--horizontal {
flex-direction: row;
}
.split-pane--vertical {
flex-direction: column;
}
.split-pane__first,
.split-pane__second {
overflow: auto;
}
/* Resizer Styles */
.split-pane__resizer {
background: var(--border-color);
position: relative;
flex-shrink: 0;
transition: background-color 0.2s;
}
.split-pane--horizontal .split-pane__resizer {
width: 4px;
cursor: col-resize;
}
.split-pane--vertical .split-pane__resizer {
height: 4px;
cursor: row-resize;
}
.split-pane__resizer:hover,
.split-pane__resizer:focus {
background: var(--color-primary);
}
.split-pane__resizer.dragging {
background: var(--color-primary);
}
/* Increase hit area for better UX */
.split-pane__resizer::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -8px;
right: -8px;
}
.split-pane--vertical .split-pane__resizer::after {
left: 0;
right: 0;
top: -8px;
bottom: -8px;
}
/* Focus styles */
.split-pane__resizer:focus {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* Code Editor Example Styles */
.file-tree {
background: var(--color-background);
padding: var(--spacing-md);
height: 100%;
}
.file-tree h3 {
margin: 0 0 var(--spacing-md) 0;
font-size: var(--text-sm);
text-transform: uppercase;
opacity: 0.7;
}
.file-tree ul {
list-style: none;
padding: 0;
margin: 0;
}
.file-tree button {
width: 100%;
text-align: left;
padding: var(--spacing-sm);
border: none;
background: transparent;
cursor: pointer;
border-radius: var(--radius-sm);
transition: background-color 0.2s;
}
.file-tree button:hover {
background: var(--color-hover);
}
.file-tree button.active {
background: var(--color-primary-light);
color: var(--color-primary);
font-weight: 500;
}
.code-editor {
height: 100%;
display: flex;
flex-direction: column;
background: var(--color-surface);
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-sm) var(--spacing-md);
border-bottom: 1px solid var(--border-color);
background: var(--color-background);
}
.editor-content {
flex: 1;
padding: var(--spacing-md);
overflow: auto;
font-family: 'Courier New', monospace;
}
.preview-pane {
padding: var(--spacing-md);
height: 100%;
background: var(--color-surface);
}
/* Comparison View Styles */
.comparison-view {
height: 100%;
display: flex;
flex-direction: column;
}
.comparison-header {
padding: var(--spacing-sm);
background: var(--color-background);
border-bottom: 1px solid var(--border-color);
}
.sync-button {
padding: var(--spacing-xs) var(--spacing-sm);
border: 1px solid var(--border-color);
background: var(--color-surface);
border-radius: var(--radius-sm);
cursor: pointer;
transition: all 0.2s;
}
.sync-button.active {
background: var(--color-primary);
color: white;
border-color: var(--color-primary);
}
.comparison-pane {
height: 100%;
display: flex;
flex-direction: column;
}
.comparison-title {
margin: 0;
padding: var(--spacing-sm) var(--spacing-md);
background: var(--color-background);
border-bottom: 1px solid var(--border-color);
font-size: var(--text-sm);
text-transform: uppercase;
opacity: 0.7;
}
.comparison-content {
flex: 1;
padding: var(--spacing-md);
overflow: auto;
}
/* Dark mode support */
@media (prefers-color-scheme: dark) {
.split-pane__resizer {
background: rgba(255, 255, 255, 0.1);
}
.split-pane__resizer:hover,
.split-pane__resizer.dragging {
background: rgba(255, 255, 255, 0.3);
}
}
/* Mobile responsive */
@media (max-width: 768px) {
.split-pane--horizontal {
flex-direction: column;
}
.split-pane__first {
width: 100% !important;
height: 50%;
}
.split-pane__resizer {
display: none;
}
}
`;skill: "designing-layouts"
version: "1.0"
domain: "frontend"
# Base outputs required for all layout implementations
base_outputs:
- path: "layouts/"
must_contain: ["components/", "styles/"]
reason: "Layout component and style directory structure"
- path: "layouts/styles/breakpoints.css"
must_contain: ["@media", "min-width"]
reason: "Responsive breakpoint definitions using mobile-first approach"
- path: "layouts/styles/spacing.css"
must_contain: ["--spacing-", "margin", "padding"]
reason: "Consistent spacing system using design tokens"
# Conditional outputs based on configuration
conditional_outputs:
maturity:
starter:
- path: "layouts/components/Container.tsx"
must_contain: ["max-width", "padding", "margin: 0 auto"]
reason: "Basic centered container component"
- path: "layouts/styles/grid.css"
must_contain: ["display: grid", "grid-template-columns", "gap"]
reason: "Basic CSS Grid layout patterns"
- path: "layouts/styles/flexbox.css"
must_contain: ["display: flex", "justify-content", "align-items"]
reason: "Basic Flexbox layout patterns"
- path: "examples/simple-grid.tsx"
must_contain: ["grid-container", "auto-fit", "minmax"]
reason: "Simple responsive grid example"
intermediate:
- path: "layouts/components/Container.tsx"
must_contain: ["max-width", "padding", "className"]
reason: "Container with customizable props"
- path: "layouts/components/Grid.tsx"
must_contain: ["grid-template-columns", "gap", "responsive"]
reason: "Reusable grid component with responsive props"
- path: "layouts/components/Flex.tsx"
must_contain: ["display: flex", "direction", "align", "justify"]
reason: "Reusable flexbox component with utility props"
- path: "layouts/components/Stack.tsx"
must_contain: ["flex-direction: column", "gap", "children"]
reason: "Vertical/horizontal stack component"
- path: "layouts/styles/breakpoints.css"
must_contain: ["640px", "768px", "1024px", "1280px"]
reason: "Standard 5-tier breakpoint system (sm, md, lg, xl, 2xl)"
- path: "examples/admin-layout.tsx"
must_contain: ["sidebar", "header", "main", "nav"]
reason: "Admin dashboard layout with sidebar and header"
- path: "examples/responsive-grid.tsx"
must_contain: ["auto-fit", "minmax", "@media"]
reason: "Auto-responsive grid system"
- path: "scripts/generate_breakpoints.js"
must_contain: ["mobile-first", "min-width"]
reason: "Generate responsive breakpoint configurations"
advanced:
- path: "layouts/components/AdminLayout.tsx"
must_contain: ["sidebar", "header", "collapsible", "responsive"]
reason: "Complete admin layout with collapsible sidebar"
- path: "layouts/components/SplitPane.tsx"
must_contain: ["resizable", "drag", "min-width", "max-width"]
reason: "Resizable split-pane interface"
- path: "layouts/components/MasonryGrid.tsx"
must_contain: ["masonry", "column-count", "break-inside"]
reason: "Pinterest-style masonry layout"
- path: "layouts/components/HolyGrail.tsx"
must_contain: ["flex", "header", "nav", "main", "aside", "footer"]
reason: "Holy Grail layout pattern"
- path: "layouts/styles/container-queries.css"
must_contain: ["container-type", "@container", "inline-size"]
reason: "Container query patterns for component-responsive design"
- path: "layouts/styles/fluid-typography.css"
must_contain: ["clamp", "calc", "vw", "font-size"]
reason: "Fluid typography scales between breakpoints"
- path: "scripts/generate_breakpoints.js"
must_contain: ["mobile-first", "desktop-first", "min-width", "max-width"]
reason: "Configurable breakpoint generation (mobile-first/desktop-first)"
- path: "scripts/calculate_fluid_typography.js"
must_contain: ["clamp", "min-vw", "max-vw", "slope"]
reason: "Calculate fluid typography scales"
- path: "scripts/validate_layout_accessibility.js"
must_contain: ["landmarks", "role", "aria", "semantic HTML"]
reason: "Validate semantic HTML and ARIA landmarks"
- path: "examples/split-pane.tsx"
must_contain: ["resizable", "onDragStart", "onDrag", "onDragEnd"]
reason: "Resizable split-pane implementation"
- path: "examples/masonry-layout.tsx"
must_contain: ["masonry", "column-gap", "break-inside: avoid"]
reason: "CSS-based masonry grid"
frontend_framework:
react:
- path: "layouts/components/Container.tsx"
must_contain: ["React", "interface", "children: React.ReactNode"]
reason: "TypeScript React container component"
- path: "layouts/components/Grid.tsx"
must_contain: ["React.FC", "props", "className"]
reason: "React grid component with prop interface"
- path: "layouts/components/Flex.tsx"
must_contain: ["React", "FlexProps", "direction", "align"]
reason: "React flexbox component with typed props"
- path: "examples/admin-layout.tsx"
must_contain: ["useState", "useEffect", "React"]
reason: "React admin layout with hooks"
vue:
- path: "layouts/components/Container.vue"
must_contain: ["<template>", "<script setup>", "defineProps"]
reason: "Vue 3 Composition API container component"
- path: "layouts/components/Grid.vue"
must_contain: ["<template>", "props", "computed"]
reason: "Vue grid component with reactive props"
- path: "examples/AdminLayout.vue"
must_contain: ["<template>", "ref", "onMounted"]
reason: "Vue admin layout with Composition API"
svelte:
- path: "layouts/components/Container.svelte"
must_contain: ["<script>", "export let", "slot"]
reason: "Svelte container component with props"
- path: "layouts/components/Grid.svelte"
must_contain: ["<script>", "export let columns", "class:"]
reason: "Svelte grid component with reactive classes"
vanilla:
- path: "layouts/vanilla/grid.html"
must_contain: ["<div class=\"grid\"", "display: grid"]
reason: "Vanilla HTML/CSS grid implementation"
- path: "layouts/vanilla/admin.html"
must_contain: ["<aside", "<nav", "<main", "sidebar"]
reason: "Vanilla HTML admin layout structure"
styling:
tailwind:
- path: "tailwind.config.js"
must_contain: ["theme:", "extend:", "screens:"]
reason: "Tailwind configuration with custom breakpoints and spacing"
- path: "layouts/components/Container.tsx"
must_contain: ["className", "container", "mx-auto", "px-"]
reason: "Tailwind utility classes for container"
- path: "examples/admin-layout.tsx"
must_contain: ["flex", "grid", "gap-", "p-", "md:"]
reason: "Tailwind responsive utilities in layout"
- path: "tailwind.config.js"
must_contain: ["spacing:", "var(--spacing-"]
reason: "Tailwind integrated with design tokens"
css_modules:
- path: "layouts/components/Container.module.css"
must_contain: [".container", "max-width", "var(--"]
reason: "CSS Modules with design token integration"
- path: "layouts/components/Grid.module.css"
must_contain: [".grid", "grid-template-columns", "gap"]
reason: "Grid styles in CSS Modules"
- path: "examples/AdminLayout.module.css"
must_contain: [".layout", ".sidebar", ".main", "@media"]
reason: "Admin layout CSS Modules with responsive styles"
styled_components:
- path: "layouts/components/Container.tsx"
must_contain: ["styled.div", "max-width", "props.theme"]
reason: "Styled-components container with theme"
- path: "layouts/components/Grid.tsx"
must_contain: ["styled.div", "grid-template-columns", "${props =>"]
reason: "Dynamic grid with styled-components props"
- path: "layouts/components/breakpoints.ts"
must_contain: ["export const breakpoints", "sm:", "md:", "lg:"]
reason: "Breakpoint definitions for styled-components media queries"
scss:
- path: "layouts/styles/_variables.scss"
must_contain: ["$breakpoint-", "$spacing-", "$container-"]
reason: "SCSS variables from design tokens"
- path: "layouts/styles/_mixins.scss"
must_contain: ["@mixin", "media-query", "container"]
reason: "SCSS mixins for responsive utilities"
- path: "layouts/components/grid.scss"
must_contain: [".grid", "display: grid", "@include media-query"]
reason: "Grid styles using SCSS mixins"
# Scaffolding files that should be created as starting points
scaffolding:
- path: "layouts/components/Container.tsx"
reason: "Initialize centered container component"
- path: "layouts/components/Grid.tsx"
reason: "Initialize reusable grid component"
- path: "layouts/components/Flex.tsx"
reason: "Initialize flexbox utility component"
- path: "layouts/components/Stack.tsx"
reason: "Initialize vertical/horizontal stack component"
- path: "layouts/styles/breakpoints.css"
reason: "Initialize responsive breakpoint system"
- path: "layouts/styles/spacing.css"
reason: "Initialize spacing utilities"
- path: "layouts/styles/grid.css"
reason: "Initialize CSS Grid patterns"
- path: "layouts/styles/flexbox.css"
reason: "Initialize Flexbox patterns"
- path: "assets/breakpoint-config.json"
reason: "Standard breakpoint configurations template"
- path: "assets/layout-templates.json"
reason: "Common layout templates (admin, landing, docs)"
- path: "assets/spacing-scale.json"
reason: "Spacing system configuration"
- path: "README.md"
reason: "Document layout system usage and patterns"
# Metadata
metadata:
primary_blueprints: ["dashboard", "frontend"]
contributes_to:
- "Responsive layout systems (Grid, Flexbox, Container Queries)"
- "Layout components (Container, Grid, Flex, Stack)"
- "Admin dashboards with sidebars and headers"
- "Mobile-first responsive design"
- "Spacing and breakpoint systems"
common_patterns:
- "Mobile-first media queries (@media min-width)"
- "CSS Grid for two-dimensional layouts"
- "Flexbox for one-dimensional layouts and alignment"
- "Container queries for component-responsive design"
- "12-column grid system with flexible columns"
- "Auto-fit/auto-fill responsive grids"
- "Holy Grail layout pattern (header, nav, main, aside, footer)"
- "Collapsible sidebar with responsive behavior"
- "Semantic HTML with landmark ARIA roles"
- "Design token integration for spacing and breakpoints"
integration_points:
design_tokens: "All spacing, breakpoints, and container widths use design tokens"
dashboards: "Admin layouts provide structure for dashboard components"
navigation: "Sidebar and header integrate with navigation components"
forms: "Form layouts use grid and flex components"
data_viz: "Grid layouts for chart galleries and dashboards"
tables: "Container components wrap table grids"
typical_directory_structure: |
project/
├── layouts/ # Layout components and styles
│ ├── components/ # Reusable layout components
│ │ ├── Container.tsx # Centered container with max-width
│ │ ├── Grid.tsx # CSS Grid wrapper component
│ │ ├── Flex.tsx # Flexbox wrapper component
│ │ ├── Stack.tsx # Vertical/horizontal stack
│ │ ├── AdminLayout.tsx # Complete admin layout
│ │ ├── SplitPane.tsx # Resizable split-pane
│ │ ├── MasonryGrid.tsx # Masonry layout
│ │ └── HolyGrail.tsx # Holy Grail pattern
│ └── styles/ # Layout CSS
│ ├── breakpoints.css # Responsive breakpoints
│ ├── spacing.css # Spacing utilities
│ ├── grid.css # Grid patterns
│ ├── flexbox.css # Flexbox patterns
│ ├── container-queries.css # Container query patterns
│ └── fluid-typography.css # Fluid type scales
├── scripts/ # Layout utilities
│ ├── generate_breakpoints.js # Generate breakpoint configs
│ ├── calculate_fluid_typography.js # Calculate fluid scales
│ └── validate_layout_accessibility.js # Validate semantic HTML
├── examples/ # Usage examples
│ ├── admin-layout.tsx # Admin dashboard layout
│ ├── responsive-grid.tsx # Auto-responsive grid
│ ├── masonry-layout.tsx # Pinterest-style layout
│ └── split-pane.tsx # Resizable split-pane
├── assets/ # Configuration templates
│ ├── breakpoint-config.json # Breakpoint definitions
│ ├── layout-templates.json # Layout presets
│ └── spacing-scale.json # Spacing scale config
├── references/ # Documentation
│ ├── layout-patterns.md # Common layout patterns
│ ├── responsive-strategies.md # Mobile-first design
│ ├── css-techniques.md # Modern CSS features
│ ├── accessibility-layouts.md # Semantic HTML/ARIA
│ ├── library-guide.md # Framework integration
│ └── performance-optimization.md # CSS performance
└── README.md # Project overview
tools_required:
- name: "Node.js"
version: "^18.0.0"
purpose: "Run generation and validation scripts"
- name: "TypeScript"
version: "^5.0.0"
purpose: "Type-safe React/Vue component development"
install: "npm install typescript"
validation_checks:
- "Mobile-first approach (min-width media queries)"
- "Semantic HTML (header, nav, main, aside, footer)"
- "ARIA landmarks for accessibility (role=\"navigation\", role=\"main\")"
- "Container max-width uses design tokens"
- "Spacing uses consistent scale from design tokens"
- "Breakpoints match design system (640px, 768px, 1024px, 1280px, 1536px)"
- "Grid gap uses design token spacing"
- "Flexbox components have accessible min-height (44px touch targets)"
- "Skip links for keyboard navigation"
- "Responsive behavior tested at all breakpoints"
anti_patterns:
- name: "Desktop-first media queries"
avoid: "@media (max-width: 768px)"
use: "@media (min-width: 768px) for mobile-first"
- name: "Hardcoded spacing values"
avoid: "padding: 16px"
use: "padding: var(--spacing-md)"
- name: "Div soup without semantic HTML"
avoid: "<div><div><div>Content</div></div></div>"
use: "<header><nav><main> with landmark roles"
- name: "Pixel-based container widths"
avoid: "max-width: 1200px"
use: "max-width: var(--container-max-width)"
- name: "Missing skip links"
avoid: "No keyboard navigation shortcuts"
use: "<a href=\"#main\" class=\"skip-link\">Skip to content</a>"
- name: "Non-responsive layouts"
avoid: "Fixed widths with no media queries"
use: "Fluid layouts with responsive breakpoints"
- name: "Physical CSS properties only"
avoid: "margin-left, padding-right"
use: "margin-inline-start, padding-inline-end for RTL support"
- name: "Inaccessible touch targets"
avoid: "Button height: 32px (too small for mobile)"
use: "min-height: 44px (WCAG AAA touch target size)"
Accessibility in Layouts
Table of Contents
- Semantic HTML Structure
- Landmark Regions
- Skip Links
- Focus Management
- Focus Trap for Modals
- Focus Restoration
- ARIA Attributes
- Live Regions
- Expanded/Collapsed States
- Describedby and Labelledby
- Keyboard Navigation
- Keyboard Patterns
- Grid Navigation
- Responsive Accessibility
- Touch Target Sizing
- Focus Indicators
- Screen Reader Helpers
- Visually Hidden
- Announcing Changes
- Testing Accessibility
- Accessibility Checklist
- Testing Tools
- Common Patterns
- Responsive Table
Semantic HTML Structure
Landmark Regions
Use semantic HTML5 elements to create accessible page structure.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Accessible Layout</title>
</head>
<body>
<!-- Skip Links -->
<a href="#main" class="skip-link">Skip to main content</a>
<a href="#nav" class="skip-link">Skip to navigation</a>
<!-- Header Landmark -->
<header role="banner">
<h1>Site Title</h1>
<!-- Site-wide header content -->
</header>
<!-- Navigation Landmark -->
<nav id="nav" role="navigation" aria-label="Main navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li aria-current="page"><a href="/contact">Contact</a></li>
</ul>
</nav>
<!-- Main Content Landmark -->
<main id="main" role="main">
<article>
<h2>Article Title</h2>
<!-- Main content -->
</article>
<!-- Complementary Content -->
<aside role="complementary" aria-label="Related information">
<!-- Sidebar content -->
</aside>
</main>
<!-- Footer Landmark -->
<footer role="contentinfo">
<!-- Site-wide footer content -->
</footer>
</body>
</html>Skip Links
Essential for keyboard navigation to bypass repetitive content.
/* Skip link styling */
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--primary-color);
color: white;
padding: 8px;
text-decoration: none;
z-index: 100;
border-radius: 0 0 4px 0;
}
.skip-link:focus {
top: 0;
}
/* Ensure focus is visible */
:focus {
outline: 2px solid var(--focus-color);
outline-offset: 2px;
}
/* Never remove focus indicators without providing alternative */
:focus:not(:focus-visible) {
outline: 2px solid transparent;
}
:focus-visible {
outline: 2px solid var(--focus-color);
outline-offset: 2px;
}Focus Management
Focus Trap for Modals
export function trapFocus(element: HTMLElement) {
const focusableElements = element.querySelectorAll(
'a[href], button, textarea, input, select, [tabindex]:not([tabindex="-1"])'
);
const firstFocusable = focusableElements[0] as HTMLElement;
const lastFocusable = focusableElements[focusableElements.length - 1] as HTMLElement;
function handleKeyDown(e: KeyboardEvent) {
if (e.key !== 'Tab') return;
if (e.shiftKey) {
// Shift + Tab
if (document.activeElement === firstFocusable) {
e.preventDefault();
lastFocusable.focus();
}
} else {
// Tab
if (document.activeElement === lastFocusable) {
e.preventDefault();
firstFocusable.focus();
}
}
}
element.addEventListener('keydown', handleKeyDown);
firstFocusable?.focus();
return () => {
element.removeEventListener('keydown', handleKeyDown);
};
}Focus Restoration
export function useFocusRestore() {
const previousFocus = useRef<HTMLElement | null>(null);
useEffect(() => {
previousFocus.current = document.activeElement as HTMLElement;
return () => {
previousFocus.current?.focus();
};
}, []);
return previousFocus;
}
// Usage in modal component
function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null);
const previousFocus = useFocusRestore();
useEffect(() => {
if (isOpen && modalRef.current) {
const cleanup = trapFocus(modalRef.current);
return cleanup;
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
>
{children}
</div>
);
}ARIA Attributes
Live Regions
<!-- Announce changes to screen readers -->
<div aria-live="polite" aria-atomic="true">
<p>Form saved successfully</p>
</div>
<!-- Urgent announcements -->
<div role="alert" aria-live="assertive">
<p>Error: Invalid email address</p>
</div>
<!-- Status updates -->
<div role="status" aria-live="polite">
<p>3 items in your cart</p>
</div>Expanded/Collapsed States
function CollapsibleSection({ title, children }: CollapsibleProps) {
const [isExpanded, setIsExpanded] = useState(false);
const contentId = useId();
return (
<section>
<button
aria-expanded={isExpanded}
aria-controls={contentId}
onClick={() => setIsExpanded(!isExpanded)}
>
{title}
<span aria-hidden="true">{isExpanded ? '▼' : '▶'}</span>
</button>
<div
id={contentId}
hidden={!isExpanded}
aria-hidden={!isExpanded}
>
{children}
</div>
</section>
);
}Describedby and Labelledby
<!-- Complex labeling -->
<div role="group" aria-labelledby="billing-label" aria-describedby="billing-desc">
<h3 id="billing-label">Billing Address</h3>
<p id="billing-desc">Enter the address associated with your payment method</p>
<label for="street">Street Address</label>
<input id="street" type="text" required>
<label for="city">City</label>
<input id="city" type="text" required>
</div>
<!-- Error messages -->
<label for="email">Email</label>
<input
id="email"
type="email"
aria-invalid="true"
aria-describedby="email-error"
required
>
<span id="email-error" role="alert">Please enter a valid email address</span>Keyboard Navigation
Keyboard Patterns
// Roving tabindex for lists
export function useRovingTabIndex(items: string[]) {
const [focusedIndex, setFocusedIndex] = useState(0);
const handleKeyDown = (e: KeyboardEvent, index: number) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault();
setFocusedIndex((index + 1) % items.length);
break;
case 'ArrowUp':
e.preventDefault();
setFocusedIndex((index - 1 + items.length) % items.length);
break;
case 'Home':
e.preventDefault();
setFocusedIndex(0);
break;
case 'End':
e.preventDefault();
setFocusedIndex(items.length - 1);
break;
}
};
return { focusedIndex, handleKeyDown };
}
// Usage
function NavigationList({ items }: { items: string[] }) {
const { focusedIndex, handleKeyDown } = useRovingTabIndex(items);
return (
<ul role="menu">
{items.map((item, index) => (
<li key={item} role="none">
<button
role="menuitem"
tabIndex={index === focusedIndex ? 0 : -1}
onKeyDown={(e) => handleKeyDown(e, index)}
ref={(el) => {
if (index === focusedIndex) el?.focus();
}}
>
{item}
</button>
</li>
))}
</ul>
);
}Grid Navigation
// Arrow key navigation for grids
function GridNavigation({ rows, cols, children }: GridNavProps) {
const [focusedCell, setFocusedCell] = useState({ row: 0, col: 0 });
const handleKeyDown = (e: KeyboardEvent) => {
const { row, col } = focusedCell;
switch (e.key) {
case 'ArrowRight':
setFocusedCell({ row, col: Math.min(col + 1, cols - 1) });
break;
case 'ArrowLeft':
setFocusedCell({ row, col: Math.max(col - 1, 0) });
break;
case 'ArrowDown':
setFocusedCell({ row: Math.min(row + 1, rows - 1), col });
break;
case 'ArrowUp':
setFocusedCell({ row: Math.max(row - 1, 0), col });
break;
}
};
return (
<div role="grid" onKeyDown={handleKeyDown}>
{children}
</div>
);
}Responsive Accessibility
Touch Target Sizing
/* Minimum 44x44px touch targets (WCAG 2.1) */
.button,
.link,
.interactive {
min-width: 44px;
min-height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
}
/* Increase tap area without visual size */
.small-button {
position: relative;
padding: 8px 12px;
}
.small-button::after {
content: '';
position: absolute;
top: -8px;
right: -8px;
bottom: -8px;
left: -8px;
}Focus Indicators
/* High contrast focus indicators */
.focus-visible {
outline: 3px solid var(--focus-color);
outline-offset: 2px;
}
/* Dark mode focus */
@media (prefers-color-scheme: dark) {
.focus-visible {
outline-color: var(--focus-color-dark);
}
}
/* Focus within containers */
.card:focus-within {
box-shadow: 0 0 0 3px var(--focus-color);
}Screen Reader Helpers
Visually Hidden
/* Hide visually but not from screen readers */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Show on focus */
.sr-only-focusable:focus {
position: static;
width: auto;
height: auto;
padding: inherit;
margin: inherit;
overflow: visible;
clip: auto;
white-space: normal;
}Announcing Changes
// Announce dynamic content changes
function useAnnounce() {
const [announcement, setAnnouncement] = useState('');
const announce = (message: string) => {
setAnnouncement('');
setTimeout(() => setAnnouncement(message), 100);
};
return {
announce,
announcer: (
<div
role="status"
aria-live="polite"
aria-atomic="true"
className="sr-only"
>
{announcement}
</div>
)
};
}Testing Accessibility
Accessibility Checklist
- [ ] All interactive elements are keyboard accessible
- [ ] Tab order follows logical reading order
- [ ] Focus indicators are visible
- [ ] Skip links are provided
- [ ] Landmarks are properly labeled
- [ ] ARIA attributes are correctly used
- [ ] Touch targets meet 44x44px minimum
- [ ] Color contrast meets WCAG standards
- [ ] Content reflows at 400% zoom
- [ ] No horizontal scrolling at 320px width
Testing Tools
// Automated testing with jest-axe
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('should not have accessibility violations', async () => {
const { container } = render(<Layout />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});Common Patterns
Responsive Table
/* Accessible responsive table */
@media (max-width: 768px) {
.table {
border: 0;
}
.table thead {
border: none;
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.table tr {
border-bottom: 3px solid var(--border-color);
display: block;
margin-bottom: 1rem;
}
.table td {
border-bottom: 1px solid var(--border-color);
display: block;
text-align: right;
}
.table td::before {
content: attr(data-label);
float: left;
font-weight: bold;
}
}Modern CSS Techniques
Table of Contents
- CSS Grid
- Grid Fundamentals
- Advanced Grid Patterns
- Grid Alignment
- Flexbox
- Flexbox Patterns
- Advanced Flexbox
- Container Queries
- Basic Container Queries
- Container Query Units
- Nested Containers
- CSS Custom Properties (Variables)
- Dynamic Properties
- Property Manipulation
- Modern Layout Features
- Aspect Ratio
- Logical Properties
- CSS Subgrid
- Scroll Snap
- CSS Math Functions
- Clamp, Min, Max
- Calc with Custom Properties
- Performance Techniques
- Content Visibility
- CSS Containment
- Will-Change
CSS Grid
Grid Fundamentals
/* Basic grid setup */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: auto 1fr auto;
gap: var(--grid-gap);
}
/* Named grid lines */
.grid-advanced {
display: grid;
grid-template-columns:
[full-start] minmax(1rem, 1fr)
[content-start] minmax(0, 80ch)
[content-end] minmax(1rem, 1fr)
[full-end];
}
/* Grid template areas */
.app-layout {
display: grid;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
grid-template-columns: 200px 1fr 200px;
grid-template-rows: auto 1fr auto;
min-height: 100vh;
}
.header { grid-area: header; }
.nav { grid-area: nav; }
.main { grid-area: main; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }Advanced Grid Patterns
/* Auto-fit with minimum size */
.auto-grid {
display: grid;
grid-template-columns: repeat(
auto-fit,
minmax(min(250px, 100%), 1fr)
);
gap: var(--grid-gap);
}
/* Asymmetric grid */
.featured-grid {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: var(--grid-gap);
}
.featured-grid > :nth-child(1) {
grid-column: span 4;
grid-row: span 2;
}
.featured-grid > :nth-child(2),
.featured-grid > :nth-child(3) {
grid-column: span 2;
}
/* RAM (Repeat, Auto, Minmax) pattern */
.ram-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
gap: clamp(1rem, 2vw, 2rem);
}Grid Alignment
/* Alignment within grid */
.grid-aligned {
display: grid;
place-items: center; /* Centers all items */
/* Or individually */
justify-items: start; /* Horizontal alignment */
align-items: end; /* Vertical alignment */
/* Or for the whole grid */
justify-content: space-between;
align-content: center;
}
/* Individual item alignment */
.grid-item {
justify-self: end;
align-self: center;
}Flexbox
Flexbox Patterns
/* Centering */
.center-flex {
display: flex;
justify-content: center;
align-items: center;
}
/* Space between with alignment */
.nav-flex {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-md);
}
/* Flex grow/shrink/basis */
.flex-item {
flex: 1 1 300px; /* grow shrink basis */
}
/* Sticky footer with flexbox */
.page {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.page__content {
flex: 1;
}
.page__footer {
flex-shrink: 0;
}Advanced Flexbox
/* Equal height columns */
.card-container {
display: flex;
gap: var(--spacing-md);
}
.card {
flex: 1;
display: flex;
flex-direction: column;
}
.card__content {
flex: 1;
}
/* Wrapping with basis */
.flex-wrap {
display: flex;
flex-wrap: wrap;
gap: var(--spacing-md);
}
.flex-wrap > * {
flex: 1 1 calc(33.333% - var(--spacing-md));
min-width: 250px;
}
/* Order manipulation */
.flex-reorder {
display: flex;
}
.flex-reorder__important {
order: -1; /* Moves to front */
}
@media (max-width: 768px) {
.flex-reorder__sidebar {
order: 1; /* Moves to end on mobile */
}
}Container Queries
Basic Container Queries
/* Define container */
.card-container {
container-type: inline-size;
container-name: card;
}
/* Query container size */
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 150px 1fr;
}
}
@container card (min-width: 700px) {
.card {
grid-template-columns: 200px 1fr 150px;
}
}
/* Style queries */
@container style(--theme: dark) {
.card {
background: var(--dark-background);
color: var(--dark-text);
}
}Container Query Units
/* Container query units */
.responsive-text {
/* cqw = container query width */
font-size: clamp(1rem, 5cqw, 2rem);
/* cqh = container query height */
padding: 2cqh 4cqw;
/* cqi = container query inline size */
/* cqb = container query block size */
margin: 2cqb 4cqi;
/* cqmin/cqmax */
width: 80cqmin; /* 80% of smaller dimension */
}Nested Containers
.outer-container {
container-type: inline-size;
container-name: outer;
}
.inner-container {
container-type: inline-size;
container-name: inner;
}
@container outer (min-width: 800px) {
.outer-content {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
@container inner (min-width: 400px) {
.inner-content {
font-size: 1.2rem;
}
}CSS Custom Properties (Variables)
Dynamic Properties
/* Scoped custom properties */
.component {
--component-spacing: var(--spacing-md);
--component-radius: 8px;
--component-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
padding: var(--component-spacing);
border-radius: var(--component-radius);
box-shadow: var(--component-shadow);
}
/* Computed properties */
.dynamic {
--columns: 3;
--gap: 20px;
--total-gap: calc((var(--columns) - 1) * var(--gap));
--column-width: calc((100% - var(--total-gap)) / var(--columns));
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
gap: var(--gap);
}
/* Fallback values */
.safe-properties {
color: var(--text-color, #333);
background: var(--bg-color, white);
padding: var(--padding, 1rem);
}Property Manipulation
/* Space toggles */
.theme-switch {
--theme-primary: var(--light-primary);
--theme-background: var(--light-background);
--theme-text: var(--light-text);
}
.theme-switch[data-theme="dark"] {
--theme-primary: var(--dark-primary);
--theme-background: var(--dark-background);
--theme-text: var(--dark-text);
}
/* Responsive properties */
.responsive-var {
--size: 1rem;
}
@media (min-width: 768px) {
.responsive-var {
--size: 1.5rem;
}
}
@media (min-width: 1024px) {
.responsive-var {
--size: 2rem;
}
}
.responsive-var > * {
font-size: var(--size);
padding: calc(var(--size) * 0.5);
}Modern Layout Features
Aspect Ratio
/* Maintain aspect ratios */
.aspect-ratio {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}
.square {
aspect-ratio: 1;
}
.golden-ratio {
aspect-ratio: 1.618;
}
/* Responsive aspect ratio */
.responsive-aspect {
aspect-ratio: 1;
}
@media (min-width: 768px) {
.responsive-aspect {
aspect-ratio: 4 / 3;
}
}
@media (min-width: 1024px) {
.responsive-aspect {
aspect-ratio: 16 / 9;
}
}Logical Properties
/* Physical properties (avoid) */
.old-way {
margin-left: 1rem;
padding-right: 2rem;
border-top: 1px solid;
}
/* Logical properties (preferred) */
.modern-way {
margin-inline-start: 1rem;
padding-inline-end: 2rem;
border-block-start: 1px solid;
}
/* Shorthand logical properties */
.logical-shorthand {
margin-inline: auto; /* left and right */
padding-block: 2rem; /* top and bottom */
/* Equivalent to width/height in horizontal writing */
inline-size: 100%;
block-size: 50vh;
/* Min/max logical */
min-inline-size: 250px;
max-block-size: 400px;
}CSS Subgrid
/* Parent grid */
.parent-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
grid-template-rows: repeat(4, minmax(100px, auto));
gap: 1rem;
}
/* Child inherits parent grid */
.subgrid-item {
display: grid;
grid-column: span 6;
grid-row: span 2;
/* Inherits parent's columns and rows */
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
/* Nested elements align to parent grid */
.subgrid-item > * {
grid-column: span 2;
}Scroll Snap
/* Horizontal scroll snap */
.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
gap: 1rem;
}
.carousel__item {
flex: 0 0 100%;
scroll-snap-align: start;
}
/* Vertical scroll snap */
.sections {
height: 100vh;
overflow-y: auto;
scroll-snap-type: y proximity;
}
.section {
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always;
}CSS Math Functions
Clamp, Min, Max
/* Responsive sizing with clamp */
.responsive {
width: clamp(300px, 50%, 800px);
padding: clamp(1rem, 3vw, 3rem);
font-size: clamp(1rem, 1rem + 1vw, 1.5rem);
}
/* Min/Max functions */
.flexible {
width: min(90%, 1200px);
height: max(300px, 50vh);
/* Complex calculations */
padding: min(max(1rem, 2vw), 3rem);
}Calc with Custom Properties
.calculated {
--base-size: 1rem;
--multiplier: 1.5;
--offset: 0.5rem;
font-size: calc(var(--base-size) * var(--multiplier) + var(--offset));
/* Nested calc */
width: calc(100% - calc(var(--spacing) * 2));
/* With different units */
height: calc(100vh - 60px - 2rem);
}Performance Techniques
Content Visibility
/* Optimize rendering of off-screen content */
.card {
content-visibility: auto;
contain-intrinsic-size: 0 300px; /* Estimated height */
}
/* For sections */
.section {
content-visibility: auto;
contain-intrinsic-size: 0 100vh;
}CSS Containment
/* Isolate layout calculations */
.isolated {
contain: layout style paint;
}
/* Size containment for fixed dimensions */
.fixed-size {
contain: size layout style paint;
width: 300px;
height: 400px;
}
/* Strict containment */
.strict {
contain: strict; /* All types except style */
}Will-Change
/* Optimize for animations */
.will-animate {
will-change: transform, opacity;
}
/* Remove after animation */
.animated {
animation: slide-in 0.3s ease;
}
.animated::after {
will-change: auto;
}Layout Patterns Reference
Table of Contents
- Common Layout Patterns
- Sidebar Layout (Admin Dashboard)
- Masonry Grid Layout
- Split-Pane Layout
- Holy Grail Layout
- Card Grid Layout
- Layout Composition Patterns
- Nested Containers
- Responsive Stack
- Performance Considerations
- Avoiding Layout Thrashing
- CSS Containment
Common Layout Patterns
Sidebar Layout (Admin Dashboard)
The most common application shell pattern.
// React component with collapsible sidebar
interface SidebarLayoutProps {
children: React.ReactNode;
sidebarContent: React.ReactNode;
headerContent: React.ReactNode;
}
export function SidebarLayout({ children, sidebarContent, headerContent }: SidebarLayoutProps) {
const [isCollapsed, setIsCollapsed] = useState(false);
return (
<div className="layout">
<header className="layout__header">
{headerContent}
</header>
<div className="layout__body">
<aside className={`layout__sidebar ${isCollapsed ? 'collapsed' : ''}`}>
<button
className="sidebar-toggle"
onClick={() => setIsCollapsed(!isCollapsed)}
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
>
{isCollapsed ? '→' : '←'}
</button>
{sidebarContent}
</aside>
<main className="layout__main">
{children}
</main>
</div>
</div>
);
}.layout {
display: flex;
flex-direction: column;
height: 100vh;
}
.layout__header {
height: var(--header-height, 64px);
flex-shrink: 0;
border-bottom: 1px solid var(--border-color);
}
.layout__body {
flex: 1;
display: flex;
overflow: hidden;
}
.layout__sidebar {
width: var(--sidebar-width, 280px);
flex-shrink: 0;
overflow-y: auto;
transition: width 0.3s ease;
border-right: 1px solid var(--border-color);
}
.layout__sidebar.collapsed {
width: var(--sidebar-collapsed-width, 64px);
}
.layout__main {
flex: 1;
overflow-y: auto;
padding: var(--content-spacing);
}
/* Mobile responsive */
@media (max-width: 768px) {
.layout__sidebar {
position: fixed;
left: 0;
top: var(--header-height, 64px);
bottom: 0;
z-index: 10;
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.layout__sidebar.open {
transform: translateX(0);
}
}Masonry Grid Layout
Pinterest-style layout with variable height items.
// CSS-only masonry (modern browsers)
export function MasonryGrid({ items }: { items: Array<{ id: string; content: ReactNode; }> }) {
return (
<div className="masonry">
{items.map(item => (
<div key={item.id} className="masonry__item">
{item.content}
</div>
))}
</div>
);
}/* CSS Grid Masonry (experimental, check browser support) */
.masonry {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-auto-rows: 10px;
gap: var(--grid-gap);
}
.masonry__item {
grid-row-end: span var(--item-rows); /* Calculate based on content height */
}
/* Fallback: Column-based masonry */
@supports not (grid-template-rows: masonry) {
.masonry {
column-count: 4;
column-gap: var(--grid-gap);
}
@media (max-width: 1200px) { .masonry { column-count: 3; } }
@media (max-width: 900px) { .masonry { column-count: 2; } }
@media (max-width: 600px) { .masonry { column-count: 1; } }
.masonry__item {
break-inside: avoid;
margin-bottom: var(--grid-gap);
}
}Split-Pane Layout
Resizable panels for editors and comparison views.
export function SplitPane({
left,
right,
defaultPosition = 50,
min = 20,
max = 80
}: SplitPaneProps) {
const [position, setPosition] = useState(defaultPosition);
const containerRef = useRef<HTMLDivElement>(null);
const isDragging = useRef(false);
const handleMouseMove = (e: MouseEvent) => {
if (!isDragging.current || !containerRef.current) return;
const rect = containerRef.current.getBoundingClientRect();
const x = e.clientX - rect.left;
const percentage = (x / rect.width) * 100;
setPosition(Math.max(min, Math.min(max, percentage)));
};
const handleMouseUp = () => {
isDragging.current = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
const handleMouseDown = () => {
isDragging.current = true;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
};
return (
<div ref={containerRef} className="split-pane">
<div className="split-pane__left" style={{ width: `${position}%` }}>
{left}
</div>
<div
className="split-pane__divider"
onMouseDown={handleMouseDown}
role="separator"
aria-valuenow={position}
aria-valuemin={min}
aria-valuemax={max}
/>
<div className="split-pane__right" style={{ width: `${100 - position}%` }}>
{right}
</div>
</div>
);
}.split-pane {
display: flex;
height: 100%;
position: relative;
}
.split-pane__left,
.split-pane__right {
overflow: auto;
}
.split-pane__divider {
width: 4px;
background: var(--border-color);
cursor: col-resize;
position: relative;
flex-shrink: 0;
}
.split-pane__divider:hover {
background: var(--primary-color);
}
.split-pane__divider::after {
content: '';
position: absolute;
top: 0;
bottom: 0;
left: -4px;
right: -4px;
}Holy Grail Layout
Classic layout with header, footer, and three-column body.
export function HolyGrailLayout({
header,
footer,
nav,
main,
aside
}: HolyGrailProps) {
return (
<div className="holy-grail">
<header className="holy-grail__header">
{header}
</header>
<div className="holy-grail__body">
<nav className="holy-grail__nav">
{nav}
</nav>
<main className="holy-grail__main">
{main}
</main>
<aside className="holy-grail__aside">
{aside}
</aside>
</div>
<footer className="holy-grail__footer">
{footer}
</footer>
</div>
);
}.holy-grail {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.holy-grail__header,
.holy-grail__footer {
flex-shrink: 0;
}
.holy-grail__body {
flex: 1;
display: flex;
overflow: hidden;
}
.holy-grail__nav,
.holy-grail__aside {
flex: 0 0 var(--sidebar-width, 200px);
}
.holy-grail__main {
flex: 1;
overflow-y: auto;
min-width: 0;
}
/* Responsive: Stack on mobile */
@media (max-width: 768px) {
.holy-grail__body {
flex-direction: column;
}
.holy-grail__nav,
.holy-grail__aside {
flex: 0 0 auto;
width: 100%;
}
}Card Grid Layout
Responsive card grid with consistent spacing.
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: var(--grid-gap);
container-type: inline-size;
}
/* Container query for card internals */
@container (min-width: 400px) {
.card {
grid-template-columns: auto 1fr;
}
}
/* Responsive columns based on container */
@container (min-width: 900px) {
.card-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@container (min-width: 1200px) {
.card-grid {
grid-template-columns: repeat(4, 1fr);
}
}Layout Composition Patterns
Nested Containers
.app-shell {
display: grid;
grid-template-areas:
"header header"
"nav main"
"nav footer";
grid-template-columns: var(--sidebar-width) 1fr;
grid-template-rows: var(--header-height) 1fr var(--footer-height);
height: 100vh;
}
.header { grid-area: header; }
.nav { grid-area: nav; }
.main { grid-area: main; overflow: auto; }
.footer { grid-area: footer; }Responsive Stack
.stack {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
}
@media (min-width: 768px) {
.stack--horizontal-md {
flex-direction: row;
}
}
.stack > * {
flex: 1;
}Performance Considerations
Avoiding Layout Thrashing
// Bad: Forces multiple reflows
elements.forEach(el => {
el.style.left = el.offsetLeft + 10 + 'px';
el.style.top = el.offsetTop + 10 + 'px';
});
// Good: Batch reads then writes
const positions = elements.map(el => ({
left: el.offsetLeft,
top: el.offsetTop
}));
elements.forEach((el, i) => {
el.style.left = positions[i].left + 10 + 'px';
el.style.top = positions[i].top + 10 + 'px';
});CSS Containment
.card {
contain: layout style paint;
}
.sidebar {
contain: layout;
content-visibility: auto;
}Layout Framework Integration Guide
Table of Contents
- Tailwind CSS
- Configuration with Design Tokens
- Tailwind Layout Patterns
- Tailwind Container Queries
- CSS-in-JS (styled-components / Emotion)
- Styled Components with Design Tokens
- Emotion with TypeScript
- CSS Modules
- Layout Module
- Bootstrap Integration
- Custom Bootstrap with CSS Variables
- Performance Optimization
- Critical CSS
- Lazy Loading Layout Components
Tailwind CSS
Configuration with Design Tokens
// tailwind.config.js
module.exports = {
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
spacing: {
'xs': 'var(--spacing-xs)',
'sm': 'var(--spacing-sm)',
'md': 'var(--spacing-md)',
'lg': 'var(--spacing-lg)',
'xl': 'var(--spacing-xl)',
'2xl': 'var(--spacing-2xl)',
'3xl': 'var(--spacing-3xl)',
},
screens: {
'sm': '640px',
'md': '768px',
'lg': '1024px',
'xl': '1280px',
'2xl': '1536px',
},
maxWidth: {
'container': 'var(--container-max-width)',
},
gridTemplateColumns: {
'12': 'repeat(12, minmax(0, 1fr))',
'auto-fit': 'repeat(auto-fit, minmax(250px, 1fr))',
'auto-fill': 'repeat(auto-fill, minmax(250px, 1fr))',
},
colors: {
primary: 'var(--color-primary)',
secondary: 'var(--color-secondary)',
background: 'var(--color-background)',
surface: 'var(--color-surface)',
text: 'var(--color-text)',
}
}
},
plugins: [
require('@tailwindcss/container-queries'),
],
}Tailwind Layout Patterns
// Admin Layout with Tailwind
export function TailwindAdminLayout({ children }) {
const [sidebarOpen, setSidebarOpen] = useState(false);
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="sticky top-0 z-40 bg-surface border-b">
<div className="flex items-center justify-between h-16 px-4 sm:px-6 lg:px-8">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="lg:hidden p-2 rounded-md hover:bg-gray-100"
>
<span className="sr-only">Open sidebar</span>
{/* Menu icon */}
</button>
<h1 className="text-xl font-semibold">Dashboard</h1>
</div>
</header>
<div className="flex">
{/* Sidebar */}
<aside className={`
fixed inset-y-0 left-0 z-30 w-64 transform bg-surface
lg:relative lg:translate-x-0 lg:inset-0
transition-transform duration-300 ease-in-out
${sidebarOpen ? 'translate-x-0' : '-translate-x-full'}
`}>
<nav className="p-4 space-y-2">
{/* Navigation items */}
</nav>
</aside>
{/* Main content */}
<main className="flex-1 p-4 lg:p-8">
<div className="max-w-container mx-auto">
{children}
</div>
</main>
</div>
</div>
);
}
// Responsive Grid with Tailwind
export function TailwindGrid({ items }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 lg:gap-6">
{items.map(item => (
<div key={item.id} className="bg-surface rounded-lg shadow-md p-4">
{item.content}
</div>
))}
</div>
);
}Tailwind Container Queries
// Container queries with Tailwind
export function ResponsiveCard({ content }) {
return (
<div className="@container">
<div className="bg-surface rounded-lg shadow p-4 @md:p-6 @lg:p-8">
<div className="@md:flex @md:items-center @md:space-x-4">
<img
src={content.image}
className="w-full @md:w-32 @lg:w-48 rounded"
alt=""
/>
<div className="mt-4 @md:mt-0 @md:flex-1">
<h3 className="text-lg @lg:text-xl font-semibold">
{content.title}
</h3>
<p className="mt-2 text-sm @lg:text-base text-gray-600">
{content.description}
</p>
</div>
</div>
</div>
</div>
);
}CSS-in-JS (styled-components / Emotion)
Styled Components with Design Tokens
import styled from 'styled-components';
// Theme provider setup
export const theme = {
spacing: {
xs: 'var(--spacing-xs)',
sm: 'var(--spacing-sm)',
md: 'var(--spacing-md)',
lg: 'var(--spacing-lg)',
xl: 'var(--spacing-xl)',
},
breakpoints: {
sm: '640px',
md: '768px',
lg: '1024px',
xl: '1280px',
},
container: {
maxWidth: 'var(--container-max-width)',
padding: 'var(--container-padding-x)',
}
};
// Layout components
const Container = styled.div`
max-width: ${props => props.theme.container.maxWidth};
margin: 0 auto;
padding: 0 ${props => props.theme.container.padding};
@media (min-width: ${props => props.theme.breakpoints.md}) {
padding: 0 ${props => props.theme.spacing.lg};
}
`;
const Grid = styled.div<{ columns?: number; gap?: string }>`
display: grid;
grid-template-columns: repeat(${props => props.columns || 12}, 1fr);
gap: ${props => props.gap || props.theme.spacing.md};
@media (max-width: ${props => props.theme.breakpoints.md}) {
grid-template-columns: 1fr;
}
`;
const FlexContainer = styled.div<{ direction?: string; gap?: string }>`
display: flex;
flex-direction: ${props => props.direction || 'row'};
gap: ${props => props.gap || props.theme.spacing.md};
@media (max-width: ${props => props.theme.breakpoints.sm}) {
flex-direction: column;
}
`;
// Responsive sidebar layout
const LayoutWrapper = styled.div`
display: flex;
min-height: 100vh;
`;
const Sidebar = styled.aside<{ isOpen: boolean }>`
width: var(--sidebar-width);
background: var(--color-surface);
transition: transform 0.3s ease;
@media (max-width: ${props => props.theme.breakpoints.lg}) {
position: fixed;
left: 0;
top: 0;
bottom: 0;
z-index: 30;
transform: translateX(${props => props.isOpen ? '0' : '-100%'});
}
`;
const MainContent = styled.main`
flex: 1;
padding: ${props => props.theme.spacing.lg};
@media (max-width: ${props => props.theme.breakpoints.md}) {
padding: ${props => props.theme.spacing.md};
}
`;Emotion with TypeScript
import { css } from '@emotion/react';
import styled from '@emotion/styled';
// Type-safe theme
interface Theme {
spacing: Record<'xs' | 'sm' | 'md' | 'lg' | 'xl', string>;
breakpoints: Record<'sm' | 'md' | 'lg' | 'xl', string>;
}
// Media query helper
const mq = (breakpoint: keyof Theme['breakpoints']) =>
`@media (min-width: ${theme.breakpoints[breakpoint]})`;
// Layout styles
const containerStyles = css`
max-width: var(--container-max-width);
margin: 0 auto;
padding: 0 var(--spacing-md);
${mq('md')} {
padding: 0 var(--spacing-lg);
}
`;
// Grid with container queries
const ResponsiveGrid = styled.div`
container-type: inline-size;
.grid {
display: grid;
gap: var(--grid-gap);
grid-template-columns: 1fr;
}
@container (min-width: 400px) {
.grid {
grid-template-columns: repeat(2, 1fr);
}
}
@container (min-width: 800px) {
.grid {
grid-template-columns: repeat(3, 1fr);
}
}
`;CSS Modules
Layout Module
/* Layout.module.css */
.container {
max-width: var(--container-max-width);
margin: 0 auto;
padding: 0 var(--container-padding-x);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--grid-gap);
}
.flexRow {
display: flex;
gap: var(--spacing-md);
flex-wrap: wrap;
}
.sidebar {
width: var(--sidebar-width);
transition: width 0.3s ease;
}
.sidebar.collapsed {
width: var(--sidebar-collapsed-width);
}
@media (max-width: 768px) {
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
transform: translateX(-100%);
}
.sidebar.open {
transform: translateX(0);
}
.flexRow {
flex-direction: column;
}
}// Layout.tsx
import styles from './Layout.module.css';
import { clsx } from 'clsx';
interface LayoutProps {
variant?: 'sidebar' | 'centered' | 'full';
sidebarOpen?: boolean;
children: React.ReactNode;
}
export function Layout({ variant = 'centered', sidebarOpen, children }: LayoutProps) {
return (
<div className={styles.container}>
{variant === 'sidebar' && (
<aside className={clsx(
styles.sidebar,
sidebarOpen && styles.open
)}>
{/* Sidebar content */}
</aside>
)}
<main className={styles.main}>
{children}
</main>
</div>
);
}Bootstrap Integration
Custom Bootstrap with CSS Variables
// custom-bootstrap.scss
// Override Bootstrap variables with CSS custom properties
$primary: var(--color-primary);
$secondary: var(--color-secondary);
$body-bg: var(--color-background);
$body-color: var(--color-text);
// Spacing
$spacer: 1rem;
$spacers: (
0: 0,
1: var(--spacing-xs),
2: var(--spacing-sm),
3: var(--spacing-md),
4: var(--spacing-lg),
5: var(--spacing-xl),
);
// Grid breakpoints
$grid-breakpoints: (
xs: 0,
sm: 640px,
md: 768px,
lg: 1024px,
xl: 1280px,
xxl: 1536px
);
// Container max-widths
$container-max-widths: (
sm: 640px,
md: 768px,
lg: 1024px,
xl: 1280px,
xxl: 1536px
);
@import '~bootstrap/scss/bootstrap';
// Custom utilities
.container-fluid-responsive {
@include make-container();
@include media-breakpoint-up(lg) {
max-width: var(--container-max-width);
}
}Performance Optimization
Critical CSS
// Extract critical CSS for above-the-fold content
import { extractCritical } from '@emotion/server';
export function extractCriticalCSS(html) {
const { css, ids } = extractCritical(html);
return {
css: css,
// Include only layout-critical styles
criticalStyles: `
.container { max-width: var(--container-max-width); margin: 0 auto; }
.grid { display: grid; gap: var(--grid-gap); }
.flex { display: flex; }
@media (max-width: 768px) {
.grid { grid-template-columns: 1fr; }
.flex { flex-direction: column; }
}
`,
ids
};
}Lazy Loading Layout Components
import { lazy, Suspense } from 'react';
// Lazy load heavy layout components
const MasonryLayout = lazy(() => import('./MasonryLayout'));
const ComplexGrid = lazy(() => import('./ComplexGrid'));
export function LazyLayout({ type, ...props }) {
const LayoutSkeleton = () => (
<div className="skeleton-layout" style={{ minHeight: '400px' }} />
);
return (
<Suspense fallback={<LayoutSkeleton />}>
{type === 'masonry' && <MasonryLayout {...props} />}
{type === 'complex' && <ComplexGrid {...props} />}
</Suspense>
);
}Related skills
FAQ
When should I use CSS Grid vs Flexbox?
Use CSS Grid for two-dimensional layouts and complex grids; use Flexbox for one-dimensional layouts and alignment.
What are container queries for?
Component-responsive design that adapts based on container size instead of the viewport.