Preventing Layout Shift

Using CSS Grid to Prevent Page Jank

Lawrence Chabela CSS & HTML, Design, JavaScript, Programming, Tutorial Leave a Comment

Layout shift, or page jank as I like to call it, rears its ugly head when a visible element changes position or dimensions, causing the position or dimensions of content around it to be changed.

There are a variety of reasons layout shifts happen, from images loading without dimensions specified, content being removed or added, elements changing their dimensions, and the list could go on.

There are too many possible reasons behind a layout shift to cover in one post, so I’ve narrowed it down to one in particular for the sake of brevity. In this article, we will discuss this scenario: a component changing its height due to its state being changed from user interaction.

Before we dive into the topic at hand, here are a few things you’ll need to have in order to get the most out of this technique:

  • A fairly good grasp of CSS
  • An understanding of how CSS grid works and is written
  • Basic Javascript knowledge

Setting the Stage

Let’s say we have a page made up of various columns of content and in the middle of that content we have a component that displays various pieces of content based on user interaction, also known as tabs. Below you can see the layout we will start with.

As you can see nothing too crazy going on here; it’s just some text with some tabs. What I want to bring to your attention is the interaction with the tabs and how the page reacts.

If we click on the last tab, we can observe how the content shifts up and down the page. Look a little closer, and you’ll notice that the scroll position changes as well.

This is an issue. There’s no arguing that! The page is not behaving how we want it to. It’s one thing to see we have an issue but it’s another to understand why. So, let’s take a look at a brief portion of the HTML

<div class="c-tabs">
    <div class="c-tabs__header">
        <button
            type="button"
            class="c-tabs__tab is-active js-tab"
            data-panel="1"
        >
            Origin of Ballooning
        </button>
        <!-- More Tabs Below Here -->
    </div>
    <div class="c-tabs__panels">
        <div
            class="c-tabs__panel is-active js-panel"
            data-panel="1"
        >
            <!-- Tab Panel Content Here -->
        </div>
        <!-- More Tabs Panels Below Here -->
    </div>
</div>

… and also some of the CSS

.c-tabs__panel {
    display: none;
}

.c-tabs__panel.is-active {
    display: block;
}

… and while we’re at it, some of the Javascript

const handleTabClick = (e) => {
    // Pseudo event delegation :)
    if (!e.target.matches(`.${tabClass}`)) return;

    // Clear any existing state
    tabsEls.forEach((t) => t.classList.remove(activeClass));
    panelEls.forEach((t) => t.classList.remove(activeClass));

    // Activate Clicked Tab
    e.target.classList.add(activeClass);

    // Activate Panel
    panelEls
        .item(Array.from(tabsEls).indexOf(e.target))
        .classList.add(activeClass);
};

What’s going on here? When the user clicks through the tabs, the above Javascript clears any existing state we already have by hiding all the c-tabs__panels by removing their is-active class and also deselecting any active c-tabs__tab by removing its is-active class.

If we look a little closer, we can see by toggling the panel elements is-active class we are changing the elements display property to either block or none.

Ah-ha! We’ve found it. This toggling of the “display” property is what is causing our layout shift.

The contents of each tab panel are different lengths, and because they’re different, the heights of the panels are different. Basically, the dimensions of the tabs themselves change based on the dimension of each bit of content. This dimension change is what causes other content around the tabs to also be shifted up or down.

Now, at first glance, the above problem might not seem like much of a concern. Why should we spend time and effort fixing something so small? I’ll tell you why. It seems minuscule, but something like this can have an impact on a lot – way more than you’d guess. Like…

  • User scroll position can be pushed up or down from where they were.
  • Actions/controls can have their position moved, which can be big annoyance. I think we have all experienced that moment when we try to click on a button but the page shifts.
  • The rendering performance of elements below can be impacted when the browser has to repaint when they are shifted.

Alright, so it is indeed an issue, but how do we go about fixing it? What should we do now?

CSS Grid to the Rescue</h2

To answer your question, we can fix this problem with CSS Grid, and now, you’re probably asking CSS Grid? How? Well, I’m glad you asked.

Let’s begin by taking our same tab component in HTML, specifically the portion that lays out our panels.

<div class="c-tabs__panels">
    <div class="c-tabs__panel is-active js-panel" data-panel="1">
        <!-- Panel Content Here -->
    </div>
    <div class="c-tabs__panel js-panel" data-panel="2">
        <!-- Panel Content Here -->
    </div>
    <div class="c-tabs__panel js-panel" data-panel="3">
        <!-- Panel Content Here -->
    </div>
</div>

Now, what we need to do to solve our issue is lay our panels out so that the tallest
panel will dictate the overall height of the tabs panel container. If we can do this, then the overall height of our tabs will stay the same, which will prevent the surrounding content from moving and causing layout shifts.

As per usual, the devil is in the details. We want our panels to layer on top of each other so they occupy the same area with the largest panel dictating the overall panel’s container its height. To handle this layering will use CSS Grid’s column and row placement to instruct the panels to occupy the same column and row.

We take our panels container and instruct it to be a grid by doing the following:

.c-tabs__panels {
    display: grid;
}

Now that we have our panels container acting as a grid, we can instruct our panels to be placed in the same row and column as each other. To do this, we will add the following:

.c-tabs__panel {
    display: block;
    grid-area: 1 / 1;

    opacity: 0;
    pointer-events: none;
}

These code snippets aren’t overly long or complicated, but I figured a little explanation might be helpful!

  • grid-area: 1 / 1; – This instructs the element to be placed in the parent’s first column and first row. This one line is what gets us the layered layout we were going for.
  • opacity: 0; pointer-events: none; – Finally, instead of using our display property to visually hide the content, we make the element transparent and prevent any user interaction with it.

This results in a tabs component that is as large as its largest panel and keeps that height while switching through its visible panels. This gives us a component that no longer affects its surrounding content and no longer creates a layout shift.

Success!!

Final Thoughts

I know that this is a relatively isolated example, especially in the grand scheme of things! However, I think this proves an important point. Solving a layout shift doesn’t always have to be a tedious and complicated process to fix. Sometimes, scenarios like this can easily be solved using CSS we already have at our disposal.

Thank you for reading. Let me know if you’ve had similar experiences in the comments below!

And if you liked this post, check out one of my previous posts on using CSS Grid in conjunction with CSS Math, Look Ma No Media Queries.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments