Responsive & Modern CSS
Responsive design means one document that adapts to any viewport, input method, and user preference. Modern CSS does most of it with intrinsic sizing and fluid values; media queries are the exception, not the foundation.
Absolute
Section titled “Absolute”px is the only absolute unit worth using. A CSS pixel is a device-independent reference unit — on a 3× phone screen, 1px is three physical pixels. pt, cm, in, mm are defined relative to px (1in = 96px) and belong in print stylesheets only.
Relative to font size
Section titled “Relative to font size”| Unit | Relative to |
|---|---|
em |
the element’s own font-size (for font-size itself: the parent’s) |
rem |
the root (<html>) font-size — 16px by default |
ch |
the advance width of 0 in the current font |
ex |
the x-height of the current font |
lh |
the element’s own line-height |
em compounds. Nested elements each multiply:
.a { font-size: 1.5em; } /* 24px if parent is 16px */.a .a { font-size: 1.5em; }/* 36px — compounds */That makes em unpredictable for font size and excellent for padding that scales with the element’s own text:
.btn { font-size: 1rem; padding: 0.6em 1.2em; /* padding scales with the button's text size */ border-radius: 0.4em;}.btn-lg { font-size: 1.25rem; } /* padding grows automatically */Rule of thumb: rem for font sizes and layout spacing, em for spacing that must track the local font size, px for hairlines and things that should never scale (1px borders).
ch is the right unit for line length:
article { max-inline-size: 65ch; } /* ~65 characters: comfortable measure */Relative to the viewport
Section titled “Relative to the viewport”| Unit | Meaning |
|---|---|
vw / vh |
1% of viewport width / height |
vmin / vmax |
1% of the smaller / larger viewport dimension |
svh / svw |
small viewport: as if mobile browser UI is fully shown |
lvh / lvw |
large viewport: as if browser UI is fully hidden |
dvh / dvw |
dynamic: updates as the URL bar collapses and expands |
On mobile, 100vh equals the large viewport, so a height: 100vh hero is taller than the visible area and its bottom is hidden behind the browser chrome. Use 100dvh (Chrome 108+, Safari 15.4+, Firefox 101+):
.hero { min-block-size: 100dvh; }vw ignores the scrollbar width in most browsers, so width: 100vw on a page with a vertical scrollbar causes horizontal overflow. Prefer 100% or the newer 100dvw.
Grid-only, and not a length: a fraction of the leftover space in a grid container. Covered in Grid.
Media queries
Section titled “Media queries”@media (min-width: 48rem) { .layout { grid-template-columns: 240px 1fr; }}Range syntax
Section titled “Range syntax”All current browsers support comparison operators, which are easier to read and avoid off-by-one overlaps:
@media (width >= 48rem) { }@media (width < 48rem) { }@media (40rem <= width < 64rem) { }The old form is (min-width: …) / (max-width: …). If you mix them, remember max-width: 48rem includes 48rem, so pairing it with min-width: 48rem makes both match at exactly 48rem.
Combining conditions
Section titled “Combining conditions”@media (min-width: 48rem) and (orientation: landscape) { }@media (min-width: 48rem), print { } /* comma = OR */@media not all and (min-width: 48rem) { }@media screen and (min-width: 48rem) { } /* media type + feature */User preference queries
Section titled “User preference queries”These matter more than breakpoints for real users.
/* Colour scheme */@media (prefers-color-scheme: dark) { :root { --bg: #0b0b0d; --fg: #e8e8ea; }}
/* Motion sensitivity — respect it, always */@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; }}
@media (prefers-contrast: more) { :root { --fg: #000; } }@media (prefers-reduced-transparency: reduce) { .glass { backdrop-filter: none; } }Even simpler for colour scheme — tell the browser which schemes you support and it will style form controls, scrollbars and the default background correctly:
:root { color-scheme: light dark; }Input capability queries
Section titled “Input capability queries”@media (hover: hover) and (pointer: fine) { .card:hover { transform: translateY(-2px); } /* mouse only */}@media (pointer: coarse) { .btn { min-block-size: 44px; } /* bigger touch targets */}(hover: hover) is how you avoid sticky hover states on touch devices, where a tap leaves the hover style applied.
Other useful queries
Section titled “Other useful queries”@media print { nav, .no-print { display: none; } }@media (display-mode: standalone) { } /* installed PWA */@media (scripting: none) { .js-only { display: none; } }Mobile-first
Section titled “Mobile-first”Write the narrow-screen layout as the base, then add complexity at wider breakpoints with min-width.
/* base: single column, applies everywhere */.layout { display: grid; gap: 1.5rem; }
@media (width >= 48rem) { .layout { grid-template-columns: 240px 1fr; }}
@media (width >= 72rem) { .layout { grid-template-columns: 240px 1fr 280px; }}Why this direction and not max-width:
- The base styles are the simplest ones, and small devices are the ones with the least capacity.
- Each query only adds, so you never write “undo” rules.
- Overrides accumulate in one direction, which is far easier to reason about than a stack of
max-widthblocks that cancel each other out.
Pick breakpoints where your layout breaks, not at device names. Resize the window until something looks wrong; that is a breakpoint.
Container queries
Section titled “Container queries”A media query asks about the viewport. A container query asks about the size of an element’s own container — so a component can be responsive to the space it is actually given, wherever it is used.
.card-wrap { container-type: inline-size; /* it can now be queried on its inline size */ container-name: card; /* optional name */}
/* shorthand for both: */.card-wrap { container: card / inline-size; }
@container card (width >= 30rem) { .card { display: grid; grid-template-columns: 12rem 1fr; gap: 1rem; }}
/* unnamed: queries the nearest ancestor container */@container (width >= 30rem) { }Supported in Chrome 105+, Safari 16+, Firefox 110+.
Key rules:
- You query an ancestor, never the element itself. Wrap the component.
container-type: inline-sizeallows querying width only, and the container’s block size becomes dependent on content as usual.container-type: sizeallows both axes but requires the container to have a definite block size, which usually means you must set a height.container-typealso creates a containment context, which means the container establishes a new stacking and layout context — it behaves likecontain: layout style inline-size.
Container query units
Section titled “Container query units”Inside a container query (and inside a container generally) you can size against the container:
| Unit | 1% of the container’s |
|---|---|
cqw / cqh |
width / height |
cqi / cqb |
inline size / block size |
cqmin / cqmax |
smaller / larger of cqi and cqb |
.card h2 { font-size: clamp(1rem, 5cqi, 2rem); }Style queries
Section titled “Style queries”@container style(--variant: compact) queries the value of a custom property on the container. Custom-property style queries are supported in Chrome 111+; queries on arbitrary properties are not broadly available. Treat style queries as a progressive enhancement.
Fluid sizing: clamp(), min(), max()
Section titled “Fluid sizing: clamp(), min(), max()”These are math functions that work anywhere a length is allowed.
width: min(100%, 60rem); /* the smaller — an implicit max-width */width: max(50%, 20rem); /* the larger — an implicit min-width */font-size: clamp(1rem, 2.5vw + 0.5rem, 2rem);clamp(MIN, PREFERRED, MAX) is exactly max(MIN, min(PREFERRED, MAX)).
Fluid type without a single media query:
:root { --step-0: clamp(1rem, 0.95rem + 0.25vw, 1.125rem); --step-1: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem); --step-2: clamp(1.5rem, 1.2rem + 1.5vw, 2.25rem); --step-3: clamp(2rem, 1.4rem + 3vw, 3.5rem);}h1 { font-size: var(--step-3); }p { font-size: var(--step-0); }Fluid gutters and a content column, in three lines:
.container { inline-size: min(100% - 2rem, 72rem); margin-inline: auto;}Related math: calc() mixes units (calc(100% - 2rem)), and round(), mod(), rem() exist in newer browsers.
Responsive images
Section titled “Responsive images”srcset + sizes
Section titled “srcset + sizes”Give the browser several files and tell it how wide the image will be rendered; it picks the best one for the device pixel ratio and viewport.
<img src="photo-800.jpg" srcset="photo-400.jpg 400w, photo-800.jpg 800w, photo-1600.jpg 1600w" sizes="(width >= 48rem) 50vw, 100vw" width="1600" height="900" alt="A harbour at dusk" loading="lazy" decoding="async"/>wdescriptors state each file’s intrinsic pixel width.sizesdescribes the rendered width at each breakpoint. Without it the browser assumes100vwand over-downloads.widthandheightattributes let the browser reserve space, preventing layout shift. CSSaspect-ratiodoes the same job.loading="lazy"for below-the-fold images only; never on the LCP image. Usefetchpriority="high"on that one instead.
<picture>
Section titled “<picture>”Use <picture> for art direction (a different crop) or format negotiation:
<picture> <source srcset="hero.avif" type="image/avif" /> <source srcset="hero.webp" type="image/webp" /> <source media="(width < 40rem)" srcset="hero-square.jpg" /> <img src="hero.jpg" alt="" width="1600" height="900" /></picture>The browser takes the first <source> whose type and media both match. The <img> is the fallback and carries the alt.
CSS side
Section titled “CSS side”img, video, svg { max-inline-size: 100%; block-size: auto; display: block; }
.thumb { aspect-ratio: 16 / 9; object-fit: cover; /* fill the box, crop the overflow */ object-position: 50% 30%;}object-fit values: fill (default, distorts), contain (letterbox), cover (crop), none, scale-down.
Logical properties
Section titled “Logical properties”Physical properties (left, top, margin-left) assume a left-to-right, top-to-bottom writing mode. Logical properties are expressed relative to the flow, so the same CSS works in Arabic, Hebrew, or vertical Japanese with no overrides.
| Physical | Logical |
|---|---|
width / height |
inline-size / block-size |
margin-left / margin-right |
margin-inline-start / margin-inline-end |
margin-top / margin-bottom |
margin-block-start / margin-block-end |
padding-left + padding-right |
padding-inline |
padding-top + padding-bottom |
padding-block |
border-left |
border-inline-start |
top/right/bottom/left |
inset-block-start / inset-inline-end / … |
text-align: left |
text-align: start |
max-width |
max-inline-size |
.card { padding-inline: 1.5rem; padding-block: 1rem; border-inline-start: 3px solid var(--accent); margin-inline: auto; max-inline-size: 60ch; text-align: start;}margin-inline: auto is the modern “centre a block” and reads better than margin: 0 auto.
The one-value and multi-value forms exist for inset too:
.overlay { inset: 0; } /* all four */.badge { inset-block-start: 8px; inset-inline-end: 8px; }Flexbox and Grid are already logical — flex-direction: row, justify-content: flex-start, and grid’s inline/block axes all follow the writing mode automatically.
Key points
Section titled “Key points”remfor type and layout,emfor element-local spacing,chfor measure,pxfor hairlines.- Use
dvh/svhinstead ofvhfor full-height sections on mobile. - Write breakpoints in
remwith range syntax, mobile-first withmin-width. - Always honour
prefers-reduced-motion; declarecolor-scheme. - Container queries make components responsive to their slot; query an ancestor with
container-type: inline-size. clamp()with aremterm removes most typography breakpoints.srcset/sizesfor resolution switching,<picture>for art direction or formats; always setwidth/height.- Prefer logical properties — shorter, and internationalisation comes free.