Frontend Errors : Units, Rendering, Viewport
This skill is the operational reference for CSS length units, viewport-percentage units, font-relative units, the CSS pixel versus device pixel distinction, and the env(safe-area-inset-*) + viewport-fit=cover pairing for notch and Dynamic Island handling. It is an errors-class skill : every section is anchored to a real-world failure mode, with the spec citation that explains it. The skill does NOT cover container-query units (cqw, cqh, cqi, cqb, cqmin, cqmax ; see [[frontend-syntax-css-container-queries]]), clamp() typography (see [[frontend-impl-typography-system]]), or animation jank (see [[frontend-errors-animation-jank]]).
Quick Reference
Floor rules
- ALWAYS use
100dvh(or100svhfor guaranteed-fit) for full-height mobile heroes. NEVER use100vh; per MDN it currently resolves to100lvh, so the hero overflows when mobile chrome is visible. - ALWAYS use
rem(or%) forfont-size. NEVER useemforfont-sizein nested contexts ; it compounds (1 5 × 1 5 × 1 5 = 3 375 at three levels deep). - ALWAYS use
emfor properties that should scale WITH the local font-size : button padding, line-height multipliers, icon sizing inside a text element. NEVER useremfor those ; the relationship to the local text breaks. - ALWAYS include
viewport-fit=coverin<meta name="viewport">AND pair every edge-touching surface withenv(safe-area-inset-*). NEVER useenv(safe-area-inset-*)alone ; withoutviewport-fit=coverthe values are 0 on iOS. - ALWAYS multiply
<canvas>backing-store width and height bydevicePixelRatioand callctx.scale(dpr, dpr). NEVER setcanvas.width = canvas.style.width; the result is blurry on Retina. - ALWAYS prefer
100%over100vwfor full-bleed sections that are children of<body>. NEVER use100vwwithout considering classic-scrollbar systems where100vwoverflows by the scrollbar width. - ALWAYS use
rem-based font sizes that respect the user's browser zoom and font-size preference. NEVER hardcodefont-size: 14px; it blocks accessibility scaling. - NEVER use
in,cm,mm,Qfor screen layouts. They are anchored to the CSS reference inch (1in = 96px), NOT to physical inches. Reserve for@media print.
Decision tree 1 : Which viewport unit for which use case ?
Full-height hero that should adapt as mobile chrome shows / hides ?
-> 100dvh. UA throttles updates (step, not animate) during chrome transition.
Hero / modal that must NEVER overflow even with chrome fully visible ?
-> 100svh. Conservative ; works on every device.
Full-screen overlay that should fill the maximum extent when chrome is retracted ?
-> 100lvh. Equivalent to current default vh behavior per MDN.
Writing-mode-agnostic full-width container (vertical-rl Japanese, ltr Latin, rtl Arabic) ?
-> 100dvi (or 100vi for legacy). Inline-axis percentage, follows writing-mode.
Full-block-axis container with writing-mode awareness ?
-> 100dvb (or 100vb for legacy).
Layered fallback for older browsers that lack dv* ?
-> Author 100svh as the conservative default, override with 100dvh inside an
@supports (height: 100dvh) block. Modern browsers get adaptive ; older
get guaranteed-fit.
Legacy support needed and you cannot rewrite ?
-> 100vh, but acknowledge mobile chrome catastrophe. Document the trade-off.
Pre-2022 community workaround : --vh CSS custom property updated from JS
on resize. This is now obsolete ; migrate to dvh as soon as possible.
Decision tree 2 : em or rem ?
Property is font-size ?
-> rem (or %). em compounds in nested rules and produces unpredictable
sizing three levels deep.
Property is button / control padding that should grow with button label text ?
-> em. Padding 0 5em 1em on a <button> scales naturally with the button's
own font-size.
Property is line-height ?
-> Unitless (preferred ; multiplies the local font-size without compounding
inheritance issues) OR em (same scaling behavior).
Property is a container max-width for prose ?
-> ch. max-width: 65ch produces the canonical 45-to-75-character measure.
Property is an icon height that aligns with surrounding lowercase text ?
-> ex (height of x). The icon sits flush with the baseline of lowercase glyphs.
Property is the gap between flex items in a card grid that scales with the
parent's rem-based font-size ?
-> rem. Predictable across nesting.
Property is a uppercase-only logo height aligned to capital-letter visual extent ?
-> cap. Excludes ascenders and descenders.
Decision tree 3 : Notch / safe-area / device-pixel ?
Designing a page that touches the top, bottom, left, or right viewport edge ?
-> Add viewport-fit=cover to the viewport meta tag AND pad each edge-touching
surface with the matching env(safe-area-inset-*) variable, wrapped in calc().
Sticky bottom nav above the iPhone home indicator ?
-> padding-bottom: calc(1rem + env(safe-area-inset-bottom));
Sticky top nav under the iPhone Dynamic Island ?
-> padding-top: calc(0.75rem + env(safe-area-inset-top));
Foldable / dual-screen layout that should respect the hinge ?
-> env(viewport-segment-width 0 0), env(viewport-segment-left 0 0), etc.
The integers select which segment (0-based row, column).
Soft keyboard overlay pushing UI ?
-> navigator.virtualKeyboard.overlaysContent = true PLUS env(keyboard-inset-*).
Chromium-only ; feature-detect and fall back to viewport resize.
Canvas / WebGL drawing on a Retina / HiDPI display ?
-> Read devicePixelRatio. Set canvas.style.width / height in CSS px ; set
canvas.width / height to size * dpr. Call ctx.scale(dpr, dpr).
Raster image that should stay sharp on HiDPI ?
-> <img srcset="img.png 1x, img-2x.png 2x" src="img.png" ...> Let the browser
pick the right asset.
Hairline border that must render at exactly one device pixel ?
-> Accept 1 CSS px (renders fine on every DPR) OR use SVG line of stroke-width=1.
NEVER use border: 0.5px ; behaviour is inconsistent across DPRs.
Patterns
Pattern : Mobile-safe full-height hero
.hero {
min-height: 100svh;
}
@supports (height: 100dvh) {
.hero {
min-height: 100dvh;
}
}
100svh is the conservative baseline. Where supported, 100dvh upgrades the experience to adapt as chrome retracts. Both avoid the 100vh catastrophe.
Pattern : Safe-area-respecting sticky footer
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
footer {
position: sticky;
bottom: 0;
padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom));
}
The calc() wrapper is mandatory ; env() values cannot be added to a bare length without calc(). The MDN warning : env(SAFE-AREA-INSET-BOTTOM) is invalid (uppercase) and falls back silently.
Pattern : Predictable font-size + scaling padding
:root {
font-size: 100%; /* respects user browser preference, typically 16px */
}
body {
font-size: 1rem; /* explicit */
}
.heading {
font-size: 1.5rem; /* always 1.5x root, regardless of nesting */
}
.button {
font-size: 1rem;
padding: 0.5em 1em; /* em so padding scales WITH the button's font-size */
}
.button.large {
font-size: 1.25rem;
/* padding stays 0.5em 1em ; the em re-resolves against the new font-size */
}
rem for font-size avoids compounding. em for padding lets the button's footprint grow naturally when the label text grows.
Pattern : Retina-sharp canvas
function setupCanvas(canvas, size) {
const dpr = window.devicePixelRatio || 1;
canvas.style.width = `${size}px`;
canvas.style.height = `${size}px`;
canvas.width = Math.floor(size * dpr);
canvas.height = Math.floor(size * dpr);
const ctx = canvas.getContext("2d");
ctx.scale(dpr, dpr);
return ctx;
}
CSS pixels are the layout unit ; backing-store pixels carry the actual rendering resolution. Without the scale, every drawing call is sized in backing-store pixels and the result is half-size on DPR 2.
Pattern : HiDPI raster images
<img
src="hero.jpg"
srcset="hero-1x.jpg 1x, hero-2x.jpg 2x, hero-3x.jpg 3x"
width="800"
height="450"
alt="..."
/>
The browser picks the highest-density asset that fits the display. Pair with explicit width / height to reserve the layout box (see [[frontend-perf-core-web-vitals-inp]]).
Pattern : Writing-mode-agnostic full-bleed
.section {
inline-size: 100dvi;
block-size: 100dvb;
}
100dvi resolves to width in horizontal writing modes and height in vertical ones. Combine with logical properties (inline-size, block-size, padding-inline, padding-block) to keep layouts portable across writing-mode and direction.
Pattern : Scrollbar-aware 100vw
:root {
scrollbar-gutter: stable;
}
.full-bleed {
width: 100%; /* preferred for body children */
}
.alt-full-bleed {
width: 100dvw; /* if you specifically need the dynamic viewport width */
margin-inline: calc(50% - 50dvw); /* break out of a constrained parent */
}
100vw includes the scrollbar gutter on classic-scrollbar systems. 100% of a body child sidesteps the issue. scrollbar-gutter: stable reserves the gutter consistently so layouts do not jump when scrollbars appear / disappear.
Baseline status
| Surface | Status | Notes |
|---|---|---|
vh, vw, vmin, vmax, vi, vb |
Widely Available since July 2015 | Default vh resolves to lvh per MDN. |
sv*, lv*, dv* families |
Newly Available 2023, Widely Available 2026 | Cross-browser : Chrome 108+, Edge 108+, Firefox 101+, Safari 15 4+. |
env(safe-area-inset-*) |
Widely Available since January 2020 | Requires viewport-fit=cover on iOS. |
env(keyboard-inset-*) |
Limited Availability | Chromium-only behind navigator.virtualKeyboard.overlaysContent = true. |
env(viewport-segment-*) |
Limited Availability | Foldable / dual-screen devices only. |
devicePixelRatio |
Baseline Widely Available | Common values 1 / 1 5 / 2 / 2 5 / 3 / 4. |
scrollbar-gutter |
Baseline Widely Available | Reserves the scrollbar space ; stable is the common value. |
Cross-references
[[frontend-impl-responsive-layout-fluid]]: fluid type-scale,clamp(), container-query-based layout.[[frontend-impl-typography-system]]: type-scale tokens,clamp()-based fluid sizing, webfont strategy.[[frontend-syntax-css-container-queries]]: container-query units (cqw,cqh,cqi,cqb,cqmin,cqmax).[[frontend-perf-core-web-vitals-inp]]: CLS prevention via explicitwidth/heighton images.[[frontend-errors-layout-pitfalls]]: layout-specific failure modes.
Reference Links
- references/methods.md : full unit surface, viewport family table with
w/h/min/max/i/baxes,env()variable list,<meta name="viewport">attribute matrix. - references/examples.md : renderable HTML demo comparing
100vhvs100dvhvs100svhside-by-side, safe-area inset visualisation, retina canvas pattern. - references/anti-patterns.md : nine anti-patterns with symptom, root cause, and fix.
Authoritative sources
- MDN : length (verified 2026-05-19)
- MDN : CSS values and units : Numeric data types (verified 2026-05-19)
- MDN : env() (verified 2026-05-19)
- MDN : Window.devicePixelRatio (verified 2026-05-19)
- MDN :
<meta name="viewport">(verified 2026-05-19) - W3C : css-values-4 : viewport-relative-lengths (verified 2026-05-19)
- web.dev : viewport-units (verified 2026-05-19)