Prevent layout shifts with CSS grid stacks
Published: 2020-10-20 00:00:00

---
date: 20201027
---
<p>There are two reasons why people use CSS grid:</p>
<ol>
<li>😎 CSS is awesome! It's a fact, deal with it.</li>
<li>🛠️ Grid is a great tool to build complex two-dimensional layouts.</li>
</ol>
<p>I sometimes have a third reason to use CSS grid: prevent <strong>layout shifts</strong>.
I tried to find a fancy acronym for this technique but all I got was ALSGST: <em>&quot;Anti Layout Shift Grid Stacking Technique&quot;</em>.
I guess I won't be receiving many skill endorsements for &quot;coining tech terms&quot; on my LinkedIn profile so you better send me some suggestions.</p>
<p>Let me explain the technique using real examples.
In this article, I'll cover:</p>
<ul>
<li>The real layout shift problem I faced with a component I worked on.</li>
<li>The limitations of solving it with absolute positioning.</li>
<li>The benefits of solving it with grid.</li>
</ul>
<blockquote>
<p>Wait, what is a layout shift anyway?</p>
</blockquote>
<p>A layout shift is what happens when an update in a web page causes something to move significantly.
Most of the time, layout shifts occur during page load while the contents progressively renders.
For example, when an image with an unspecified height gets inserted in the page, just before the very paragraph you were reading, the text moves away and it gets pretty frustrating.
This is also very common when third party scripts (ad services or partnered contents) append new unknown elements in the page.</p>
<p>This article will focus on layout shifts occuring when toggling state on a UI component.
I won't cover layout shifts happening during page load.</p>
<h2>Let's get some context</h2>
<p>I work for <a href="https://www.clever-cloud.com/en/">Clever Cloud</a>, an IT automation platform.
Our clients push their code and we handle the rest for them: build, deploy, host, scale, maintain, recover and so on.
When they need to configure their applications, databases and other services, they use our web UI: &quot;the console&quot;.
I spend most of my time on this project and it looks like this:</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/console-overview.9f0ce33e.png" alt="Screenshot of Clever Cloud's Web console displaying the overview of a Node.js application"></p>
<p>What you see in the screenshot above is the overview of my own website, a Node.js application, hosted on our platform.
On the right, you can see a pie chart representing the repartition of HTTP response codes returned by the application in the last 24 hours.</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/cc-tile-status-codes-chart.f651c2e3.png" alt="UI Component displaying a pie chart"></p>
<p>When I worked on this component, I tried to keep things simple.
I imagined our users would implicitly understand the chart is related to the current application.
To unclutter the display, I decided to move some details like the exact number of requests in tooltips, accessible via mouse or touch.
With the chart all cleaned up, I faced a problem.
How would I explain the following details?</p>
<ul>
<li>The chart only displays data from the last 24 hours.</li>
<li>Each item in the legend can be clicked to show/hide the different status code categories. Thank you <a href="https://www.chartjs.org/">Chart.js</a>!</li>
</ul>
<p>The available space was too small to add a detailed title at the top or bottom of the chart.
So to help our users figure this out, I added an information button ℹ️ in the top right corner.
When this button is clicked, the chart is hidden and a short text is displayed instead.
It looks like this:</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/cc-tile-status-codes-help.aa1a1f06.png" alt="UI Component displaying a short text"></p>
<p>When the close button is clicked, the short text is hidden and the chart displayed again.
Implementing this toggle behaviour was not that hard.
Let's take a simplified version of the template:</p>

```html
<div class="chart-component">
  <div class="title">
    HTTP response codes <button>toggle chart/info</button>
  </div>
  <div class="chart">
    <!-- chart here -->
  </div>
  <div class="info">
    <!-- short text here -->
  </div>
</div>
```

<p>In this HTML, I can, when the button is clicked, simply toggle the component state between <code>.chart</code> and <code>.info</code> and hide the appropriate panel with <code>display: none</code>.</p>
<h2>Layout shift problems</h2>
<p>This approach has one big problem, it is subject to <strong>layout shifts</strong>.</p>
<blockquote>
<p>Why is that?</p>
</blockquote>
<p>When I work on such components, I tend to follow what I consider a good practice: don't set a fixed height on the component, let it adapt to its own content.
This is even more important in this situation because I have no way to know how much space is needed to display the short text (without scrollbars).
The minimum required height will depend on:</p>
<ul>
<li>The unknown width of the component, which depends on usage context</li>
<li>The length of the short text, which depends on the language (English or French)</li>
</ul>
<p>The result is, the component height is a bit smaller containing the chart than containing the short text.</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/heigh-change-en.63c17222.png" alt="Two states of the same UI Component displayed side by side. Pie chart on the left, short text on the right. English version."></p>
<p>The issue is worse in French because the text is a bit longer:</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/heigh-change-fr.2d82c1b5.png" alt="Two states of the same UI Component displayed side by side. Pie chart on the left, short text on the right. French version."></p>
<p>Depending on the situation, this innocent looking height change can have big impacts on the whole layout and the scroll position.
This kind of details can drive your users crazy:</p>
<ul>
<li>Best case scenario, other parts of the layout slightly move.</li>
<li>Worst case scenario: the button under the mouse cursor moves and something else is placed under (hello Twitter search input).</li>
</ul>
<p>The video below shows what happens when I toggle between states.</p>
<video controls="">
  <source src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/console-overview-shift.671b0216.mp4" type="video/mp4">
  Sorry, your browser doesn't support embedded videos.
</video>
<p>You can observe a first layout shift when I go from &quot;chart&quot; to &quot;info&quot;.
The bar chart moves down and the map gets bigger.</p>
<p>Now look what happens when I scroll down the page and go back to the &quot;chart&quot; state.
Look carefully at the mouse cursor.
Once over the button, I don't move it, yet a simple click triggers a chain reaction:</p>
<ul>
<li>The height of the component is reduced to display the chart.</li>
<li>The height of the whole page is reduced.</li>
<li>The whole page is scrolled up a bit.</li>
<li>The mouse cursor is no longer above the button and is now above some text.</li>
</ul>
<p>Now, how can we prevent this layout shift from happening?</p>
<h2>The absolute position solution</h2>
<p>Before CSS grid, I would have solved this problem with the <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/position">position CSS property</a>.</p>
<p>First, I would use <code>visibility: hidden</code> instead of <code>display: none</code> to hide the &quot;inactive&quot; panel:</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/visibility-hidden-full-height.5110dd5c.png" alt="Two states of the same UI Component displayed side by side. Pie chart on the left, short text on the right."></p>
<p>As you can see, with <code>visibility: hidden</code>, the &quot;inactive&quot; panel is hidden but the component computes its own size as if both panels were there.
Now, I have a component with a stable height, based on the heights of <code>.chart</code> and <code>.info</code>.</p>
<p>Then, I would use <code>position: absolute</code> on the <code>.chart</code>.
Doing this will <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/position#absolute">remove the element from normal flow</a>.
This means the component will compute its own size as if <code>.chart</code> was not there.
Therefore, the component size would mostly depend on the size of <code>.info</code>, which is exactly what I want.</p>
<p>The only step left is to give <code>.chart</code> exactly the same position and size as <code>.info</code>.
Using <code>position: absolute</code> means <code>.chart</code> will be positioned <em>&quot;relative to its closest positioned ancestor&quot;</em>.
That is, an ancestor with a <code>position</code> different from the default value (<code>static</code>).
Most of the time, I use <code>position: relative</code> on the parent to trigger this.
In this situation, I would need an additional <code>.wrapper</code> around both panels like this:</p>

```html
<div class="chart-component">
  <div class="title">
    HTTP response codes <button>toggle chart/info</button>
  </div>
  <div class="wrapper">
    <div class="chart">
      <!-- chart here -->
    </div>
    <div class="info">
      <!-- short text here -->
    </div>
  </div>
</div>
```

<p>Now that I have this HTML, the CSS solution would look like this:</p>

```css
.wrapper {
  /* .wrapper is the closest positioned ancestor of .chart */
  position: relative;
}

.chart {
  /* .chart is removed from normal flow, therefore
     .wrapper has the exact same size (and position) as .info
     because .info is the only child left in the normal flow
     .chart is positioned relatively to .wrapper */
  position: absolute;
  /* same position as .wrapper (thus same position as .info) */
  left: 0;
  top: 0;
  /* same size as .wrapper (thus same size as .info) */
  height: 100%;
  width: 100%;
}
```

<p>This <code>position: absolute</code> solution helps me achieve my goal.
The size of my component will now always depend on the size of <code>.info</code>, even when it's hidden and <code>.chart</code> is visible.</p>
<p>🤔 No more layout shifts when I toggle between states but still a few limitations:</p>
<p>First, I had to add a <code>.wrapper</code> with a <code>position: relative</code> just so I could use <code>position: absolute</code> on <code>.chart</code>.
CSS is awesome but it's also tricky to master.
If you're still struggling with how the position CSS property works, this approach can be scary but don't worry:</p>
<ul>
<li>It's OK. You're not alone!</li>
<li>It gets better with time and practice...</li>
</ul>
<p>Second, I had to assume the component size would only depend on <code>.info</code>.
In more complex situations, I could have more than just two panels where I cannot assume which one should guide the size of the whole component.</p>
<p>Now, let's find out how grid can solve this better.</p>
<h2>The grid solution</h2>
<p>CSS grid are supported in <a href="https://caniuse.com/css-grid">all evergreen browsers</a> now.
This means we can rely on the fact that you can place and overlap multiple elements in the same grid area.</p>
<blockquote>
<p>What do you mean by grid area?</p>
</blockquote>
<p>If you're not familiar with CSS grid, before continuing this article, I would advise you to:</p>
<ul>
<li>Watch any talk from <a href="https://twitter.com/rachelandrew">Rachel Andrew</a>. She's awesome and in this <a href="https://www.youtube.com/watch?v=g1osnSY9mSU">recent video</a> she did with <a href="https://twitter.com/jlengstorf">Jason Lengstorf</a>, they explain CSS grid through live coding examples.</li>
<li>Always have the CSS-Tricks <a href="https://css-tricks.com/snippets/css/complete-guide-grid/">Complete Guide to Grid</a> open in a tab.</li>
<li>Practice, practice, practice...</li>
</ul>
<p>Now that you're more familiar with CSS grid, we can talk about the now famous <em>&quot;Anti Layout Shift Grid Stacking Technique&quot;</em> (sorry).
Let's take the initial simple template we had earlier:</p>

```html
<div class="chart-component">
  <div class="title">
    HTTP response codes <button>toggle chart/info</button>
  </div>
  <div class="chart">
    <!-- chart here -->
  </div>
  <div class="info">
    <!-- short text here -->
  </div>
</div>
```

<p>If we use CSS grid on <code>.chart-component</code> like this:</p>

```css
.chart-component {
  display: grid;
  gap: 1rem;
}
```

<p>Our component looks like this:</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/component-grid-all.05666afe.png" alt="UI Component displaying the pie chart and short text states in a one column grid with Firefox CSS grid inspector"></p>
<p>Thanks to the awesome <a href="https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Examine_grid_layouts">CSS Grid Inspector</a> from Firefox DevTools, we have annotations for columns and rows.
We can see that:</p>
<ul>
<li><code>.title</code>, <code>.chart</code> and <code>.info</code> are between column 1 and 2</li>
<li><code>.title</code> is between row 1 and 2</li>
<li><code>.chart</code> is between row 2 and 3</li>
<li><code>.info</code> is between row 3 and 4</li>
</ul>
<p>This is the default behaviour in a simple 1 column grid: children are placed in order.
With <code>grid-column</code> and <code>grid-row</code>, we can force an element to be placed in a specific area of the grid.</p>
<p>For example, if we place our two elements in the same area like this:</p>

```css
.chart,
.info {
  grid-column: 1 / 2;
  grid-row: 2 / 3;
}
```

<p>The result looks like this:</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/component-grid-superposed.fdd517ac.png" alt="UI Component displaying the pie chart and short text states on top of each other with Firefox CSS grid inspector"></p>
<p>If you are sceptical, remember the appropriate panel will be hidden with a <code>visibility: hidden</code>.</p>
<p>With this technique, we actually told the CSS engine to prepare a grid where the area at column <code>1 / 2</code> and row <code>2 / 3</code> should adapt to what's inside.
In other words: this very area will always be as big as the biggest between <code>.chart</code> and <code>.info</code>.
Therefore, the height of the whole component will be the same when toggling between states.</p>
<video controls="">
  <source src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/console-overview-no-shift.846024a1.mp4" type="video/mp4">
  Sorry, your browser doesn't support embedded videos.
</video>
<p>😎 No more layout shifts!</p>
<p>Compared to the absolute position solution, we improved the situation:</p>
<ul>
<li>No need to add a <code>.wrapper</code></li>
<li>Would work fine with more than two panels</li>
<li>No need to assume which panel needs to guide the whole component size</li>
</ul>
<p>Now, as a bonus, let me show you another real example where I used this technique.</p>
<h2>Preventing horizontal shifts</h2>
<p>The previous example was about the height of a component but you can also use this technique with width.
If you give another look at the Clever Cloud console overview, in the top right corner, you'll notice a few buttons to control your app's state.</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/console-overview.9f0ce33e.png" alt="Screenshot of Clever Cloud's Web console displaying the overview of a Node.js application"></p>
<p>The &quot;Stop app&quot; button has bigger horizontal paddings than the other buttons.
This is because, when clicking this very button, the text is replaced with &quot;Click to cancel&quot; for 3 seconds in case you 
panick
 changed your mind.</p>
<p>If we didn't use the grid technique; when the button changes its text from &quot;Stop app&quot; to &quot;Click to cancel&quot;, the width change would move all the other buttons like this:</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/buttons-shift.41246622.png" alt="Two lines of four buttons next to each other with different widths"></p>
<p>On narrow screens, the wrapping of the buttons could trigger a bigger layout shift like this:</p>
<video controls="">
  <source src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/stop-button-shift.6d521d56.mp4" type="video/mp4">
  Sorry, your browser doesn't support embedded videos.
</video>
<p>Thanks to the  <em>&quot;Anti Layout Shift Grid Stacking Technique&quot;</em> (please send naming suggestions, I'm dying), no need to assume which state will be larger.
Whatever the lengths of the <em>normal text</em> and the <em>cancel text</em> are, the button will always be big enough.</p>
<p><img src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/buttons-no-shift.08e3cd4d.png" alt="Two lines of four buttons next to each other with the same widths"></p>
<video controls="">
  <source src="https://www.hsablonniere.com/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/stop-button-no-shift.1be9b925.mp4" type="video/mp4">
  Sorry, your browser doesn't support embedded videos.
</video>
<p>😎 No more layout shifts!</p>
<p>I hope this article helped you consider this kind of layout shifts as a potential problem to solve.
I also hope you'll find this technique useful in your own projects.</p>
<h2>References &amp; Links</h2>
<p>About CSS and grid:</p>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Web/CSS/position">CSS position property reference</a> on MDN</li>
<li><a href="https://caniuse.com/css-grid">CSS Grid Layout support in browsers</a> on Can I use</li>
<li><a href="https://www.youtube.com/watch?v=g1osnSY9mSU">Let’s Learn CSS Grid! (with Rachel Andrew)</a> on Jason Lengstorf YouTube channel</li>
<li><a href="https://css-tricks.com/snippets/css/complete-guide-grid/">Complete Guide to Grid</a> on CSS-Tricks</li>
<li>Please consider using Firefox and their awesome <a href="https://developer.mozilla.org/en-US/docs/Tools/Page_Inspector/How_to/Examine_grid_layouts">CSS Grid Inspector</a></li>
</ul>
<p>The pie chart component and the button component are part of Clever Cloud's component library.
The code is open-sourced on GitHub and the documentation (with live preview) is published with <a href="https://storybook.js.org/">Storybook</a>.</p>
<p>You can find out more about the components we talked about here:</p>
<ul>
<li><a href="https://www.chartjs.org/">Charts.js</a>: the JavaScript library used for those charts</li>
<li><code>&lt;cc-tile-status-codes&gt;</code>: the pie chart component
<ul>
<li><a href="https://github.com/CleverCloud/clever-components/blob/master/src/overview/cc-tile-status-codes.js">Source code</a></li>
<li><a href="https://www.clever-cloud.com/doc/clever-components/?path=/story/%F0%9F%9B%A0-overview-cc-tile-status-codes--data-loaded">Docs with live preview</a></li>
</ul>
</li>
<li><code>&lt;cc-button&gt;</code>: the button component with &quot;click to cancel&quot;
<ul>
<li><a href="https://github.com/CleverCloud/clever-components/blob/master/src/atoms/cc-button.js">Source code</a></li>
<li><a href="https://www.clever-cloud.com/doc/clever-components/?path=/story/%F0%9F%A7%AC-atoms-cc-button--delay-and-outlined">Docs with live preview</a></li>
</ul>
</li>
</ul>
<h2>Thank you</h2>
<p>😍 Thank you wonderful reviewers for your time: <a href="https://twitter.com/juuduu/">Julien Durillon</a>, <a href="https://alexandre.berthaud.me/">Alexandre Berthaud</a>, <a href="https://ricaud.me/blog/">Anthony Ricaud</a>, <a href="https://twitter.com/mereteresa">Sarah Haïm-Lubczanski</a>, <a href="https://twitter.com/jlengrand">Julien Lengrand-Lambert</a> and <a href="https://twitter.com/RalfDMueller">Ralf D. Müller</a>.</p>

  <div class="edit-link-wrapper">
    <a href="https://github.com/hsablonniere/hsablonniere.com/blob/master/pages/prevent-layout-shifts-with-css-grid-stacks--qcj5jo/index.md">Edit this page on GitHub</a>
  </div>