Install
openclaw skills install responsive-designImplement responsive layouts using container queries, fluid typography, CSS Grid, mobile-first breakpoints, responsive images, and adaptive navigation.
openclaw skills install responsive-designModern responsive CSS patterns using container queries, fluid typography, CSS Grid, and mobile-first strategies.
Comprehensive responsive design techniques:
clamp()responsive, container query, media query, breakpoint, mobile-first, fluid typography, clamp, css grid, flexbox, viewport, adaptive, responsive images
npx clawhub@latest install responsive-design
/* Base: Mobile (< 640px) - no media query needed */
@media (min-width: 640px) { /* sm: Large phones, small tablets */ }
@media (min-width: 768px) { /* md: Tablets */ }
@media (min-width: 1024px) { /* lg: Laptops */ }
@media (min-width: 1280px) { /* xl: Desktops */ }
@media (min-width: 1536px) { /* 2xl: Large screens */ }
Tailwind equivalents: sm:, md:, lg:, xl:, 2xl:
Component-level responsiveness independent of viewport:
/* Define containment context */
.card-container {
container-type: inline-size;
container-name: card;
}
/* Query the container, not viewport */
@container card (min-width: 400px) {
.card {
display: grid;
grid-template-columns: 200px 1fr;
}
}
@container card (min-width: 600px) {
.card-title {
font-size: 1.5rem;
}
}
/* Container query units */
.card-title {
font-size: clamp(1rem, 5cqi, 2rem); /* 5% of container inline-size */
}
function ResponsiveCard({ title, image, description }) {
return (
<div className="@container">
<article className="flex flex-col @md:flex-row @md:gap-4">
<img
src={image}
alt=""
className="w-full @md:w-48 @lg:w-64 aspect-video @md:aspect-square object-cover"
/>
<div className="p-4 @md:p-0">
<h2 className="text-lg @md:text-xl @lg:text-2xl font-semibold">
{title}
</h2>
<p className="mt-2 text-muted-foreground @md:line-clamp-3">
{description}
</p>
</div>
</article>
</div>
)
}
:root {
--text-xs: clamp(0.75rem, 0.7rem + 0.25vw, 0.875rem);
--text-sm: clamp(0.875rem, 0.8rem + 0.375vw, 1rem);
--text-base: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
--text-lg: clamp(1.125rem, 1rem + 0.625vw, 1.25rem);
--text-xl: clamp(1.25rem, 1rem + 1.25vw, 1.5rem);
--text-2xl: clamp(1.5rem, 1.25rem + 1.25vw, 2rem);
--text-3xl: clamp(1.875rem, 1.5rem + 1.875vw, 2.5rem);
--text-4xl: clamp(2.25rem, 1.75rem + 2.5vw, 3.5rem);
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
p { font-size: var(--text-base); }
:root {
--space-xs: clamp(0.25rem, 0.2rem + 0.25vw, 0.5rem);
--space-sm: clamp(0.5rem, 0.4rem + 0.5vw, 0.75rem);
--space-md: clamp(1rem, 0.8rem + 1vw, 1.5rem);
--space-lg: clamp(1.5rem, 1.2rem + 1.5vw, 2.5rem);
--space-xl: clamp(2rem, 1.5rem + 2.5vw, 4rem);
}
clamp(MIN, PREFERRED, MAX)
MIN: Smallest allowed size
PREFERRED: Ideal fluid calculation (often uses vw)
MAX: Largest allowed size
.grid-auto {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(300px, 100%), 1fr));
gap: 1.5rem;
}
.page-layout {
display: grid;
grid-template-areas:
"header"
"main"
"sidebar"
"footer";
gap: 1rem;
}
@media (min-width: 768px) {
.page-layout {
grid-template-columns: 1fr 300px;
grid-template-areas:
"header header"
"main sidebar"
"footer footer";
}
}
@media (min-width: 1024px) {
.page-layout {
grid-template-columns: 250px 1fr 300px;
grid-template-areas:
"header header header"
"nav main sidebar"
"footer footer footer";
}
}
.header { grid-area: header; }
.main { grid-area: main; }
.sidebar { grid-area: sidebar; }
.footer { grid-area: footer; }
function ProductGrid({ products }) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4 md:gap-6">
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
function ResponsiveNav({ items }) {
const [isOpen, setIsOpen] = useState(false)
return (
<nav className="relative">
{/* Mobile toggle */}
<button
className="lg:hidden p-2"
onClick={() => setIsOpen(!isOpen)}
aria-expanded={isOpen}
aria-controls="nav-menu"
>
<span className="sr-only">Toggle navigation</span>
{isOpen ? <X /> : <Menu />}
</button>
{/* Navigation links */}
<ul
id="nav-menu"
className={cn(
// Mobile: dropdown
"absolute top-full left-0 right-0 bg-background border-b",
"flex flex-col",
isOpen ? "flex" : "hidden",
// Desktop: horizontal, always visible
"lg:static lg:flex lg:flex-row lg:border-0"
)}
>
{items.map(item => (
<li key={item.href}>
<a
href={item.href}
className="block px-4 py-3 lg:px-3 lg:py-2 hover:bg-muted lg:hover:bg-transparent"
>
{item.label}
</a>
</li>
))}
</ul>
</nav>
)
}
function ResponsiveHero() {
return (
<picture>
{/* Different crops for different screens */}
<source media="(min-width: 1024px)" srcSet="/hero-wide.webp" type="image/webp" />
<source media="(min-width: 768px)" srcSet="/hero-medium.webp" type="image/webp" />
<source srcSet="/hero-mobile.webp" type="image/webp" />
<img
src="/hero-mobile.jpg"
alt="Hero description"
className="w-full h-auto"
loading="eager"
fetchPriority="high"
/>
</picture>
)
}
function ProductImage({ product }) {
return (
<img
src={product.image}
srcSet={`
${product.image}?w=400 400w,
${product.image}?w=800 800w,
${product.image}?w=1200 1200w
`}
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
alt={product.name}
className="w-full h-auto object-cover"
loading="lazy"
/>
)
}
function ResponsiveTable({ data, columns }) {
return (
<div className="w-full overflow-x-auto">
<table className="w-full min-w-[600px]">
<thead>
<tr>
{columns.map(col => (
<th key={col.key} className="text-left p-3">{col.label}</th>
))}
</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i} className="border-t">
{columns.map(col => (
<td key={col.key} className="p-3">{row[col.key]}</td>
))}
</tr>
))}
</tbody>
</table>
</div>
)
}
function ResponsiveDataTable({ data, columns }) {
return (
<>
{/* Desktop: table */}
<table className="hidden md:table w-full">
{/* standard table markup */}
</table>
{/* Mobile: cards */}
<div className="md:hidden space-y-4">
{data.map((row, i) => (
<div key={i} className="border rounded-lg p-4 space-y-2">
{columns.map(col => (
<div key={col.key} className="flex justify-between">
<span className="font-medium text-muted-foreground">{col.label}</span>
<span>{row[col.key]}</span>
</div>
))}
</div>
))}
</div>
</>
)
}
/* Standard viewport units - problematic on mobile */
.full-height { height: 100vh; }
/* Dynamic viewport units (recommended) */
.full-height-dynamic { height: 100dvh; } /* Accounts for mobile browser UI */
/* Small viewport (minimum when UI shown) */
.min-full-height { min-height: 100svh; }
/* Large viewport (maximum when UI hidden) */
.max-full-height { max-height: 100lvh; }
clamp() and relative units over fixed pxinline/block for internationalization| Issue | Cause | Fix |
|---|---|---|
| Horizontal scroll | Fixed widths | Use relative units, max-width: 100% |
| 100vh too tall on mobile | Address bar | Use 100dvh or 100svh |
| Tiny tap targets | Desktop design | Min 44px height/width on interactive elements |
| Images breaking layout | Missing constraints | Add max-width: 100%; height: auto; |
| Text too small | Fixed font size | Use fluid typography with clamp() |
px for typography (use rem)100vh on mobile without fallback