Viewport Units vs Container Units: Choosing the Right Reference Box
5vw and 5cqi look almost identical in a stylesheet and behave the same way when a component happens to fill the window. They diverge everywhere else: in a sidebar, a modal, a grid cell, a card that appears at three different widths on the same page. The difference is the reference box — the viewport for one, the nearest query container for the other — and choosing correctly is what makes a component portable. This page compares the two families precisely, including the fallbacks, the scrollbar and zoom behaviours that catch people out, and a decision rule for mixing them. It belongs to Viewport Units & Mobile Layout in the Mastering Container Queries & Responsive Layouts guide.
Why the reference box decides portability
A component designed with viewport units carries an assumption: that it spans roughly the viewport. A hero banner does, so clamp(2rem, 6vw, 4rem) for its heading is reasonable. A product card does not — it might be a quarter of the page on desktop and the full width on mobile — so a heading sized in vw is enormous on a wide screen where the card is narrow, and correct only by coincidence on a phone where the card fills the width.
Container query units remove the assumption. 1cqi is 1% of the inline size of the nearest ancestor that is a size container. Drop the card into a sidebar and its heading shrinks with the sidebar; drop it into a full-width band and it grows. The component's proportions travel with it, which is the whole promise of container queries extended from breakpoints to continuous sizing.
The two families side by side
| Viewport units | Container query units | |
|---|---|---|
| Inline axis | vw, svw, lvw, dvw, vi | cqw, cqi |
| Block axis | vh, svh, lvh, dvh, vb | cqh, cqb |
| Min / max | vmin, vmax and variants | cqmin, cqmax |
| Reference box | The viewport (initial containing block) | Nearest size container's content box |
| Includes scrollbar | Yes, on platforms with space-taking scrollbars | No — content box only |
| Fallback when no reference | Always has one | Small viewport units (sv*) |
| Changes with mobile toolbar | dv* only | Only if the container changes size |
Two rows deserve emphasis. The scrollbar row explains a whole class of bugs: 100vw is wider than the page's content area on Windows and Linux, while 100cqi on a container equals its content width exactly. The fallback row explains why container units are safe to use in a component that might be rendered outside any container: they degrade to small-viewport units rather than to zero.
The complete implementation
The component below uses container units for everything that belongs to the component and viewport units only for the one thing that belongs to the page — the maximum height of its expandable panel. Resize the frame to see the typography and padding track the card rather than the window.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Viewport vs container units</title>
<style>
body { font: 16px/1.5 system-ui, sans-serif; margin: 1rem; }
/* The card's wrapper is an inline-size container, so cqi resolves
against the card's available width wherever it is placed. */
.card-slot { container-type: inline-size; }
.card {
/* Component-owned spacing: grows with the card, bounded in rem so it
never gets silly on very narrow or very wide cards. */
padding: clamp(0.75rem, 4cqi, 2rem);
border: 1px solid #cbd5e1;
border-radius: clamp(8px, 2cqi, 16px);
}
.card__title {
/* Component-owned type: rem floor and ceiling keep zoom working
(WCAG 1.4.4); the cqi term provides the scaling. */
font-size: clamp(1.1rem, 0.8rem + 3cqi, 2rem);
margin: 0 0 0.5em;
}
.card__details {
/* Page-owned limit: an expanded panel should never be taller than
most of the visible screen, whatever the card's width. */
max-height: 60svh;
max-height: 60dvh;
overflow: auto;
}
</style>
</head>
<body>
<div class="card-slot">
<article class="card">
<h2 class="card__title">Quarterly report</h2>
<div class="card__details"><p>Details that may be long…</p></div>
</article>
</div>
</body>
</html>
The split in that stylesheet is the transferable lesson. Padding, radius and heading size are properties of the card's design, so they scale with the card. The panel's maximum height is a property of the screen — no matter how the card is laid out, a scrolling panel taller than the visible viewport is unusable — so it references the viewport.
The key technique: ask who owns the dimension
Most confusion disappears once each dimension is assigned an owner. The heading of a page-wide hero is owned by the page and can reasonably use vw. The heading of a card is owned by the card and should use cqi. The maximum height of any scrollable overlay is owned by the screen and should use a viewport unit whatever component it belongs to. There is no global rule that one family is better; there is only the question of which box the dimension is really about.
Text needs one extra rule in both cases. Neither vw nor cqi responds to browser zoom in the way rem does — zooming a desktop browser shrinks the CSS viewport, so vw text barely grows, and container widths behave similarly. WCAG 1.4.4 Resize Text requires text to reach 200% without loss of content, which a pure viewport- or container-unit font size can fail. Always include a rem term: a rem minimum and maximum in clamp(), and ideally a rem addend in the preferred value, so zoom always contributes. Fluid Type, Accessibility and Zoom works through the arithmetic.
Variation: block-axis container units need size containment
cqi is the everyday unit because inline-size containers are the everyday container type. The block-axis units, cqb and cqh, need a container with size containment in the block axis, which means container-type: size — and a size container does not take its height from its content, so it must be given one.
/* A fixed-height tile whose internal type scales with its height.
Without an explicit height, a size container collapses to zero. */
.tile {
container-type: size;
block-size: 12rem;
inline-size: 100%;
}
.tile__value {
/* Scales with the tile's HEIGHT, which a width-based unit cannot see. */
font-size: clamp(1.5rem, 25cqb, 4rem);
line-height: 1;
}
/* cqmin picks the smaller axis: useful for square-ish icons that must
fit whichever dimension is tighter. */
.tile__icon {
inline-size: 30cqmin;
block-size: 30cqmin;
}
If cqb seems to be measuring the wrong thing, the usual cause is that the nearest container is inline-size only, so the unit skips it and resolves against a further ancestor or the small viewport. The full explanation of why size containment requires an explicit block size is in container-type: size vs inline-size. Container units also work in animation: a slide distance written in cqi scales with the component, as shown in Motion That Scales With Container Size.
Migrating component styles from vw to cqi
Most codebases already have vw inside component styles, usually in fluid type written before container units existed. Converting them is mechanical but has one trap: a container unit needs a container, and the container has to be the right one.
Start by deciding where each component's container lives. The safest convention is a wrapper element owned by the component itself — a .card-slot or the component's root element — with container-type: inline-size. That guarantees cqi inside the component always refers to the component's own width, rather than to whatever ancestor happens to be a container on a given page. Then replace each vw with cqi and re-tune the coefficient, because the reference box is smaller: a heading that was 4vw in a card occupying a third of the viewport needs roughly 12cqi to look the same at that placement, and will now look right at every other placement as well.
Test the smallest real placement before the largest. Container-relative sizes are most likely to hit their clamp() floor inside narrow slots such as sidebars and table cells, and a floor that was never reached with vw may suddenly apply everywhere. If it does, the floor is doing its job — protecting legibility — but it may reveal that the design assumed more room than the component actually gets.
Finally, check that no component relied on the scrollbar overhang of vw for a full-width effect. Swapping to cqi removes the overhang, which fixes the horizontal scrollbar bug but can leave a one-scrollbar gap where a band previously reached the edge. The full-bleed grid technique handles those cases without either unit.
Browser support
Container query units (cqw, cqh, cqi, cqb, cqmin, cqmax) are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+. The small, large and dynamic viewport units are supported in Chrome and Edge 108+, Firefox 101+ and Safari 15.4+, and classic vw and vh are universal. clamp() is supported in Chrome and Edge 79+, Firefox 75+ and Safari 13.1+. For container units in an older engine, declare a vw- or rem-based value first; the unrecognised unit invalidates only its own declaration.
FAQ
What does cqi measure if there is no container?
It falls back to the small viewport. If no ancestor is a size container in the relevant axis, 1cqi resolves as 1svi, so a component using container units still gets a sensible viewport-relative size when placed outside any container.
Are container units the same as percentages?
No. A percentage resolves against a property-specific reference, such as the containing block's width for padding or the parent's font size for font-size. Container units always measure the query container's content box, so 5cqi means the same length in padding, font-size, gap or a transform.
Why is my cqb value zero or wrong?cqb measures the block size of the nearest container that has size containment in the block axis. An inline-size container does not qualify, so cqb skips it and uses a further ancestor or the small viewport. Set container-type: size on the intended container, which requires giving it an explicit height.
Should font sizes use vw or cqi?
Neither alone. Both fail WCAG 1.4.4 if used without a rem component, because zoom does not change them proportionally. Use clamp() with a rem-based minimum and maximum and a unit-plus-rem preferred value, and choose cqi when the type belongs to a component and vw when it belongs to the page.
Related
- Viewport Units & Mobile Layout — the parent guide to viewport-relative sizing.
- Container Query Units: cqi, cqb Explained — the container unit family in depth.
- dvh, svh and lvh — choosing among the viewport height units.
- Motion That Scales With Container Size — container units applied to animation distance.
Related articles
More pages in the same section.