This page is the working form of the attitude the rest of the section keeps returning to: write a sound baseline, then let the page respond to the space it has, the preferences its reader has set, and the features the browser supports. The order of the sections is the order to apply them in: fluid first, then breakpoints, then container queries, then preferences, capabilities and feature queries on top, with defensive habits throughout.
1. The spectrum of contexts
CSS Basics introduced media types: a style sheet for screen, another for print. That was the first idea of context, and it was far too coarse. A "screen" can be 20 characters wide or 300. It can be touched or pointed at. It can belong to someone who has asked their operating system for dark colors, less motion or more contrast. These dimensions vary independently, and a single style sheet has to serve every combination of them.
| Dimension | Range | Where CSS sees it |
|---|---|---|
| Viewport size | A narrow phone to a wide desktop monitor, in both dimensions | @media (width …), viewport units |
| Container size | Different for each component, even on one page | @container, container units |
| Input | Touch, mouse, pen, keyboard, voice | pointer, hover, any-pointer, any-hover |
| Color scheme | Light or dark | prefers-color-scheme, color-scheme, light-dark() |
| Motion | Full or reduced | prefers-reduced-motion |
| Contrast | Standard, more, less, or colors forced by the system | prefers-contrast, forced-colors |
| Text size and zoom | The reader's default font size, and zoom up to 500 percent | Relative units: rem, em, ch |
| Medium | Screen or paper | @media print, @page |
| Feature support | The browser version the reader happens to run | @supports, cascade fallbacks |
The table is not a list of special cases. Every visitor sits at some point in all of these dimensions at once, and the author cannot know which point. Someone on a small touch screen in landscape, with reduced motion and a large default font, is not exotic. They are using the site right now.
Responsive is more than width
The phrase "responsive web design" (Ethan Marcotte, 2010) came to mean adjusting a layout to the screen's width, and media queries made that easy. Width is the least interesting part of the problem. A narrow viewport does not tell you the device is slow, the connection is metered, or the reader is on a phone; a wide one does not tell you the reader is using a mouse. Rearranging boxes for a narrow screen does nothing about a page that downloads three megabytes of images, or an interface whose hover menus cannot be opened by a finger. Treat width as one input among many, and remember that CSS controls presentation, not payload.
Three strategies, one order
- Fluid: the design scales continuously with the space available. No thresholds; sizes change smoothly. The tools are relative units,
clamp(), and grids that fit as many columns as there is room for. - Responsive: the design changes at chosen thresholds. The tools are
@mediaand@containersize queries. Use it for structural changes that cannot be expressed as scaling: a sidebar that appears, a navigation bar that collapses. - Adaptive: the design responds to preferences and capabilities, not to size. The tools are the
prefers-*queries,forced-colors,pointer,hoverand@supports.
A good component uses all three, in that order. Start fluid. Add a breakpoint where fluid scaling stops working. Layer adaptive rules on top to respect what the reader and the device have told you.
2. Fluid design first
Responsive design built only from breakpoints has a known failure: the page is designed at the breakpoints and nowhere else. It looks right at 640 pixels and at 1024, and between them it is whatever stretched or squeezed version of the nearest design falls out of the rules. With the range of real viewport sizes (viewports.fyi charts it), most visitors land in between.
Fluid design removes the gap by defining values for every width at once. Normal flow is already fluid, as Basics showed: block boxes fill the inline space and text wraps. Percentages, fr tracks and ch-based measures keep that property. The piece flow lacks is a way to scale sizes, such as type and spacing, smoothly between a sensible minimum and maximum. That piece is clamp().
clamp() as a function of the viewport
clamp(MIN, PREFERRED, MAX) uses the preferred value unless it falls outside the bounds. When the preferred value mixes a fixed part with a viewport-relative part, the result is a piecewise-linear function of the viewport width: flat at the minimum, a straight rising line, flat at the maximum. Three values describe the whole curve.
The arithmetic is a line through two points. At a 20rem viewport (320 pixels at the default font size), 1.25vw is 0.25rem, so the preferred value is 1.25rem. At 80rem it is 1rem + 1rem = 2rem. Choose the two sizes you want at two widths, solve for the slope (the vw term) and the intercept (the rem term), and the clamp bounds the ends.
Keep a rem term in the preferred value. A size written purely in vw ignores the reader's default font size and barely changes when they zoom, which fails the WCAG requirement that text can be resized to 200 percent. The rem part keeps the reader in charge. The Fonts section measures this zoom trap and builds fluid type around it in Size, spacing and measure.
Let a tool do the algebra
Solving for slopes by hand for every heading level and spacing step gets old quickly. Utopia (utopia.fyi) generates a whole fluid type scale and space scale from a minimum and maximum viewport, a base size at each end and a ratio. It outputs custom properties, which drop straight into a token layer:
Intrinsic grids
Column counts can be fluid too. A grid that asks for as many columns as fit, each at least a minimum width, needs no breakpoints at all. The min() inside keeps a single column from overflowing a container narrower than the minimum:
Type, spacing, measure and column counts are usually fully handled by fluid rules. What fluid cannot do is change the structure of the page. That is what breakpoints are for, and fluid design does not abolish breakpoints. It saves them for the changes that need them.
3. Media queries and breakpoints
@media is one of three conditional at-rules, with @container and @supports. All three share a shape: a condition in the prelude, and a block of rules that apply only while the condition is true. They are the closest thing CSS has to an if statement, and the only place it can make rules conditional. Unlike a script that tests the screen once, they are live: when the window is resized, the device rotates, or the reader switches to dark mode, the browser re-evaluates them and restyles the page.
The parts of a media query
A query is an optional media type, then media features in parentheses, joined with and, or and not. A comma-separated list of queries matches if any one of them does.
The same queries work outside CSS. A link element's media attribute takes one, as does the media attribute on a source inside picture, and JavaScript can test or listen to one with window.matchMedia("(width >= 40rem)"). One vocabulary covers markup, style and script.
| Family | Features | Notes |
|---|---|---|
| Viewport | width, height, aspect-ratio, orientation | The layout viewport, not the physical device |
| Display | resolution, color-gamut, dynamic-range, update | update: slow describes e-ink and similar screens |
| Interaction | pointer, hover, any-pointer, any-hover | Covered in section 5 |
| Preference | prefers-color-scheme, prefers-reduced-motion, prefers-contrast, forced-colors | Covered in section 5 |
| Deprecated | device-width, device-height, device-aspect-ratio | Measured the screen rather than the viewport; use width and friends |
Of the media types, only all, screen and print remain useful, and a query without a type means all. The only keyword seen in older code (only screen and …) existed to hide a query from browsers too old to understand media features. Those browsers are gone, and so is the reason for the keyword.
Range syntax
Older queries wrote ranges as min-width and max-width. Both are inclusive, so two adjacent breakpoints overlap at the exact boundary, and authors patched the overlap with fractional pixel values:
Media Queries Level 4 added real comparison operators. They read as the condition they express, they make the boundary explicit, and they are Baseline widely available:
Breakpoints in rem, not px
A breakpoint written in pixels is a guess about screens. A breakpoint written in rem or em is a statement about content: "when there is room for about 40 characters of body text beside the sidebar". In a media query, both units are measured against the browser's initial font size, which the reader controls. A reader who sets a larger default font gets a larger breakpoint along with the larger text, so the page switches to its narrow layout sooner, exactly when the bigger text needs it. A pixel breakpoint would keep the two-column layout and squeeze the enlarged text into it.
Mobile first, and where to break
Write the baseline for the narrowest, most constrained case, then add rules for wider spaces with width >= queries. Each query adds capability rather than undoing it, and the narrow layout, which is usually close to plain normal flow, is also what a browser shows when an enhancement fails.
Choose breakpoints where this design breaks, not where some device's screen happens to end. Widen the window slowly and watch: the moment lines get too long, a row of links wraps awkwardly or a sidebar gets cramped is the breakpoint. Device widths change every year; the width at which your content needs a new arrangement does not. Two or three page-level breakpoints are usually plenty. A component that needs four is asking to be fluid instead, or to use a container query.
4. Container queries
A media query answers one question for the whole page: how wide is the viewport? That is the right question for page structure, such as whether a sidebar exists. It is the wrong question for a component. A card can appear in a wide main column and in a narrow sidebar on the same page, at the same viewport width. The viewport cannot tell the two apart; the card's container can.
Two steps
First, make an ancestor a size container with container-type. The value inline-size lets descendants query its inline size, and applies containment so the browser can size the container without looking at its contents (that is what keeps the query from depending on its own result). Then write @container rules, which match against the nearest ancestor container.
The stacked card is the baseline; the two-column card is the enhancement for containers with room. The heading uses cqi, one percent of the container's inline size, so the type is fluid relative to the card's space rather than the screen's. The six container units (cqw, cqh, cqi, cqb, cqmin, cqmax) mirror the viewport units. Size container queries and container units are Baseline widely available.
Naming containers, and what to avoid
When containers nest, container-name (or the container shorthand, container: card / inline-size) lets a query target a specific one: @container card (inline-size >= 30rem). Avoid container-type: size unless you really need to query height: it contains both dimensions, so the element no longer grows to fit its content and needs an explicit block size.
Size is not the only thing a container can be asked. Style queries such as @container style(--theme: dark) test a custom property's value on the container. Check their Baseline status before relying on them; at the time of writing they were not yet available in every major engine, so treat them as an enhancement.
| Concern | Tool | Why |
|---|---|---|
| Page structure: is there a sidebar? | @media width | The answer depends on the page |
| Component layout: how does this card render? | @container | The component may sit anywhere |
| Type and spacing scale | clamp() | Continuous change, no threshold |
| Color scheme, motion, contrast | Preference @media | Not about size at all |
| Touch versus mouse | pointer, hover | A capability, not a size |
A component that queries its container is portable: drop it anywhere and its layout follows. A component that queries the viewport is quietly tied to the one page layout it was designed inside.
5. Preference and capability queries
Size queries react to space. Preference queries react to the person: settings they chose in their operating system or browser, often for accessibility reasons. Ignoring them is not neutral. It overrides a choice the reader already made. This is where a style sheet does its part in the negotiation Basics described, in which the user, not the author, has the last word.
Color scheme, with light-dark()
The color-scheme property tells the browser which schemes a page supports, so its own parts (form controls, scrollbars, the default canvas) match. With light dark, the page follows the reader's prefers-color-scheme setting. The light-dark() function then picks one of two colors according to the scheme in use, so each token is defined once, with both values side by side:
light-dark() is Baseline newly available (since 2024), so a careful page gives older browsers a light baseline and wraps the light-dark() tokens in @supports, as the Basics demo does. The longer route works everywhere: define light tokens, then override them inside @media (prefers-color-scheme: dark). Either way the components never mention a scheme; they use tokens, and only the tokens change. A <meta name="color-scheme" content="light dark"> in the head tells the browser before any CSS arrives, which avoids a white flash on a dark page. The Color section's Light and dark page designs both palettes and adds a three-state toggle.
A site that wants a manual theme switch, the modern successor to alternate style sheets, can let a control set color-scheme itself. The tokens follow, with no script needed:
Reduced motion
Large movement, parallax and zooming transitions can cause dizziness and nausea for people with vestibular disorders, who turn on "reduce motion" in their system settings. Treat motion as an enhancement: the baseline has none, and it is added only for readers who have not asked to avoid it.
Reduced does not have to mean none. A gentle fade can replace a slide, and feedback such as a focus ring should never be removed. Animation itself belongs to the Animation topic (coming).
Contrast and forced colors
prefers-contrast: more matches readers who have asked for stronger contrast. Respond by strengthening what is already there: darker text tokens, solid borders where there were faint ones, no text over images. Color for people shows the tokens that change.
forced-colors: active is different in kind. In a forced-colors mode, such as Windows contrast themes, the system replaces the page's colors with a small palette the reader chose, and drops box shadows. The author's job is not to fight it but to make sure nothing important was carried only by color, background or shadow. A focus indicator drawn as a box-shadow disappears; one drawn with outline survives. System color keywords such as CanvasText and ButtonText name the palette's colors where a border has to come back.
Further preference features exist, such as prefers-reduced-transparency and prefers-reduced-data, but they are not Baseline. Use them only as optional refinements.
Capability: pointer and hover
The interaction features describe input, not screen size. pointer is fine for a mouse or trackpad, coarse for a finger, none without one. hover is hover when the primary input can hover, none when it cannot. Both describe the primary input; any-pointer and any-hover match if any attached input qualifies, which matters for a laptop with a touch screen.
Use the primary-input features for the main experience and the any- features for optional extras, and never make hover the only way to reach something. A menu that opens on :hover should also open on :focus-within or a click, because keyboards and fingers cannot hover.
6. @supports and progressive enhancement
Progressive enhancement is the discipline of building up from a baseline that works everywhere. Semantic HTML works with no CSS. CSS makes it look designed without any script. Script adds interaction on top. And newer CSS features are added behind detection, so their absence costs the reader an enhancement rather than a working page. Every layer can fail: a script is blocked, a style sheet times out, a browser is a few versions old. The page that degrades gracefully is the one built from the bottom up.
Two kinds of feature detection
The first is free. Because the parser drops declarations it does not understand (see forgiving parsing), a fallback followed by a modern value is already feature detection:
The second is @supports, for when several rules only make sense together, or when the baseline must change rather than simply be overridden. The condition is tested by parsing; nothing is applied until the block matches.
The last form tests a function through a property that accepts it: if the browser does not know light-dark(), the declaration is invalid and the condition is false. The selector() form tests whether a selector parses, which is how :has() was used safely before it reached every browser.
Conditions compose
Conditional at-rules nest inside rules and inside each other, and nested conditions combine as and. The transition below applies only when the card's container is wide and the reader has not asked for reduced motion:
Deciding with Baseline
| Status | Meaning | Approach |
|---|---|---|
| Widely available | In every major engine for at least 30 months | Use it directly |
| Newly available | In the current version of every major engine | Use it if its absence is harmless; otherwise give a fallback or gate it with @supports |
| Limited availability | Missing from at least one major engine | Enhancement only, behind @supports, with a tested baseline |
Status changes month by month, so check MDN or webstatus.dev at the time you ship, not the time you learned the feature.
7. Print
Print is the oldest media context and still a real one: receipts, boarding passes, recipes, articles saved as PDF, readers who think better with paper. Basics showed the minimal @media print block. A careful print style sheet does more.
- Remove what paper cannot use. Navigation, forms, video and buttons are dead on a page. Hide them.
- Absolute units are fine here. On screen,
ptandcmare approximations; on paper they are real. Print is the one place they are the natural choice. - Control page breaks. The old
page-break-beforeandpage-break-afterproperties are now the generalbreak-before,break-afterandbreak-inside, which also apply to columns. Keep a heading with the text after it and a figure in one piece. - Mind the paper. US Letter (8.5 by 11 inches) and A4 (210 by 297 millimeters) differ in shape. Margins set with
@pageand a fluid content width suit both; a layout tuned to one sheet will clip on the other. - Expose links. A link on paper is just underlined text. Printing the URL after it keeps the reference.
- Save ink. Browsers drop background colors and images by default when printing, so never carry meaning with a background alone.
8. Defensive design
Defensive CSS assumes things will go wrong and makes sure the page survives them: content longer than the design expected, images wider than their column, a font that never arrives, a custom property nobody set, a component whose script has not run. None of these habits is impressive alone. Together they are the difference between a page that holds up for every visitor and one that only works in the screenshot.
- Flexible media. An image or video wider than its column pushes the page sideways.
max-inline-size: 100%withblock-size: autolets it shrink and keep its proportions. Setwidthandheightattributes in the HTML too, so the browser reserves the space before the file arrives. - Long words. A long URL, an email address or a German compound noun does not wrap by default and overflows a narrow column.
overflow-wrap: break-wordbreaks it only when nothing else fits. Code blocks scroll inside themselves instead. min()for widths.min(100% - 2rem, 70rem)is a maximum width and a gutter in one value: never wider than 70rem, never wider than the space minus a margin. The same trick insideminmax()kept the intrinsic grid from overflowing in section 2.- Gaps, not margins, between items.
gapon a flex or grid container puts space only between items, so there is no stray margin on the last one to cancel when items wrap or reorder. - Fallbacks in
var().var(--button-bg, …)keeps a usable value if a token is missing or misspelled. - Undefined custom elements. Before a script registers a custom element, the browser renders it as an unknown inline element.
:not(:defined)can hide or reserve space for it until then, so the unstyled flash never shows. Use it only where the element's contents are not needed without script.
Test like a reader
The reader can override almost anything, and the test plan should assume they will. Zoom to 200 percent and check that nothing is cut off. Narrow the window to 320 CSS pixels, which is what a 1280-pixel window looks like at 400 percent zoom, and check that the content reflows into one column without scrolling sideways in two directions; WCAG's reflow criterion asks for exactly that. Set the browser's default font size larger, switch the system to dark mode, reduced motion and high contrast, and open the print preview. Open reader mode, which throws your CSS away entirely, to see whether the HTML alone carries the structure. Each check takes seconds, and each one is a real person's everyday setup. The Accessibility quality, and CSS for people next in this section, take these checks further.
9. Summary
| Concept | Key points |
|---|---|
| Contexts | Viewport, container, input, color scheme, motion, contrast, text size, medium and feature support vary independently. Width is the least of them. Start with the viewport meta tag. |
| Fluid first | clamp(MIN, PREFERRED, MAX) is a line with a floor and a ceiling. Keep a rem term so zoom works. Let Utopia generate the scales; use auto-fit and minmax(min(…, 100%), 1fr) for grids. |
| Media queries | Conditional, live, and shared with HTML and JavaScript. Use range syntax, (width >= 40rem). Write breakpoints in rem or em, mobile first, where the design breaks rather than where devices end. |
| Container queries | container-type: inline-size on an ancestor, @container on the component, cqi for fluid sizes within it. Page structure uses @media; components use @container. |
| Preferences | color-scheme with light-dark() tokens; motion only under prefers-reduced-motion: no-preference; strengthen under prefers-contrast: more; never carry meaning by color or shadow alone in forced-colors. |
| Capabilities | pointer and hover describe the primary input; the any- forms describe every input. Hover is never the only way in. |
| @supports | Cascade fallbacks for single values; @supports for grouped enhancements and selectors. Conditions nest and combine as and. Decide with Baseline status at ship time. |
Hide what paper cannot use, set @page margins, control break-*, expose link URLs, and use real units such as pt and cm. | |
| Defensive design | Flexible media, overflow-wrap, min() widths, gap, var() fallbacks, :not(:defined). Test at 200 percent zoom, at 320 CSS pixels, and with every preference switched on. |