Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Reference-grade playbook for responsive and adaptive design across every screen class — phone, tablet, laptop, desktop, TV, watch, foldable — using mobile-first breakpoints, fluid clamp() type, container queries, logical properties, and dvh/safe-area viewport handling.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 119% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 128% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 323% | 0% |
| case-14 | ✓→✓ | = Same ✓ | 173% | 0% |
| case-08 | ✓→✓ | = Same ✓ | 479% | 0% |
One layout, every screen. The goal is not "make it shrink" — it's to serve the right information density, interaction model, and ergonomics for each device class. A phone is held one-handed with a thumb; a TV is driven by a D-pad from 10 feet; a watch is glanceable in 2 seconds. Same content, different affordances.
min-width media/container queries. Progressive enhancement: the base CSS is the floor everyone gets; larger screens layer on. Smaller cascade, fewer overrides, better perf on weak devices.max-width, graceful degradation) is legacy. Avoid for new work — you end up un-styling for mobile, which is more code and more bugs.%, vw, clamp()), no jumps. Responsive = fluid + breakpoint reflows. Adaptive = discrete fixed layouts swapped at thresholds (server-detected or JS), no in-between. Modern default: fluid + a few responsive breakpoints. Reserve adaptive for genuinely different device classes (e.g. a real TV/watch app, not a width).minmax(), auto-fit, clamp(), fit-content — fewer hard breakpoints, no cliffs.Prefer content-driven breakpoints: resize the browser, add one where the layout breaks (line length >~75ch, cramped columns, awkward gaps). Don't target specific devices — they change yearly.
| Class | Reference width | Common use | |---|---|---| | Small phone | 320–360px | absolute floor; must work at 320 | | Phone | 390–430px | iPhone 14/15, modern Android | | Tablet (portrait) | 768px | iPad mini/Air portrait | | Tablet (landscape) / small laptop | 1024px | iPad landscape, 11" laptop | | Desktop | 1280–1440px | mainstream laptop/monitor | | Large desktop | 1920px+ | external monitors, cap content width |
em for breakpoints, not px: @media (min-width: 48em) respects the user's root font size / browser zoom. (48em = 768px at default 16px.)@media (orientation: landscape). Useful for tablet master-detail and short-viewport keyboard handling, not as a primary axis.max-width + margin-inline: auto (e.g. text max-width: 65ch, shells max-width: 1280px) so 4K monitors don't stretch lines to 200ch.css/* mobile-first, em breakpoints, content-capped */ .container { width: 100%; padding-inline: clamp(1rem, 5vw, 2rem); } @media (min-width: 48em) { .grid { grid-template-columns: 1fr 1fr; } } /* 768 */ @media (min-width: 64em) { .grid { grid-template-columns: 1fr 2fr 1fr; } } /* 1024 */ @media (min-width: 90em) { .container { max-width: 1280px; margin-inline: auto; } } /* 1440 */
| Unit | Use it for | Avoid for | |---|---|---| | rem | type, spacing, radii — scales with user root | nothing; default choice | | em | spacing relative to local font size, media queries | global spacing | | % | fluid container widths | type sizes | | ch | text measure / line length (max-width: 65ch) | non-text | | vw/vh | hero sizing, but pair with clamp() | locking font size alone | | dvh/svh/lvh | full-height mobile layouts (below) | — | | fr | grid track distribution | fixed gutters | | px | borders, 1px hairlines, fine detail | type, layout spacing |
dvh / svh / lvh solve the mobile URL-bar problem. On mobile, 100vh = the largest viewport (URL bar hidden), so a height:100vh element overflows when the bar is showing and content gets cut off. svh = small (bar shown), lvh = large (bar hidden), dvh = dynamic (resizes live as the bar shows/hides). Use min-height: 100dvh for app shells / full-screen heroes. Keep a vh fallback for old browsers: min-height: 100vh; min-height: 100dvh;.@1x (DPR 1) → 1 CSS px = 1 hardware px; @2x (Retina, most phones) = 2×2; @3x (iPhone Pro) = 3×3. Ship srcset with 1x/2x/3x so a 2× screen gets a sharp image without a 3× screen over-downloading.rem is the closest analog to sp — it scales with user preference. Reference math: iPhone 15 = 393pt wide × DPR 3 = 1179 physical px; design at 393 CSS px, export assets at 3×.clamp(MIN, PREFERRED, MAX) scales smoothly between two anchors so you don't need a media query for every size jump.
css:root { /* fluid type: 16px floor → 20px ceiling, scaling with viewport */ --step-0: clamp(1rem, 0.9rem + 0.5vw, 1.25rem); --step-1: clamp(1.25rem, 1rem + 1.2vw, 2rem); /* h3 */ --step-2: clamp(1.6rem, 1.2rem + 2vw, 3rem); /* h2 */ /* fluid space scale */ --space-s: clamp(0.75rem, 0.6rem + 0.7vw, 1.25rem); --space-l: clamp(2rem, 1.5rem + 2.5vw, 4rem); } h2 { font-size: var(--step-2); } .stack > * + * { margin-block-start: var(--space-l); } /* fluid grid: as many ~16rem cards as fit, no media queries */ .cards { display: grid; gap: var(--space-s); grid-template-columns: repeat(auto-fit, minmax(min(16rem, 100%), 1fr)); }
min(16rem, 100%) inside minmax prevents overflow on tiny screens (the column never exceeds the container).auto-fit collapses empty tracks (cards stretch); auto-fill keeps empty tracks (cards stay fixed-width). Pick auto-fit for "fill the row," auto-fill for "consistent card size."clamp() anchors from a min/max viewport + scale ratio. Don't hand-tune every value.Media queries respond to the viewport; container queries (@container) respond to the element's own container. For reusable components this is decisive: the same card behaves correctly in a wide main column and a narrow sidebar without knowing the page layout.
css.card-wrap { container-type: inline-size; container-name: card; } .card { display: grid; gap: 1rem; } @container card (min-width: 30rem) { .card { grid-template-columns: 8rem 1fr; } /* image beside text when wide */ }
cqw/cqh (1% of container width/height), cqi/cqb (inline/block — prefer these, they respect writing direction), cqmin/cqmax. Example: font-size: clamp(1rem, 4cqi, 1.5rem) scales type to the container, not the page.Use logical (flow-relative) properties instead of physical ones so RTL, vertical writing, and i18n work for free.
| Physical | Logical | |---|---| | margin-left/right | margin-inline-start/end, margin-inline | | margin-top/bottom | margin-block-start/end, margin-block | | padding-left | padding-inline-start | | width / height | inline-size / block-size | | text-align: left | text-align: start | | left / right (inset) | inset-inline-start/end |
padding-inline: 1rem = left+right in LTR, automatically right+left in RTL. No mirrored stylesheet needed.
A 1024px screen could be a touch tablet or a mouse laptop. Width tells you nothing about input. Query the input:
css@media (hover: hover) and (pointer: fine) { .menu:hover .submenu { display:block; } } /* mouse */ @media (hover: none) and (pointer: coarse) { .btn { min-height: 44px; } } /* touch */
:hover. Touch has no hover — tooltips, hover menus, and reveal-on-hover content must have a tap/focus equivalent. @media (hover: hover) to enhance, never to require.pointer: coarse (finger/stylus, imprecise) vs pointer: fine (mouse/trackpad). Use it to size hit areas, not to assume device class.html<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
width=device-width, initial-scale=1 is mandatory — without it mobile browsers render at 980px and shrink. Never set maximum-scale=1 / user-scalable=no: it blocks pinch-zoom and fails WCAG.viewport-fit=cover lets content extend under the notch/rounded corners — required for env() safe areas to do anything.env(safe-area-inset-*). Combine with your own padding via max():css.app-bar { padding-top: max(1rem, env(safe-area-inset-top)); } .bottom-nav { padding-bottom: max(0.5rem, env(safe-area-inset-bottom)); } /* clears home indicator */ .screen { padding-inline: max(1rem, env(safe-area-inset-left), env(safe-area-inset-right)); }
100dvh + the VirtualKeyboard API (navigator.virtualKeyboard.overlaysContent = true) and the env(keyboard-inset-*) values let you keep the input visible. At minimum, scrollIntoView() the focused field and avoid pinning critical controls to the bottom when an input is focused.dvh (above). Don't lock heights with 100vh.| Class | Layout | Navigation | Notes | |---|---|---|---| | Phone | single column, stacked | bottom tab bar, hamburger, sheets/modals | thumb-reachable actions, 44px targets, full-width CTAs | | Tablet | master-detail / split view, 2 columns | sidebar or tab bar | support iPad Slide Over/Split View → treat as a narrower viewport, don't assume full width; portrait vs landscape differs a lot | | Desktop | multi-pane, dense, sidebars | persistent top + side nav | hover affordances, keyboard shortcuts, right-click menus, denser tables, cursor precision allows small targets (but keep ≥24px) | | TV (10-foot UI) | large, few items per screen, generous spacing | D-pad focus traversal, no cursor | huge type (min ~24–32px body, scale up), strong visible focus state (the focus ring is the cursor), respect overscan (5% safe margin), high contrast, no hover, no tiny text | | Watch | one thing per screen, glanceable | crown/swipe, minimal taps | 2-second readability, largest type, single primary action, no dense data, complications-style summaries | | Foldable | adapt to posture (folded/unfolded/half-open "tabletop"); avoid content under the hinge | reflows phone→tablet on unfold | use env(fold-*) / Viewport Segments API to detect the seam; continuity — preserve state across fold; book vs tabletop posture changes layout |
:focus-visible with a thick ring/scale), and a logical spatial traversal order — D-pad moves between visible neighbors, so layout = navigation graph.srcset + sizes lets the browser pick.html <img src="hero-800.jpg" srcset="hero-400.jpg 400w, hero-800.jpg 800w, hero-1600.jpg 1600w" sizes="(min-width: 64em) 50vw, 100vw" alt="…" width="1600" height="900" loading="lazy" decoding="async"> Always set width/height (or aspect-ratio) to reserve space and prevent CLS.
<picture> with <source media="...">.html <picture> <source media="(min-width: 64em)" srcset="banner-wide.jpg"> <img src="banner-square.jpg" alt="…"> </picture>
<picture><source type="image/avif"> with a JPEG fallback.aspect-ratio: 16 / 9 on the container keeps media boxes stable while fluid; object-fit: cover for crop-to-fill.overflow-x: auto for horizontal scroll as the floor; for true mobile reflow, switch to a stacked card layout (display: block rows with data-label pseudo-content) below a breakpoint. Never let a wide table cause the whole page to scroll horizontally.clamp() (see Fluid section); don't redefine font sizes inside every breakpoint.prefers-reduced-motion.| Don't | Do | |---|---| | Design only at 1440px (or only at 390px) | Design at the extremes + one mid; test 320 → 1920+ | | height: 100vh for full-screen mobile | min-height: 100dvh (with 100vh fallback) | | Hover-only menus/tooltips | Tap/focus equivalent; @media (hover:hover) to enhance only | | 12 breakpoints chasing devices | 3–5 content-driven, em-based breakpoints + clamp() fluid | | Ignore notch/home indicator | viewport-fit=cover + env(safe-area-inset-*) | | 30px touch targets, packed together | 44–48px targets, ≥8px apart | | user-scalable=no | Allow pinch-zoom (accessibility) | | Fixed px everywhere | rem/clamp() for type & space, %/fr for layout | | Wide table breaks page (horizontal page scroll) | Scroll-container or stacked-card reflow | | Physical margin-left/text-align:left | Logical margin-inline-start/text-align:start | | Component queries the viewport | Component uses @container; only the shell uses media queries | | Images with no width/height | Always set dimensions or aspect-ratio (no CLS) |
Bottom line: Build mobile-first with logical properties, let clamp() and auto-fit grids handle the in-between so you need only 3–5 real breakpoints, scope component responsiveness to @container, fix heights with dvh and edges with env() safe areas, size for the input (touch vs pointer) not the width, and treat TV/watch/foldable as distinct ergonomic classes — then verify on real hardware at 320px and 400% zoom.
Other measured skills in the registry, with their headline benchmark lift.