building-responsive-layouts
SKILL.md
Building Responsive Layouts
Quick Start
// Responsive grid with auto-fit
export function ResponsiveGrid({ children, minWidth = '300px' }: GridProps) {
return (
<div style={{
display: 'grid',
gridTemplateColumns: `repeat(auto-fit, minmax(min(${minWidth}, 100%), 1fr))`,
gap: '1.5rem'
}}>
{children}
</div>
);
}
// With Tailwind
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
Features
| Feature | Description | Guide |
|---|---|---|
| Mobile-First Breakpoints | sm(640), md(768), lg(1024), xl(1280), 2xl(1536) | ref/breakpoints.md |
| Fluid Typography | clamp() for responsive font sizes |
ref/fluid-type.md |
| Container Queries | Component-level responsive design | ref/container-queries.md |
| Responsive Images | srcset, sizes, and art direction | ref/images.md |
| Touch-Friendly | 44px minimum targets, hover vs touch handling | ref/touch.md |
| Safe Areas | Handle notched devices and dynamic viewports | ref/safe-areas.md |
Common Patterns
useMediaQuery Hook
export function useMediaQuery(query: string): boolean {
const [matches, setMatches] = useState(false);
useEffect(() => {
const media = window.matchMedia(query);
setMatches(media.matches);
const listener = (e: MediaQueryListEvent) => setMatches(e.matches);
media.addEventListener('change', listener);
return () => media.removeEventListener('change', listener);
}, [query]);
return matches;
}
// Usage
const isMobile = useMediaQuery('(max-width: 639px)');
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)');
Container Query Component
.card-container {
container-type: inline-size;
}
.card {
display: flex;
flex-direction: column;
}
@container (min-width: 400px) {
.card {
flex-direction: row;
gap: 1rem;
}
.card-image { width: 40%; }
}
export function ResponsiveCard({ image, title, content }: CardProps) {
return (
<div className="card-container">
<article className="card">
<img src={image} alt="" className="card-image" />
<div className="card-content">
<h3>{title}</h3>
<p>{content}</p>
</div>
</article>
</div>
);
}
Responsive Navigation
export function ResponsiveNav() {
const [isOpen, setIsOpen] = useState(false);
return (
<nav className="relative">
<div className="flex justify-between items-center h-16 px-4">
<Logo />
{/* Desktop nav */}
<div className="hidden md:flex items-center space-x-8">
<NavLink href="/">Home</NavLink>
<NavLink href="/about">About</NavLink>
</div>
{/* Mobile menu button */}
<button
className="md:hidden p-2 min-h-[44px] min-w-[44px]"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
>
<span className="sr-only">{isOpen ? 'Close' : 'Open'} menu</span>
{isOpen ? <XIcon /> : <MenuIcon />}
</button>
</div>
{/* Mobile nav */}
{isOpen && (
<div className="md:hidden px-2 pb-3 space-y-1">
<MobileNavLink href="/" onClick={() => setIsOpen(false)}>Home</MobileNavLink>
<MobileNavLink href="/about" onClick={() => setIsOpen(false)}>About</MobileNavLink>
</div>
)}
</nav>
);
}
Best Practices
| Do | Avoid |
|---|---|
| Start with mobile-first CSS | Hiding essential content on mobile |
| Use relative units (rem, %, vw) | Fixed pixel widths |
| Test on real devices | Relying only on hover states |
| Use 44px minimum touch targets | Small touch targets on mobile |
| Consider reduced motion preferences | Ignoring landscape orientation |
| Use CSS logical properties (inline, block) | Device-specific breakpoints |
| Handle safe-area-inset for notched devices | Assuming mouse input |
Weekly Installs
1
Repository
doanchienthangdev/omgkitGitHub Stars
3
First Seen
6 days ago
Security Audits
Installed on
zencoder1
amp1
cline1
openclaw1
opencode1
cursor1