DEV Community

Tony Wallace for RedBit Development

Posted on • Originally published at redbitdev.com

Small Viewport Units in CSS

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.

Introduction to Viewport Units

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:

  • The small viewport (svh/svw/svmin/svmax): These units give you the height, width, minimum dimension and maximum dimension of the viewport when the browser chrome is visible (e.g. on page load or after scrolling to the top of the page).
  • The large viewport (lvh/lvw/lvmin/lvmax): These units give you the height, width, minimum dimension and maximum dimension of the viewport when the browser chrome is hidden (e.g. after scrolling down the page).
  • The dynamic viewport (dvh/dvw/dvmin/dvmax): These units give you the small or large viewport dynamically, depending on whether the browser chrome is currently visible.

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 Grid Layout

HTML

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.

index.html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Grid</title>
    <link rel="stylesheet" type="text/css" href="./styles.css" />
  </head>
  <body>
    <ul class="svh-grid">
      <li>1</li>
      <li>2</li>
      <li>3</li>
      <li>4</li>
      <li>5</li>
      <li>6</li>
      <li>7</li>
      <li>8</li>
      <li>9</li>
      <li>10</li>
      <li>11</li>
      <li>12</li>
    </ul>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

CSS

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.

styles.css
:root {
  --page-height: 100svh;
  --gap-size: 1svh;

  --column-count-portrait: 2;
  --row-count-portrait: 3;
  --gap-height-portrait: calc((var(--row-count-portrait) + 1) * var(--gap-size));
  --content-height-portrait: calc(var(--page-height) - var(--gap-height-portrait));
  --row-height-portrait: calc(var(--content-height-portrait) / var(--row-count-portrait));

  --column-count-landscape: 3;
  --row-count-landscape: 2;
  --gap-height-landscape: calc((var(--row-count-portrait) + 1) * var(--gap-size));
  --content-height-landscape: calc(var(--page-height) - var(--gap-height-landscape));
  --row-height-landscape: calc(var(--content-height-landscape) / var(--row-count-landscape));
}

html,
body {
  padding: 0;
  margin: 0;
  background-color: #FFF;
}

ul.svh-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(var(--column-count-portrait), 1fr);
  grid-auto-rows: var(--row-height-portrait);
  grid-gap: var(--gap-size);
  padding: var(--gap-size);
  margin: 0;
}

ul.svh-grid > li {
  display: flex;
  justify-content: center;
  align-items: center;
  color: #FFF;
  background-color: #333;
  font-family: sans-serif;
  font-size: 10vmax;
}

@media (orientation: landscape) {
  ul.svh-grid {
    grid-template-columns: repeat(var(--column-count-landscape), 1fr);
    grid-auto-rows: var(--row-height-landscape);
  }
}
Enter fullscreen mode Exit fullscreen mode

We start by defining several custom properties. These aren't absolutely necessary, but they will help keep the CSS rules clean and readable.

  • The --page-height property is set to 100% of the small viewport height. (To reiterate, the small viewport encompasses all the space available to your application when the browser chrome is visible.)
  • The --gap-size property represents the size of the gutters between grid items and the padding around the grid container. In this example, --gap-size is set to 1% of the small viewport height, but it could have any value and use any units. If you want gutters to be 20px or 1rem on all devices, you're free to do so.

The grid properties are defined separately for portrait and landscape orientation:

  • --column-count-portrait/--column-count-landscape: The number of columns we want in the grid.
  • --row-count-portrait/--row-count-landscape: The number of rows we want to render above the fold. The actual number of rows in the grid is determined by the html.
  • --gap-height-portrait/--gap-height-landscape: The sum of all the gaps between rows that we want to render above the fold. The number of gaps will be one more than the number of rows. If we want three rows above the fold, there will be four gaps including the top and bottom.
  • --content-height-portrait/--content-height-landscape: The total height available for content, calculated by subtracting the total gap height from the page height.
  • --row-height-portrait/--row-height-landscape: The row height, calculated by dividing the total content height by the number of rows we want to render above the fold.

Next, we use the custom properties in our CSS rules:

  • The html, body rule is a simple reset to make sure the browser doesn’t apply default padding or margins that would affect the layout. (You might already have a reset like this in your stylesheet, in which case you might not need this.)
html,
body {
  padding: 0;
  margin: 0;
  background-color: #FFF;
}
Enter fullscreen mode Exit fullscreen mode

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.

ul.svh-grid {
  list-style: none;
  display: grid;
  grid-template-columns: repeat(var(--column-count-portrait), 1fr);
  grid-auto-rows: var(--row-height-portrait);
  grid-gap: var(--gap-size);
  padding: var(--gap-size);
  margin: 0;
}
Enter fullscreen mode Exit fullscreen mode

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.

ul.svh-grid > li {
  display: flex;
  justify-content: center;
  align-items: center;
  color: #FFF;
  background-color: #333;
  font-family: sans-serif;
  font-size: 10vmax;
}
Enter fullscreen mode Exit fullscreen mode

Finally, we use a media query to override the grid styles in landscape orientation:

@media (orientation: landscape) {
  ul.svh-grid {
    grid-template-columns: repeat(var(--column-count-landscape), 1fr);
    grid-auto-rows: var(--row-height-landscape);
  }
}
Enter fullscreen mode Exit fullscreen mode

In portrait orientation, these styles give us a two column layout where three rows appear above the fold.

On page load or when scrolled to the top of the page:

Two column grid layout in portrait orientation, scrolled to the top

When scrolled to the bottom of the page:

Two column grid layout in portrait orientation, scrolled to the bottom

In landscape orientation, these styles give us a three column layout where two rows appear above the fold:

On page load or when scrolled to the top of the page:

Three column grid layout in landscape orientation, scrolled to the top

When scrolled to the bottom of the page:

Three column grid layout in landscape orientation, scrolled to the bottom

Why Not Use the Dynamic Viewport?

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.)

Top comments (0)