[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"verticals":3,"quiz-css-dynamic-viewport-units-dvh-svh-lvh":44,"search-suggestions":60,"quiz-article-css-dynamic-viewport-units-dvh-svh-lvh":106},[4,20,32],{"id":5,"slug":6,"name":7,"tagline":8,"description":9,"accentFrom":10,"accentTo":11,"icon":12,"defaultLocale":13,"locales":14,"features":16,"position":19},"019fe637-3d33-714b-b57f-23e163ffca0c","dev","Web Development","Read it. Run it. Prove it.","A post a day on modern web development — most with an editable playground and a quiz that explains every answer. Free, no account needed.","violet-500","cyan-400","◇","en",[13,15],"fa",{"courses":17,"paths":17,"articles":17,"exams":18,"flashcards":18,"packages":17,"community":17,"certificates":17,"teams":17,"commerce":17},true,false,0,{"id":21,"slug":22,"name":23,"tagline":24,"description":25,"accentFrom":26,"accentTo":10,"icon":27,"defaultLocale":13,"locales":28,"features":30,"position":31},"019fe637-3dc2-754c-8657-0f175bfee7c6","lang","Languages","Learn a language the way you learn a codebase.","Grammar explained the way good documentation explains an API — one idea at a time, each with a quiz.","amber-400","⌘",[13,15,29],"es",{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":17,"packages":18,"community":17,"certificates":17,"teams":18,"commerce":18},2,{"id":33,"slug":34,"name":35,"tagline":36,"description":37,"accentFrom":38,"accentTo":39,"icon":40,"defaultLocale":13,"locales":41,"features":42,"position":43},"7b3c16f2-931d-410e-802e-e1fa4edab7de","soft","Soft Skills","The half of the job nobody wrote documentation for.","Weekly, on the parts of working life that decide more than your code does — first weeks, meetings, interviews, promotions, and the people around you. Written from what actually happens, and recorded as a podcast you can listen to on the walk.","emerald-400","teal-300","◉",[13],{"courses":18,"paths":18,"articles":17,"exams":18,"flashcards":18,"packages":18,"community":17,"certificates":18,"teams":18,"commerce":18},3,{"id":45,"slug":46,"kind":47,"title":48,"description":49,"config":50,"verticalId":5,"vertical":55,"course":52,"_count":56,"access":57,"attempts":59,"questionCount":51},"01a0af18-2529-7212-bebc-55f06913a310","css-dynamic-viewport-units-dvh-svh-lvh","PRACTICE_QUIZ","Dynamic viewport units: dvh, svh, lvh","Check what actually stuck about the 100vh mobile bug and the units that replace the JavaScript fix.",{"questionCount":51,"timeLimitSec":52,"shuffleQuestions":18,"shuffleOptions":17,"negativeMarking":19,"passScorePct":53,"maxAttempts":52,"revealAnswers":54,"allowFlagging":18,"allowBacktracking":17},7,null,70,"AFTER_SUBMIT",{"slug":6,"name":7},{"questions":51},{"allowed":17,"reason":58},"FREE",[],[61,65,69,73,77,81,85,89,93,97,100,103],{"slug":62,"name":63,"articles":64},"webdev","Webdev",107,{"slug":66,"name":67,"articles":68},"javascript","Javascript",89,{"slug":70,"name":71,"articles":72},"frontend","Frontend",72,{"slug":74,"name":75,"articles":76},"css","Css",36,{"slug":78,"name":79,"articles":80},"tutorial","Tutorial",35,{"slug":82,"name":83,"articles":84},"typescript","Typescript",15,{"slug":86,"name":87,"articles":88},"react","React",13,{"slug":90,"name":91,"articles":92},"performance","Performance",12,{"slug":94,"name":95,"articles":96},"browser","Browser",9,{"slug":98,"name":99,"articles":96},"node","Node",{"slug":101,"name":102,"articles":51},"html","Html",{"slug":104,"name":105,"articles":51},"accessibility","Accessibility",{"id":107,"slug":46,"title":108,"subtitle":52,"excerpt":109,"coverUrl":110,"locale":13,"readingMinutes":111,"publishedAt":112,"viewCount":113,"likeCount":19,"commentCount":19,"author":114,"vertical":119,"topic":120,"tags":122,"_count":127,"playground":129,"body":131,"bodyMd":286,"seo":287,"translationGroupId":290,"series":52,"podcastUrl":52,"verticalId":5,"thread":291,"assessments":293,"translations":296,"quiz":298},"01a0af18-24b4-73ad-95ed-c9479ae0e91f","Stop Writing JavaScript To Fix `100vh` On Mobile","Your full-screen hero looks perfect in the simulator and cuts off the CTA on a real phone. It's not a bug in your code — it's `vh` answering a question you didn't ask. Three new units fix it with zero JavaScript.","\u002Fmedia\u002Fcovers\u002Fcss-dynamic-viewport-units-dvh-svh-lvh.png",6,"2026-09-22T19:35:37.351Z",40,{"id":115,"name":116,"username":117,"avatarUrl":52,"headline":118},"019fe637-3c25-7088-9034-39c9f15dc3c8","Parsa Jiravand","parsa","Frontend engineer · building bestpractic",{"slug":6,"name":7,"accentFrom":10,"accentTo":11},{"slug":74,"name":121},"CSS",[123,124,125,126],{"slug":74,"name":75,"color":52},{"slug":62,"name":63,"color":52},{"slug":70,"name":71,"color":52},{"slug":78,"name":79,"color":52},{"assessments":128},1,{"slug":46,"title":130},"dvh vs svh vs vh — interactive playground",{"blocks":132,"version":128},[133,137,140,143,147,150,153,159,163,166,169,173,176,179,182,185,188,191,198,201,205,208,211,214,217,220,223,226,229,232,235,238,244,247,250,253,256,259,262,265,268,271,274,277,280],{"id":134,"html":135,"type":136},"b1","\u003Cp>Your hero section is \u003Ccode>height: 100vh\u003C\u002Fcode>. Big background image, headline centered, a CTA button pinned near the bottom. You check it in the browser, check it in the iOS simulator, ship it.\u003C\u002Fp>","paragraph",{"id":138,"html":139,"type":136},"b2","\u003Cp>Then the support tickets come in: &quot;the sign-up button is cut off.&quot; You open it on your own phone to confirm — and it looks fine. Scroll down an inch, though, and there&#39;s a sliver of button peeking out from under where the fold \u003Cem>should\u003C\u002Fem> be.\u003C\u002Fp>",{"id":141,"html":142,"type":136},"b3","\u003Cp>\u003Cstrong>Guess what&#39;s different between your simulator and a real phone in someone&#39;s hand — before you scroll.\u003C\u002Fstrong>\u003C\u002Fp>",{"id":144,"html":145,"text":145,"type":146,"level":31},"b4","The fix everyone reaches for first","heading",{"id":148,"html":149,"type":136},"b5","\u003Cp>The instinct is to blame the button, not the unit. Add padding, shrink the image, tweak breakpoints. None of it holds, because the box itself is the wrong size — \u003Ccode>100vh\u003C\u002Fcode> is taller than what the user can actually see, and no amount of internal spacing fixes a container that&#39;s bigger than its own screen.\u003C\u002Fp>",{"id":151,"html":152,"type":136},"b6","\u003Cp>So the real fix people land on, for years now, has been JavaScript:\u003C\u002Fp>",{"id":154,"code":155,"type":156,"language":157,"highlight":158},"b7","function setViewportHeight() {\n  document.documentElement.style.setProperty(\n    \"--vh\",\n    `${window.innerHeight * 0.01}px`\n  );\n}\n\nsetViewportHeight();\nwindow.addEventListener(\"resize\", setViewportHeight);","code","js",[],{"id":160,"code":161,"type":156,"language":74,"highlight":162},"b8",".hero {\n  height: calc(var(--vh) * 100);\n}",[],{"id":164,"html":165,"type":136},"b9","\u003Cp>This works. It&#39;s also been copy-pasted into thousands of codebases as a shrug-emoji workaround — a \u003Ccode>resize\u003C\u002Fcode> listener recalculating a custom property, just so CSS can know something the browser already knows. It renders with the \u003Cem>wrong\u003C\u002Fem> height for one frame before the script runs. It has to be re-wired for every new full-screen element. And on some Android keyboards, &quot;resize&quot; fires in ways that make the hero jump while someone&#39;s mid-scroll.\u003C\u002Fp>",{"id":167,"html":168,"type":136},"b10","\u003Cp>You&#39;re not wrong to reach for it. \u003Ccode>vh\u003C\u002Fcode> really doesn&#39;t tell you what you think it tells you.\u003C\u002Fp>",{"id":170,"html":171,"text":172,"type":146,"level":31},"b11","What \u003Ccode>100vh\u003C\u002Fcode> actually measures","What 100vh actually measures",{"id":174,"html":175,"type":136},"b12","\u003Cp>Here&#39;s the part that makes the bug feel random instead of consistent: \u003Ccode>100vh\u003C\u002Fcode> isn&#39;t measuring &quot;the screen right now.&quot; It&#39;s measuring the browser&#39;s \u003Cstrong>largest possible viewport\u003C\u002Fstrong> — the height you&#39;d get if the address bar and toolbar were fully collapsed.\u003C\u002Fp>",{"id":177,"html":178,"type":136},"b13","\u003Cp>On iOS Safari, the address bar collapses when you scroll down and reappears when you scroll up or land on a fresh page. \u003Ccode>100vh\u003C\u002Fcode> was sized for the collapsed state the whole time — so the moment that bar is visible (which, on a page load, it always is), your &quot;full screen&quot; hero is taller than the actual visible area by exactly the height of that bar. The bottom of your hero, CTA included, sits behind it.\u003C\u002Fp>",{"id":180,"html":181,"type":136},"b14","\u003Cp>And for years, Chrome on Android handled the same unit differently — sizing \u003Ccode>vh\u003C\u002Fcode> around the \u003Cem>smallest\u003C\u002Fem> viewport instead, bar included. Two major browsers, one CSS unit, two different answers to &quot;how tall is the screen.&quot; That&#39;s why this bug never had a clean repro: it depended on the browser, the scroll position, and whether the user had just loaded the page or scrolled once already.\u003C\u002Fp>",{"id":183,"html":184,"type":136},"b15","\u003Cp>\u003Ccode>vh\u003C\u002Fcode> isn&#39;t lying. It&#39;s answering a question — &quot;how tall is this viewport at its largest?&quot; — that nobody actually asked.\u003C\u002Fp>",{"id":186,"html":187,"text":187,"type":146,"level":31},"b16","The units that were built to answer the real question",{"id":189,"html":190,"type":136},"b17","\u003Cp>Since 2022, every major browser engine ships three more precise viewport units, and they turn that whole JavaScript workaround into one CSS declaration:\u003C\u002Fp>",{"id":192,"type":193,"items":194,"ordered":18},"b18","list",[195,196,197],"\u003Cstrong>\u003Ccode>svh\u003C\u002Fcode>\u003C\u002Fstrong> — small viewport height. The height when browser UI (address bar, toolbar) is fully \u003Cem>expanded\u003C\u002Fem>. This is the safe, stable floor: content sized to \u003Ccode>svh\u003C\u002Fcode> always fits, bars showing or not.","\u003Cstrong>\u003Ccode>lvh\u003C\u002Fcode>\u003C\u002Fstrong> — large viewport height. The height when browser UI is fully \u003Cem>collapsed\u003C\u002Fem>. This is what \u003Ccode>vh\u003C\u002Fcode> was already measuring on iOS Safari.","\u003Cstrong>\u003Ccode>dvh\u003C\u002Fcode>\u003C\u002Fstrong> — dynamic viewport height. Tracks the \u003Cem>real, current\u003C\u002Fem> visible height, live, recalculating as the address bar shows and hides.",{"id":199,"html":200,"type":136},"b19","\u003Cp>Swap the unit, delete the JavaScript:\u003C\u002Fp>",{"id":202,"code":203,"type":156,"language":74,"highlight":204},"b20",".hero {\n  height: 100vh; \u002F* fallback for browsers that don't parse dvh *\u002F\n  height: 100dvh;\n}",[],{"id":206,"html":207,"type":136},"b21","\u003Cp>Browsers that don&#39;t recognize \u003Ccode>dvh\u003C\u002Fcode> treat that whole declaration as invalid and ignore it — so the \u003Ccode>100vh\u003C\u002Fcode> line above it quietly stands as the fallback, no \u003Ccode>@supports\u003C\u002Fcode> block required. Everywhere \u003Ccode>dvh\u003C\u002Fcode> \u003Cem>is\u003C\u002Fem> understood, the second line wins and the hero tracks the real viewport, live, as the bar animates in and out. No \u003Ccode>resize\u003C\u002Fcode> listener, no custom property, no one-frame flash of the wrong size.\u003C\u002Fp>",{"id":209,"html":210,"type":136},"b22","\u003Cp>The same trio exists for width (\u003Ccode>svw\u003C\u002Fcode>\u002F\u003Ccode>lvw\u003C\u002Fcode>\u002F\u003Ccode>dvw\u003C\u002Fcode>) and for logical dimensions (\u003Ccode>svi\u003C\u002Fcode>\u002F\u003Ccode>lvi\u003C\u002Fcode>\u002F\u003Ccode>dvi\u003C\u002Fcode>, \u003Ccode>svb\u003C\u002Fcode>\u002F\u003Ccode>lvb\u003C\u002Fcode>\u002F\u003Ccode>dvb\u003C\u002Fcode>, for writing-mode-aware layouts) — same three questions, different axis.\u003C\u002Fp>",{"id":212,"html":213,"type":136},"b23","\u003Cp>\u003Cstrong>Try it before you keep reading:\u003C\u002Fstrong> the playground below simulates a phone&#39;s address bar collapsing as you scroll, with three stacked panels — one sized in \u003Ccode>vh\u003C\u002Fcode>, one in \u003Ccode>svh\u003C\u002Fcode>, one in \u003Ccode>dvh\u003C\u002Fcode> — so you can watch all three respond (or fail to respond) to the exact same scroll.\u003C\u002Fp>",{"id":215,"html":216,"type":136},"b24","\u003C!-- playground:start -->",{"id":218,"html":219,"text":219,"type":146,"level":31},"b25","🎮 Try it yourself",{"id":221,"html":222,"type":136},"b26","\u003Cp>\u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-dynamic-viewport-units-dvh-svh-lvh\u002Fplayground\">▶️ Open the interactive playground →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":224,"html":225,"type":136},"b27","\u003Cp>\u003Cem>Runs right in your browser — poke at it and watch the concept react live.\u003C\u002Fem>\u003C\u002Fp>",{"id":227,"html":228,"type":136},"b28","\u003C!-- playground:end -->",{"id":230,"html":231,"text":231,"type":146,"level":31},"b29","Pick the right one, not just the new one",{"id":233,"html":234,"type":136},"b30","\u003Cp>Reaching for \u003Ccode>dvh\u003C\u002Fcode> everywhere trades one bug for a subtler one. Because \u003Ccode>dvh\u003C\u002Fcode> recalculates \u003Cem>while the address bar is mid-animation\u003C\u002Fem>, anything sized with it visibly resizes as the user scrolls — a hero that gently grows and shrinks by fifty-odd pixels as the bar slides. For a full-bleed section that&#39;s supposed to feel locked to the screen, that&#39;s the right behavior. For, say, a sticky modal or a fixed bottom sheet, that same live resize reads as jitter.\u003C\u002Fp>",{"id":236,"html":237,"type":136},"b31","\u003Cp>A rough rule that holds up in practice:\u003C\u002Fp>",{"id":239,"type":193,"items":240,"ordered":18},"b32",[241,242,243],"\u003Cstrong>\u003Ccode>dvh\u003C\u002Fcode>\u003C\u002Fstrong> for full-bleed sections you want to genuinely hug the visible viewport — landing page heroes, full-screen intro panels.","\u003Cstrong>\u003Ccode>svh\u003C\u002Fcode>\u003C\u002Fstrong> for anything that must never get cut off, even worst-case — a modal&#39;s max-height, a &quot;fits on one screen&quot; onboarding step. It&#39;s the pessimistic, always-safe number.","\u003Cstrong>\u003Ccode>lvh\u003C\u002Fcode>\u003C\u002Fstrong> rarely, when you specifically want the old \u003Ccode>100vh\u003C\u002Fcode> behavior on purpose (matching a background that&#39;s meant to bleed past the visible edge).",{"id":245,"html":246,"type":136},"b33","\u003Cp>Support is solid enough to reach for today without a polyfill: Safari since 15.4 (March 2022), Chrome and Edge since 108 (December 2022), Firefox since 101 (May 2022). The \u003Ccode>height: 100vh; height: 100dvh;\u003C\u002Fcode> stacking pattern above is your fallback for anything older — no feature query needed, because the invalid-property-drop behavior does the work for you.\u003C\u002Fp>",{"id":248,"html":249,"text":249,"type":146,"level":31},"b34","The takeaway",{"id":251,"html":252,"type":136},"b35","\u003Cp>\u003Ccode>100vh\u003C\u002Fcode> was never broken — it was always precisely, consistently answering &quot;how tall is this viewport with the browser chrome fully out of the way,&quot; which is a real answer to a question almost nobody meant to ask. \u003Ccode>dvh\u003C\u002Fcode>, \u003Ccode>svh\u003C\u002Fcode>, and \u003Ccode>lvh\u003C\u002Fcode> let you ask the actual question directly, in CSS, and delete the \u003Ccode>resize\u003C\u002Fcode> listener that&#39;s been standing in for them.\u003C\u002Fp>",{"id":254,"html":255,"type":136},"b36","\u003Cp>If you&#39;ve got a \u003Ccode>--vh\u003C\u002Fcode> custom property hack sitting in a codebase right now — how old is it, and what would it take to rip out?\u003C\u002Fp>",{"id":257,"html":258,"type":136},"b37","\u003C!-- quiz:start -->",{"id":260,"html":261,"text":261,"type":146,"level":31},"b38","🧠 Test yourself",{"id":263,"html":264,"type":136},"b39","\u003Cp>Think it clicked? \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-dynamic-viewport-units-dvh-svh-lvh\u002Fquiz\">Take the 7-question quiz →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>",{"id":266,"html":267,"type":136},"b40","\u003Cp>\u003Cem>Instant feedback, a hint on every question, and an explanation for each answer — right or wrong.\u003C\u002Fem>\u003C\u002Fp>",{"id":269,"html":270,"type":136},"b41","\u003C!-- quiz:end -->",{"id":272,"type":273},"b42","divider",{"id":275,"html":276,"type":136},"b43","\u003Cp>🚀 \u003Cstrong>Want more like this?\u003C\u002Fstrong> Every guide, playground, and quiz lives on \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">bestpractic.org\u003C\u002Fa>\u003C\u002Fstrong> — open it and \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fbestpractic.org\u002F\">sign up free\u003C\u002Fa>\u003C\u002Fstrong> so the next one finds you.\u003C\u002Fp>",{"id":278,"html":279,"type":136},"b44","\u003Cp>\u003Cem>Thanks for reading! Let&#39;s stay connected:\u003C\u002Fem>\u003C\u002Fp>",{"id":281,"type":193,"items":282,"ordered":18},"b45",[283,284,285],"⭐ \u003Cstrong>GitHub\u003C\u002Fstrong> — follow me and star the projects: \u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fparsajiravand\">github.com\u002Fparsajiravand\u003C\u002Fa>","💬 \u003Cstrong>Discord\u003C\u002Fstrong> — join the frontend best-practices community: \u003Ca href=\"https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ\">discord.gg\u002Fd9KRhuAwQ\u003C\u002Fa>","📸 \u003Cstrong>Instagram\u003C\u002Fstrong> — frontend best practices, daily: \u003Ca href=\"https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F\">@bestpractice___\u003C\u002Fa>","Your hero section is `height: 100vh`. Big background image, headline centered, a CTA button pinned near the bottom. You check it in the browser, check it in the iOS simulator, ship it.\n\nThen the support tickets come in: \"the sign-up button is cut off.\" You open it on your own phone to confirm — and it looks fine. Scroll down an inch, though, and there's a sliver of button peeking out from under where the fold *should* be.\n\n**Guess what's different between your simulator and a real phone in someone's hand — before you scroll.**\n\n## The fix everyone reaches for first\n\nThe instinct is to blame the button, not the unit. Add padding, shrink the image, tweak breakpoints. None of it holds, because the box itself is the wrong size — `100vh` is taller than what the user can actually see, and no amount of internal spacing fixes a container that's bigger than its own screen.\n\nSo the real fix people land on, for years now, has been JavaScript:\n\n```js\nfunction setViewportHeight() {\n  document.documentElement.style.setProperty(\n    \"--vh\",\n    `${window.innerHeight * 0.01}px`\n  );\n}\n\nsetViewportHeight();\nwindow.addEventListener(\"resize\", setViewportHeight);\n```\n\n```css\n.hero {\n  height: calc(var(--vh) * 100);\n}\n```\n\nThis works. It's also been copy-pasted into thousands of codebases as a shrug-emoji workaround — a `resize` listener recalculating a custom property, just so CSS can know something the browser already knows. It renders with the *wrong* height for one frame before the script runs. It has to be re-wired for every new full-screen element. And on some Android keyboards, \"resize\" fires in ways that make the hero jump while someone's mid-scroll.\n\nYou're not wrong to reach for it. `vh` really doesn't tell you what you think it tells you.\n\n## What `100vh` actually measures\n\nHere's the part that makes the bug feel random instead of consistent: `100vh` isn't measuring \"the screen right now.\" It's measuring the browser's **largest possible viewport** — the height you'd get if the address bar and toolbar were fully collapsed.\n\nOn iOS Safari, the address bar collapses when you scroll down and reappears when you scroll up or land on a fresh page. `100vh` was sized for the collapsed state the whole time — so the moment that bar is visible (which, on a page load, it always is), your \"full screen\" hero is taller than the actual visible area by exactly the height of that bar. The bottom of your hero, CTA included, sits behind it.\n\nAnd for years, Chrome on Android handled the same unit differently — sizing `vh` around the *smallest* viewport instead, bar included. Two major browsers, one CSS unit, two different answers to \"how tall is the screen.\" That's why this bug never had a clean repro: it depended on the browser, the scroll position, and whether the user had just loaded the page or scrolled once already.\n\n`vh` isn't lying. It's answering a question — \"how tall is this viewport at its largest?\" — that nobody actually asked.\n\n## The units that were built to answer the real question\n\nSince 2022, every major browser engine ships three more precise viewport units, and they turn that whole JavaScript workaround into one CSS declaration:\n\n- **`svh`** — small viewport height. The height when browser UI (address bar, toolbar) is fully *expanded*. This is the safe, stable floor: content sized to `svh` always fits, bars showing or not.\n- **`lvh`** — large viewport height. The height when browser UI is fully *collapsed*. This is what `vh` was already measuring on iOS Safari.\n- **`dvh`** — dynamic viewport height. Tracks the *real, current* visible height, live, recalculating as the address bar shows and hides.\n\nSwap the unit, delete the JavaScript:\n\n```css\n.hero {\n  height: 100vh; \u002F* fallback for browsers that don't parse dvh *\u002F\n  height: 100dvh;\n}\n```\n\nBrowsers that don't recognize `dvh` treat that whole declaration as invalid and ignore it — so the `100vh` line above it quietly stands as the fallback, no `@supports` block required. Everywhere `dvh` *is* understood, the second line wins and the hero tracks the real viewport, live, as the bar animates in and out. No `resize` listener, no custom property, no one-frame flash of the wrong size.\n\nThe same trio exists for width (`svw`\u002F`lvw`\u002F`dvw`) and for logical dimensions (`svi`\u002F`lvi`\u002F`dvi`, `svb`\u002F`lvb`\u002F`dvb`, for writing-mode-aware layouts) — same three questions, different axis.\n\n**Try it before you keep reading:** the playground below simulates a phone's address bar collapsing as you scroll, with three stacked panels — one sized in `vh`, one in `svh`, one in `dvh` — so you can watch all three respond (or fail to respond) to the exact same scroll.\n\n\u003C!-- playground:start -->\n\n## 🎮 Try it yourself\n\n**[▶️ Open the interactive playground →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-dynamic-viewport-units-dvh-svh-lvh\u002Fplayground)**\n\n_Runs right in your browser — poke at it and watch the concept react live._\n\n\u003C!-- playground:end -->\n\n## Pick the right one, not just the new one\n\nReaching for `dvh` everywhere trades one bug for a subtler one. Because `dvh` recalculates *while the address bar is mid-animation*, anything sized with it visibly resizes as the user scrolls — a hero that gently grows and shrinks by fifty-odd pixels as the bar slides. For a full-bleed section that's supposed to feel locked to the screen, that's the right behavior. For, say, a sticky modal or a fixed bottom sheet, that same live resize reads as jitter.\n\nA rough rule that holds up in practice:\n\n- **`dvh`** for full-bleed sections you want to genuinely hug the visible viewport — landing page heroes, full-screen intro panels.\n- **`svh`** for anything that must never get cut off, even worst-case — a modal's max-height, a \"fits on one screen\" onboarding step. It's the pessimistic, always-safe number.\n- **`lvh`** rarely, when you specifically want the old `100vh` behavior on purpose (matching a background that's meant to bleed past the visible edge).\n\nSupport is solid enough to reach for today without a polyfill: Safari since 15.4 (March 2022), Chrome and Edge since 108 (December 2022), Firefox since 101 (May 2022). The `height: 100vh; height: 100dvh;` stacking pattern above is your fallback for anything older — no feature query needed, because the invalid-property-drop behavior does the work for you.\n\n## The takeaway\n\n`100vh` was never broken — it was always precisely, consistently answering \"how tall is this viewport with the browser chrome fully out of the way,\" which is a real answer to a question almost nobody meant to ask. `dvh`, `svh`, and `lvh` let you ask the actual question directly, in CSS, and delete the `resize` listener that's been standing in for them.\n\nIf you've got a `--vh` custom property hack sitting in a codebase right now — how old is it, and what would it take to rip out?\n\n\u003C!-- quiz:start -->\n\n## 🧠 Test yourself\n\nThink it clicked? **[Take the 7-question quiz →](https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-dynamic-viewport-units-dvh-svh-lvh\u002Fquiz)**\n\n_Instant feedback, a hint on every question, and an explanation for each answer — right or wrong._\n\n\u003C!-- quiz:end -->\n\n---\n\n🚀 **Want more like this?** Every guide, playground, and quiz lives on **[bestpractic.org](https:\u002F\u002Fbestpractic.org\u002F)** — open it and **[sign up free](https:\u002F\u002Fbestpractic.org\u002F)** so the next one finds you.\n\n*Thanks for reading! Let's stay connected:*\n\n- ⭐ **GitHub** — follow me and star the projects: [github.com\u002Fparsajiravand](https:\u002F\u002Fgithub.com\u002Fparsajiravand)\n- 💬 **Discord** — join the frontend best-practices community: [discord.gg\u002Fd9KRhuAwQ](https:\u002F\u002Fdiscord.gg\u002Fd9KRhuAwQ)\n- 📸 **Instagram** — frontend best practices, daily: [@bestpractice___](https:\u002F\u002Fwww.instagram.com\u002Fbestpractice___\u002F)",{"title":108,"canonical":288,"description":289},"https:\u002F\u002Fbestpractic.org\u002Fblog\u002Fcss-dynamic-viewport-units-dvh-svh-lvh","Your full-screen hero looks perfect in the simulator and cuts off the CTA on a real phone. It's not a bug in your code — it's `vh` answering a question you didn't ask. Three new un","01a0af18-24b4-73ad-95ed-cf3be4525fe6",{"id":292,"locked":18},"01a0af18-24f4-7289-9611-b58d73f469f3",[294],{"id":45,"slug":46,"title":48,"_count":295},{"questions":51},[297],{"locale":13,"slug":46},{"id":45,"slug":46,"title":48,"_count":299,"questionCount":51},{"questions":51}]