In this post, I'll give a short demonstration of how small viewport units in CSS can be used to build a grid that keeps a certain number of elements above the fold on all devices.
Let's begin with a quick primer on CSS viewport units. You may already be familiar with the 'vh' and 'vw' units, which have been around for a while and have broad browser support. These units specify dimensions as a percentage of the viewport height ('vh') or width ('vw'). These are distinct from percentage ('%') units, which specify an element's dimensions as a percentage of one of its ancestor's dimensions. For example, '100vh' is 100% of the viewport height while '5vw' is 5% of the viewport width. There are additional units like 'vmin' and 'vmax', which express dimensions as a percentage of the minimum and maximum viewport size, respectively. For example, '50vmax' will be equal to '50vh' in portrait orientation or '50vw' in landscape orientation.
The trouble with 'wh' and 'vw' is that the full dimensions of the viewport aren't always available to your application, especially on mobile devices. Browser controls like the location bar will occupy some of that space at least some of the time, which means that an element whose height is '100vh' will overflow the available space and some of its content will be hidden when the browser controls are visible. There is a good explanation of this problem at web.dev.
If you're trying to keep certain content above the fold, you can’t reliably use '100vh' as an indicator of how much space will be available before the user has to scroll. You also can’t reliably predict the amount of space that will be occupied by browser controls because it varies by operating system and browser. (Otherwise you could probably get away with a rule like 'height: calc(100vh - <location bar height in px>)', but please don't do that. It's an invitation to have your site broken by a future OS or browser update.)
This problem can be solved by taking advantage of three new types of units:
These units are relatively new to CSS but have, at the time of writing, been implemented in Chrome, Edge, Firefox and Safari. You can check the current support for this feature on caniuse. (This link is for dynamic viewport units, but browser support should be the same for the small and large viewport units.) If you need to support older browsers that haven't implemented these units, consider using fallback rules. This strategy will allow you to provide the best experience for newer browsers at the cost of delivering a less optimal (but still good) experience on older browsers.
The HTML for this demo is about as simple as it gets — an unordered list that contains twelve items, each of which contains a number.
The stylesheet does all the work of rendering the list as a grid and setting the size of the items so that a certain number of rows will be visible without scrolling. Before you continue, it will help to be familiar with custom properties, the 'calc' function and CSS grid.
We start by defining several custom properties. These aren't absolutely necessary, but they will help keep the CSS rules clean and readable.
The grid properties are defined separately for portrait and landscape orientation:
Next, we use the custom properties in our CSS rules:
The 'ul.svh-grid' rule unsets the default list styles and sets 'display: grid' to enable CSS grid layout. 'grid-template-columns' is set to render the list items in equal-width columns, with the number of columns defined by '--column-count-portrait'. 'grid-auto-rows' is set to render as many rows as necessary, with the row height defined by '--row-height-portrait'. The grid’s gap and padding are set to the value of the '--gap-size' property. The margin is set to zero to remove any default margins that the browser might apply to the 'ul' element.
The 'ul.svh-grid > li' rule applies to the individual items in the grid. These styles affect the appearance of the list items and have no impact on the grid layout. Notice that the font size for the numbers is set using the 'vmax' unit. This makes the font size 10% of the maximum viewport dimension, which is the same in both portrait and landscape orientation. This is a simple way to set the font size proportionally to the viewport without having it shift on orientation change.
Finally, we use a media query to override the grid styles in landscape orientation:
In portrait orientation, these styles give us a two column layout where three rows appear above the fold.
In landscape orientation, these styles give us a three column layout where two rows appear above the fold:
You might wonder why we didn't use the dynamic viewport height ('dvh') instead of the small viewport, which would have allowed us to optimize the size of the grid items to the height of the viewport regardless of whether or not the browser chrome is visible. As soon as the user scrolls down the page, the browser chrome transitions offscreen and the dynamic viewport compensates by switching from the small viewport to the large viewport. If the layout is based on the dynamic viewport, the grid items will increase in height when that transition occurs. In addition to that problem, the CSS specification doesn’t require that the viewport transition animates at a full 60fps, likely because that would be computationally expensive in many scenarios. Some browsers perform the transition quite abruptly which causes a visible shift in the layout. In this case, using the small viewport height produces a more natural feeling result when scrolling. The behaviour will vary depending on the OS, browser and individual use cases, though, so experiment with the different viewports to find what works best for you. (If you want to see how the dynamic viewport works in this scenario, simply change the '--page-height' property to '1dvh'.)