Viewport · Chapter 4 of 7 · 24 units

Viewport Units

The screen is not a rectangle. It’s a negotiation.

Viewport units measure the page against the viewport — the visible area where the document is rendered. They are easy to start using, but they hide one of the most important layout problems in modern CSS: the viewport is not always one stable rectangle.

On desktop, the mental model is simple: the browser window has a width and a height, and 1vw or 1vh is one percent of those dimensions. On mobile, the browser itself takes up space — address bars, bottom toolbars, tab controls, pull-to-refresh areas, safe areas, and sometimes a virtual keyboard. Some of that browser UI appears and disappears while the user scrolls. That is why viewport units now come in several families: default, small, large, and dynamic.

The short version:

vw / vh      old default viewport units
vmin / vmax  smaller / larger side of the viewport
vi / vb      logical viewport units, aware of writing direction
sv*          small viewport: sized as if chrome were expanded
lv*          large viewport: sized as if chrome were retracted
dv*          dynamic viewport: current visible state

The big lesson:

100vh does not always mean “the visible screen right now”. On modern mobile browsers it often behaves like 100lvh — the height of the large viewport, as if dynamic browser chrome were hidden. That one detail explains years of broken mobile full-screen layouts.

The simulator below shows the difference. Toggle the browser toolbars and watch each unit respond — both on the screen and in the numbers beside it.

Demo — Viewport units on a simulated mobile screen
100vh
100svh
100lvh
100dvh
9:41●●●
example.com

A simplified phone: 800px of web content with the toolbars away, 720px with them out.

vh800px= lvh, ignores the toolbars
svh720pxsmall viewport, always fits
lvh800pxlarge viewport, may clip
dvh800pxtracks the current state

Toolbars away — every unit but svh measures the full 800px.svh is still holding back the 80px a toolbar would need, so it leaves a gap at the bottom.

Toggle the toolbars and watch which numbers move: only dvh changes. On screen, svh and dvh stop above the bottom bar, whilevh and lvh keep going behind it — those last 80px are where bottom buttons go to hide.


vw / vh

Formula
1vw = 1% of default viewport width; 1vh = 1% of default viewport height
Relative to
Default viewport size
Support
Universal

vw and vh are the classic viewport units. They let an element size itself as a percentage of the viewport width or height.

1vw  = 1% of the viewport width
1vh  = 1% of the viewport height

If the viewport is 1200px wide and 800px tall, then 1vw = 12px, 1vh = 8px, 100vw = 1200px, and 100vh = 800px.

These felt magical when they became widely supported. You could create full-screen panels without JavaScript:

.hero {
  min-height: 100vh;
}

Or make typography respond to the window size:

.headline {
  font-size: 8vw;
}

But hold that thought — pure viewport units on font-size are an accessibility hazard, covered just below. The safe form caps them with clamp() and a rem term.

The default family is defined against the large viewport. That is worth stating precisely, because it changed: earlier editions of CSS Values 4 let each browser pick between the small size, the large size or something in between, and the current text pins v* to the large viewport size because that is what engines had already shipped and what web compatibility now requires. So vw/vh are lvw/lvh, not “usually” but by definition. (On desktop there’s no retractable chrome, so small, large and default all coincide.) The distinction only shows up on mobile.

vw and vh are relative to the initial containing block, which is based on the viewport in continuous media such as screens. They are not relative to a parent, not relative to font size, and not relative to the element itself. That makes them useful for page-level composition.

Demo — A box sized at 30vw × 20vh
30vw × 20vh ×
Viewport ×
1vw / 1vh /

Resize your browser window to see vw and vh respond. The box uses real viewport units, so it tracks the window directly.

Use when
  • Full-bleed visual sections: width: 100vw on a deliberate hero band
  • Editorial headlines — via clamp(2rem, 5vw + 1rem, 4rem), where the rem term keeps it zoom-accessible
  • Hero sections, slide-like layouts, app shells where main region fills vertical space
  • Map or editor canvases that should fill the window
Avoid when
  • Bare font-size: 8vw for text — viewport units don't enlarge on zoom (accessibility risk)
  • Replacing width: 100% blindly — 100vw can include the scrollbar gutter
  • Critical mobile full-screen UI — bottom buttons can hide behind chrome
  • Anything where overflow or clipping would be unacceptable

Pitfall: viewport units and the zoom accessibility trap

There’s a catch with viewport-based typography that bites real users. Pure viewport-relative text is tied to the viewport geometry, not to the user’s chosen text scale. Depending on the browser’s zoom and reflow behavior, a headline sized only in vw can fail to become meaningfully larger for a low-vision user. That can fail WCAG 1.4.4 Resize Text, which requires text to be resizable up to 200% without loss of content or functionality.

/* Risky: may fail to reach 200% effective text enlargement */
.headline { font-size: 8vw; }

/* Safer: rem terms still respond to zoom, vw adds fluidity */
.headline { font-size: clamp(2rem, 5vw + 1rem, 4rem); }

Two further nuances from accessibility research:

  • Capping with clamp()/max() can itself fail 1.4.4 if the cap stops text from reaching 200%. Maxwell Barvian’s analysis for Smashing Magazine offers a rule of thumb: keep the maximum at or under 2.5× the minimum. Treat it as a smell test rather than a proof — how much zoom actually reaches the text also depends on how the preferred value splits between its vw and rem terms, so verify at 200% instead of trusting the ratio.
  • rem is preferable to px in the fluid formula, but rem alone isn’t a guarantee — always test at 200% zoom and with an enlarged browser default font-size.

Pitfall: the 100vh mobile bug

The famous 100vh problem is not that browsers cannot calculate height. The problem is that mobile viewport height has more than one reasonable answer.Mobile Safari has collapsed its address bar on scroll since 2014 — the same year it shipped, then withdrew, a minimal-ui viewport flag aimed at this exact problem. dvh, the real fix, arrived in 2022. Eight years is the going rate for a viewport bug to become a unit.

Imagine an iPhone browser in two states:

State A: address bar visible
Visible content height: 700px

State B: address bar hidden after scroll
Visible content height: 780px

What should 100vh be?

If 100vh = 700px, the section fits while the address bar is visible, but leaves a gap when it hides. If 100vh = 780px, the section fills the larger screen after chrome retracts, but part of it is hidden behind the address bar when chrome is visible.

Browsers historically chose stability. Instead of changing vh constantly while the user scrolls, many mobile browsers made 100vh behave like the larger height. That avoids jumpy layouts but creates the classic bug:

100vh is taller than the visible screen when mobile browser chrome is visible.

That is why a height: 100vh modal can have its bottom buttons hidden behind Safari’s bottom toolbar or Google Chrome’s address controls.

Pitfall: 100vw and scrollbars

100vw means the full viewport width. On desktop systems with classic scrollbars, the viewport width can include the scrollbar area. The document content area is then slightly narrower than 100vw, which causes a horizontal scrollbar to appear.


vmin / vmax

Formula
1vmin = 1% of the smaller default viewport dimension; 1vmax = 1% of the larger
Relative to
Default viewport width/height comparison
Support
Universal

vmin and vmax compare the viewport’s two dimensions:

1vmin = 1% of the smaller viewport dimension
1vmax = 1% of the larger viewport dimension

On a 1200×800 landscape viewport, 1vmin = 8px and 1vmax = 12px. On a 390×844 portrait viewport, 1vmin = 3.9px and 1vmax = 8.44px. The key idea: vmin and vmax are orientation-aware without needing media queries.vmin is the unit of board games, clock faces, and camera viewfinders — anything that must stay square no matter the shape of the room it’s shown in.

1vmin = min(1vw, 1vh)
1vmax = max(1vw, 1vh)

They do not care about the parent element. They do not care about writing mode. They only ask: which side of the viewport is smaller, and which side is larger?

The most common real use for vmin is a square or circular object that should fit inside the viewport in both portrait and landscape:

.orb {
  width: 80vmin;
  height: 80vmin;
  border-radius: 50%;
}

Or a viewport-sized chessboard:

.board {
  width: 90vmin;
  height: 90vmin;
}

This works because vmin always follows the limiting side. On a phone, width is often the limit. On a laptop, height may be the limit.

vmax is useful when something should scale with the larger dimension — typically oversized background shapes, full-bleed masks, or large visual treatments that should feel huge in any orientation.

Demo — vmin vs vmax across orientations
80vmin
50vmax

The solid square (80vmin) always fits inside the viewport. The dashed circle (50vmax) is sized to the long side, so it can stretch past the short side.

Use when
  • Square or circular hero artwork: width/height: 80vmin
  • Game boards, diagrams, centered visual demos that must stay fully visible
  • vmax for oversized background shapes and full-bleed masks
  • Anywhere you want orientation-aware sizing without media queries
Avoid when
  • Plain text headings via bare 8vmin — it can land too small on phones and skips zoom-safety (wrap it in clamp() with a rem term)
  • 100vmax for layout dimensions — it exceeds one axis by definition, so it overflows
  • Anywhere overflow or clipping would be unacceptable

vmin and vmax inherit the same mobile viewport ambiguity as vh and vw. Plain vmin/vmax belong to the default viewport family — equivalent to lvmin/lvmax. That means mobile browser chrome can still affect whether the value matches what the user currently sees. For most vmin cases it is less dramatic, because phone width is usually the smaller dimension and chrome usually changes height. But it can matter in landscape, on foldables, on tablets, or in unusual browser UI.


vi / vb

Formula
1vi = 1% of viewport inline size; 1vb = 1% of viewport block size
Relative to
Default viewport size on the logical inline/block axes
Support
Modern browsersall engines since Nov 2022

vi and vb are logical viewport units. They are like vw and vh, but instead of measuring physical width and height, they measure the viewport along the document’s logical text axes.

vi = viewport inline axis
vb = viewport block axis

In a typical English or Russian page (writing-mode: horizontal-tb), the inline axis is horizontal and the block axis is vertical, so 1vi = 1vw and 1vb = 1vh. But in vertical writing modes this flips:

html {
  writing-mode: vertical-rl;
}

In vertical writing, the inline axis runs vertically and the block axis runs horizontally — 1vi behaves like 1vh, and 1vb like 1vw. One precision that matters: the mapping is decided by the root element’s writing mode. An element’s own writing-mode doesn’t change what 1vi resolves to — that per-element sensitivity belongs to container units like cqi.

vi and vb are part of the same mental model as logical CSS properties: margin-inline, padding-block, inset-inline. The point is not “left/right/top/bottom”; the point is inline direction and block direction.

Use them when your layout should work across writing modes — CJK typography (especially vertical Japanese or Chinese), multilingual templates, e-book or reading interfaces, publishing systems:

.reader-page {
  max-inline-size: 80vi;
  min-block-size: 100vb;
  padding-inline: 5vi;
}

In horizontal writing this behaves like a viewport-width-aware reading page. In vertical writing, the same CSS follows the text flow.

Demo — Writing mode and logical viewport units
block axis →
inline axis →
The quick brown fox jumps over the lazy dog. The text flows along the inline axis. Lines stack along the block axis.
viruns along width (= vw)
vbruns along height (= vh)

In horizontal writing, vi = vw and vb = vh. In vertical writing, the axes swap: vi runs vertically and vb runs horizontally.

Use when
  • CJK typography, especially vertical Japanese or Chinese layouts
  • Multilingual article templates that must support horizontal and vertical writing
  • Codebases that already use logical properties everywhere
  • Components that need to support RTL/LTR without rewriting CSS
Avoid when
  • Single-language horizontal sites where writing mode never changes
  • When the team isn't familiar with logical CSS — adds cognitive overhead
  • As decorative complexity — only use when logical axes are a real product requirement

Pitfall: vivw

vi and vb are not just new names for vw and vh. In horizontal writing, vi = vw and vb = vh, but that equivalence is a coincidence of the writing mode. Once the root’s writing mode changes, the mapping changes.

Another nuance: direction: rtl changes the inline direction, but not which physical dimension is inline. For any horizontal text — LTR or RTL — inline runs horizontally, so vi still maps to width. The dramatic difference comes only from vertical writing modes.


The Disappearing Toolbar Problem

Retractable browser toolbars are the reason the new viewport unit families exist.

Chrome is the old interface word for the frame around the content: the address bar, the bottom toolbar, the tab strip, the navigation controls. Every browser has chrome, and this chapter is about all of them — the word is decades older than the browser that took it for a name. On a phone that frame is dynamic: it is there when the page loads, then shrinks or slides away as the reader scrolls. The visible content area changes while the layout underneath still needs to hold still. That creates a conflict:

Should viewport units follow the current visible area? Or should they stay stable while the user scrolls? Both answers are useful. Neither answer is always correct.

Why 100vh can be bigger than the iPhone screen

Imagine a simplified phone:

Physical screen height:                  844px
Visible web content with browser chrome: 720px
Visible web content after chrome hides:  800px

(The large viewport is 800, not the full 844 — non-retractable UI like the status bar and home indicator still reserves space.)

If CSS says:

.modal {
  height: 100vh;
}

the browser has to choose what 100vh means. Historically, many mobile browsers made vh match the larger viewport: the size available after dynamic browser UI retracts. That makes the layout stable and prevents the page from resizing every time the address bar animates.

So the browser effectively says 100vh = 800px — but the user currently sees only 720px. Result: 80px of the element are behind browser chrome or below the visible area.

Why browsers did this

At first glance, it sounds wrong. If the visible area is 720px, why not make 100vh = 720px?

Because then vh would change while scrolling. Consider:

.hero {
  height: 100vh;
}

If browser chrome hides during scroll, 100vh would animate from 720px to 800px. That means the layout changes while the user is in the middle of reading or scrolling. Content jumps. Scroll positions become weird. Animations and sticky elements feel unstable.

So browsers picked a stable value. That solved one class of problems and created another.

The old workaround era

Before svh, lvh, and dvh, developers used JavaScript:

document.documentElement.style.setProperty(
  '--vh',
  `${window.innerHeight * 0.01}px`
);

Then:

.screen {
  height: calc(var(--vh) * 100);
}

This measured the current inner height and stored it in a CSS custom property. It worked, but it had costs: JavaScript had to run before layout was correct; resize and orientation changes needed handling; mobile browser behavior still varied; scroll-driven chrome changes could cause jank; every project reinvented the same workaround.

The new viewport units are the CSS-native answer to this problem.

The conceptual fix

CSS now gives names to the different possible viewport heights:

small viewport:   sized as if browser chrome were expanded
large viewport:   sized as if browser chrome were retracted
dynamic viewport: current visible state

Authors can finally choose the behavior they actually want:

.safe-screen     { min-height: 100svh; }
.immersive-hero  { min-height: 100lvh; }
.modal           { height: 100dvh; }

svh / lvh / dvh — and the full S/L/D family

The new viewport units split viewport sizing into three explicit families:

sv* = small viewport
lv* = large viewport
dv* = dynamic viewport

Each family has six units. Together with the default family, this gives the complete viewport unit map:

Axis / Strategy Default Small Large Dynamic
Width vw svw lvw dvw
Height vh svh lvh dvh
Inline axis vi svi lvi dvi
Block axis vb svb lvb dvb
Smaller of width / height vmin svmin lvmin dvmin
Larger of width / height vmax svmax lvmax dvmax

The default units are defined against the large family, so each pair is the same unit under two names:

vw = lvw   vh = lvh   vi = lvi   vb = lvb   vmin = lvmin   vmax = lvmax

That is the reason 100vh behaves like the large viewport on mobile — and since the spec now says so normatively, it is not a browser quirk you can hope to see fixed.

svh (small viewport)

Formula
1svh = small viewport height ÷ 100
Relative to
Viewport with dynamic browser UI assumed to be expanded
Support
Modern browsersall engines since Nov 2022

The small viewport is the viewport size when dynamic browser UI is expanded: address bar visible, toolbar visible, maximum browser chrome taking space.

100svh = the height that safely fits when browser chrome is visible

The full small family is svw, svh, svi, svb, svmin, svmax — each measured against the conservative viewport size.

.onboarding {
  min-height: 100svh;
}

This says: fit within the conservative mobile viewport.

Use when
  • First-screen content with important controls at the bottom
  • Onboarding screens, payment / checkout steps
  • Compact app screens where safety matters more than immersion
  • Fallback sizing when leaving extra space is better than hiding content
Avoid when
  • Immersive hero sections — svh can leave a visible gap after chrome retracts
  • Layouts that should always cover the full available visual area

svh is stable, not dynamic. It does not grow just because more space becomes available. That is its strength and its weakness:

  • Pro: content remains safely visible.
  • Con: it may not use all available space.

lvh (large viewport)

Formula
1lvh = large viewport height ÷ 100
Relative to
Viewport with dynamic browser UI assumed to be retracted
Support
Modern browsersall engines since Nov 2022

The large viewport is the viewport size when dynamic browser UI is retracted: address bar hidden, toolbar minimized, maximum content area.

100lvh = the height of the screen when browser chrome is out of the way

The full large family is lvw, lvh, lvi, lvb, lvmin, lvmax.

.landing-hero {
  min-height: 100lvh;
}

This says: size the hero to the largest mobile viewport state.

Use when
  • Immersive hero sections, full-bleed background media
  • Large editorial visuals where being partially behind chrome at load is acceptable
  • Decorative content that can safely be cropped
  • Layouts where the user is expected to scroll past the first screen
Avoid when
  • Critical controls — bottom buttons can hide behind chrome
  • Modals, drawers, or any flow where the bottom must always be reachable

lvh is essentially the named version of the old vh behavior. That is useful because it is now explicit. But it also means it carries the classic 100vh mobile pitfall:

dvh (dynamic viewport)

Formula
1dvh = dynamic viewport height ÷ 100
Relative to
Currently available viewport, taking dynamic browser UI into account
Support
Modern browsersall engines since Nov 2022

The dynamic viewport tracks the current viewport state as browser UI expands or retracts.

100dvh = the currently available visible viewport height

When chrome is fully expanded, 100dvh equals 100svh; when fully retracted, it equals 100lvh; mid-transition it lies between the two.

The full dynamic family is dvw, dvh, dvi, dvb, dvmin, dvmax.

.dialog {
  height: 100dvh;
}

This is usually the best first choice for a true mobile full-screen modal. For app shells:

.app {
  min-height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;
}
Use when
  • Mobile modals, full-screen dialogs, drawers, bottom sheets
  • App shells: chat, maps, dashboards, editors
  • Camera / scanner UIs, layouts where bottom controls must remain visible
  • Anywhere fits the visible screen right now beats layout stability
Avoid when
  • As a default replacement for every vh
  • Long article pages — chrome animations cause layout shifts mid-scroll
  • Performance-sensitive layouts — dvh may cause layout work as chrome animates

dvh is tempting because it sounds like the correct modern answer, but dynamic sizing can make layouts move while the user scrolls — scroll position shifts, animation jank, expensive reflow, content that feels unstable.

Which S/L/D unit should you never use?

There is no unit that should literally never be used. But there is one dangerous habit:

Do not replace every vh with dvh automatically. Use it for surfaces that need the current visible height. Do not use it blindly for long scrolling pages.

The cleaner mental model:

svh → safety
lvh → visual fullness
dvh → current fit

Safe-area insets — a separate problem

The small / large / dynamic families solve one thing: retractable browser chrome. They do not account for the non-rectangular parts of a display — the notch, rounded corners, and the home indicator. 100dvh fills the visible viewport, but its bottom edge can still sit under the home indicator, so controls pinned to the bottom of a 100dvh shell may be partly covered.

That’s what the env() safe-area insets are for:

.app         { min-block-size: 100dvh; }
.app__footer {
  /* keep the action bar clear of the home indicator */
  padding-block-end: calc(0.75rem + env(safe-area-inset-bottom));
}

env(safe-area-inset-top | right | bottom | left) resolve to 0 by default — they become positive only once the page opts into the full display area with the viewport meta tag:

<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

Together the four insets describe a rectangle of always-safe space. Pair them with viewport units rather than replacing them: dvh decides how tall the surface is, env() decides how much edge to keep clear.


Decision tree

Full-screen mobile modal

.modal {
  height: 100dvh;
}

A modal is an active surface — buttons, form fields, and close controls must remain visible in the current viewport. If stability matters more than using all space, use min-height: 100svh instead. Avoid height: 100vh here, because 100vh can behave like 100lvh and hide bottom content behind browser chrome.

Landing-page hero

.hero {
  min-height: 100svh;
}

Use 100svh when the first viewport contains important text and buttons that must be visible immediately. Use 100lvh if the hero is immersive and slight initial cropping is acceptable. Use 100dvh only if the hero genuinely needs to track browser chrome changes.

A practical pattern combines both:

.hero {
  min-height: 100svh;
}

@supports (height: 100dvh) {
  .hero.is-interactive {
    min-height: 100dvh;
  }
}

Full-bleed background image / video

.visual {
  min-height: 100lvh;
}

Background media can usually tolerate being partially covered or cropped. The large viewport gives the intended immersive size. If important content sits at the bottom, combine with safe padding or move content into a safer layout region.

App shell

.app {
  min-height: 100dvh;
}

App shells need to fit the current visible viewport. This is especially true for chat, maps, dashboards, and editors.

Bottom sheet

.sheet      { max-height: 100dvh; }
.sheet__body { overflow: auto; }

A bottom sheet must stay reachable when browser chrome is visible. Add internal scrolling so content can grow without the sheet escaping the viewport.

Square visual that should fit on screen

.demo {
  width: 90vmin;
  height: 90vmin;
}

Use vmin for simple stable demos. Use dvmin only if changing browser chrome should affect the size.

Huge visual that must cover the viewport

.cover {
  width: 120vmax;
  height: 120vmax;
}

Or, for explicit large-viewport behavior, use 120lvmax.

Multilingual reading layout

.reader {
  max-inline-size: 80vi;
  min-block-size: 100vb;
}

Logical viewport units follow writing mode. This matters for vertical CJK layouts and systems that already use logical CSS properties.

Ordinary full-width section

.section {
  width: 100%;
}

Prefer 100% here — it matches the container’s content width. Reserve 100vw for deliberate edge-to-edge bleed, and note that on systems with classic scrollbars 100vw includes the scrollbar gutter and can trigger horizontal overflow.


Compact reference

Axis Default Small Large Dynamic 1 unit =
Width vw svw lvw dvw 1% of that viewport’s width
Height vh svh lvh dvh 1% of that viewport’s height
Inline axis vi svi lvi dvi 1% along the inline axis
Block axis vb svb lvb dvb 1% along the block axis
Smaller side vmin svmin lvmin dvmin 1% of the smaller dimension
Larger side vmax svmax lvmax dvmax 1% of the larger dimension

Small = sized as if chrome were expanded · Large = as if retracted · Dynamic = current state · Default is defined as Large.

Viewport-family behavior checked against MDN and CSS Values 4 ·

Sources