
Creating Custom Viewport Units Instead of Using vh and vw

I've written about using CSS viewport units (specifically vw and vh) in the past. However, there still remain two big weaknesses when it comes to using these (and vh in particular, as we'll get to in a minute):
- Even all these years later, cross‑browser compatibility remains a concern. Admittedly support gets better with every passing day, but when you have the likes of Microsoft Edge, which didn't fully support them until the end of 2017, you have a potential problem.
- A quirk in the way these values are calculated. Both
vhandvware calculated to the 'largest possible viewport', which includes things like address bars and virtual keyboards (vertically) and scrollbars (horizontally), meaning your '100%' element will sit beneath these things.
I use a solution (which I can't take much personal credit for) that resolves both of these issues with a little JavaScript.
100vh !== 100% of visible height
This is something you will no doubt come across every time you develop something that takes up the full‑screen. As soon as you load it up on an iPhone (or Android, or any physical mobile device), you'll find that your element is actually overflowing the viewport and falling behind things like the URL bar.
I touched on this briefly in the introduction above, but essentially, how viewport units are calculated can feel counter‑intuitive and not as useful as they might first seem. 1vh is not the same as 1% of the visible screen height (nor is 1vw the same as 1% of the visible screen width).
This is because these devices calculate 100vh as a percentage of the 'largest possible viewport' rather than what's actually visible at that time. It means that things like the URL bar that appears at the bottom of Safari and Chrome, overlap your 100vh height element, and your 100vw width content will fall behind browser elements like the scrollbars.
These values do not change when the browser elements (like the URL bar, virtual keyboard, or scrollbars) are shown and hidden.
Here's an example from a project I've been working on:

This is the new fly‑out navigation for Virgin Atlantic Holidays, where the main panel is expected to be the full height of the screen, with a set of image‑based location panels positioned off the bottom edge.
On the left, that navigation panel is set to height: 100vh and ‑ as you can see ‑ those bottom image panels are sitting down behind the URL bar. On the right is how it is intended to look and how it does look with a little additional JavaScript help.
The Solution: A Custom CSS Property and a Little JavaScript
The solution to this is relatively simple and robust. Instead of using viewport units, we create a dynamic custom CSS property that represents 1% of the visible width and/or height of the current viewport, and combine that with calc when used in our stylesheets. This way, we get real‑time layout adjustments as the CSS properties change.
Handily, this also provides consistent behaviour across browsers, removing some of the cross‑browser compatibility issues I discussed earlier on.
Implementation: JavaScript
Every time I've implemented this, it's been in React, but there's no reason vanilla JavaScript couldn't be used to the same effect:
Create a Resize Handler
: the crux of this implementation is responding to window resize events. Fortunately, both iOS Safari and Android Chrome trigger aresizeevent onwindowwhen browser elements change. I wrap this in auseCallbackhook to make sure that the function isn't recreated unnecessarily.Debounce it
: In the interest of improving performance further, I also debounce the handler and avoid excessive recalculations during rapid viewport changes (like resizing the browser window). This is optional because the downside of debouncing like this is that there will be a delay between the resize event itself and the dimensions updating.Create the Custom Property/Properties
: The resize handler function measures the current viewport, divides it by100(to get1%), and then assigns it as a custom CSS property to thehtmlelement.Use it in the CSS
Here's what my code looks like:
const handleResize = useCallback( debounce(() => { if (document && window) { const documentEl = document.documentElement; const { innerHeight, innerWidth } = window; documentEl.style.setProperty('--calculated-vh', `${innerHeight / 100}px`); documentEl.style.setProperty('--calculated-vw', `${innerWidth / 100}px`); } }, 250), []);I've included both vw and vh calculations here, but it's fair to say that for the most part, most of my issues relate to heights rather than widths, so you may not find it necessary to include both.
It's also worth mentioning that unlike assigning multiple 'normal' CSS style properties to an element, which you can do with Object.assign, you have to assign custom CSS variables one at a time because they aren't part of the CSSOM and need individual getters and setters.
In order to trigger this listener, you use a 'run once effect' (with an empty dependency array: []) to firstly trigger the function on‑mount so that the CSS variables can be set, and then start and stop the listener during the wider application lifecycle:
useEffect(() => { if (document) { handleResize(); window.addEventListener('resize', handleResize); return () => { window.removeEventListener('resize', handleResize); }; }}, []);In listening to the window.resize event, we can also take advantage of the 'phantom' resize events that iOS triggers each time an item of browser chrome is shown or hidden.
Using this in your CSS
Using these custom properties in your CSS is as simple as creating a calc to multiply the custom value by the number of them you want to use. For example:
// the calculated equivalent of 50vh:calc(var(--calculated-vh) * 50); // the calculated equivalent of 100vh:calc(var(--calculated-vh) * 100);// the calculated equivalent of 10vw:calc(var(--calculated-vw) * 10); So, if you wanted an element that was 100% the width of the viewport and 25% of the height, it would look something like this:
.element{ width: calc(var(--calculated-vw) * 50); height: calc(var(--calculated-vh) * 25)}Using Viewport Units as Fallbacks
As part of making sure this is as robust as possible, it is important that we consider what happens if the visitor either:
- Has a browser that doesn't support calc;
- Doesn't have the custom properties in their DOM.
There are any number of reasons why either might occur: a third‑party script might scrub the html style object for example, or your resize handler might simply not mount.
Either way, what you don't want is to have a situation where all your calculated dimensions suddenly collapse to zero just because the custom properties don't exist.
It may seem counterintuitive considering everything we've discussed until now, but the answer is simple: use vw and vh as fallback, like this:
.element { width: 50vw; // if this following declaration fails, then the browser will fall back to the one above width: calc(var(--calculated-vw) * 50); height: 25vh; // the same is true here too height: calc(var(--calculated-vh) * 25);}This just follows basic CSS inheritance principles where we define the same property twice (e.g., height). The browser will apply the last one first because it appears further down the CSS file, and then revert back to the previous declaration for the same property if need be.
This also has the added benefit of capturing any rogue visitors who have JavaScript disabled ‑ although, from my more recent experience, the only time anybody visits one of my projects without JavaScript is when I or my QA team are testing to make sure it works without JavaScript...!
Benefits and Drawbacks
This has become a bit of a meandering article now, and many of these points have already been touched‑upon above (some of them more than once), but nevertheless it's worth quickly rounding up the benefits and disadvantages of using this approach:
Benefits
Dynamic Adjustment:
with this approach, our layout will dynamically react to changes in the viewport, in a way that we're simply unable to achieve with regular viewport units. You can also tie the calculation into other emitted events should you need to trigger recalculation outside of the regularwindowresizeevent.Cross‑Browser Consistency:
here, we get a unified solution across different browsers and devices rather than the slightly muddled differences in different browser implementations.Integration with JavaScript:
We can use this for further JavaScript‑based logic in our application since we are already listening for theresizeevent.
Drawbacks
Performance:
As I've mentioned above, I used debounce to avoid running those big full‑document recalculations on every resize event. This helps to reduce performance issues that might otherwise be introduced by the feature, but does also mean that our custom properties don't update instantaneously. What you get is a payoff between potential performance impacts from the calculations, and not having a snappy response to user resize events.JavaScript Dependency:
Again, I mentioned this just a couple of paragraphs above, but you need to be aware of potential issues that might occur if your JavaScript doesn't work for any reason. Use fallbacks in your CSS, otherwise your layouts may collapse altogether.
Wrapping up
It is annoying that vw and vh don't actually behave as you might hope; that they calculate off the maximum viewport, rather than what is being displayed to our users at that time. However, this approach offers a robust solution which has the added benefit of also smoothing‑out some of the cross‑browser inconsistencies you would come across in the vast mobile device landscape.
Related Articles

Introducing Seeded Randomisation into an SSR Gatsby Project. 
Exploring CSS Viewport Units Beyond vw and vh. Exploring CSS Viewport Units Beyond
vwandvh
Using Viewport Units in CSS: vw and vh. Using Viewport Units in CSS:
vwandvh
Removing Duplicates from a JavaScript Array ('Deduping'). Removing Duplicates from a JavaScript Array ('Deduping')
Where to Find Jobs in Web Development. Where to Find Jobs in Web Development

Dynamically Create a Script Element with JavaScript. Dynamically Create a Script Element with JavaScript

CSS Animations: Transitions vs. Keyframes. CSS Animations: Transitions vs. Keyframes

How to Handle Multiple Named Exports in One JavaScript File. How to Handle Multiple Named Exports in One JavaScript File
ReferenceError: Window is Not Defined in Gatsby. ReferenceError: Window is Not Defined in Gatsby

React vs. Vue vs. Angular. React vs. Vue vs. Angular

LeetCode Container with Most Water: The Two‑Pointer Solution. LeetCode Container with Most Water: The Two‑Pointer Solution
Disabling Source Maps in Gatsby for Production. Disabling Source Maps in Gatsby for Production