Animating CSS Grid (How To + Examples)

I’m pleased to shine a light on the fact that the CSS grid-template-rows and grid-template-columns properties are now animatable in all major web browsers! Well, CSS Grid has technically supported animations for a long time, as it’s baked right into the CSS Grid Layout Module Level 1 spec.

But animating these grid properties only recently gained supported by all three major browsers. Shall we take a look at a few examples to get the creative juices flowing?

Table of contents

Example 1: Expanding sidebar

First of all, this is what we’re talking about:


<div id="grid">
  <div id="left"></div>

  <div id="right"></div>

</div>
<style>
/* the magic */
#grid {
  transition: 300ms;
  display: grid;
  grid-template-columns: 48px auto;
}

#grid:has(#left:hover) {
  grid-template-columns: 30% auto;
}
/* magic + CSS variable */
#grid {
  transition: 300ms;
  display: grid;
  grid-template-columns: var(--left, 48px) auto;
}

#grid:has(#left:hover) {
  --left: 30%;
}
</style>

A simple two-column grid. Now, before, you might not have built this using CSS Grid because animations and transitions weren’t supported, but what if you wanted the left column — perhaps a sidebar navigation — to expand on hover? Well, now that’s possible.

I know what you’re thinking: “Animating a CSS property? Easy peasy, I’ve been doing it for years!” Me too. However, I ran into an interesting snag while experimenting with a particular use case.

So, we want to transition the grid itself (specifically grid-template-columns, which is set on the .grid class in the example). But the left column (.left) is the selector that requires the :hover pseudo-class. While JavaScript can solve this conundrum easily — thanks, but no thanks — we can accomplish it with CSS alone.

Let’s walk through the whole thing, starting with the HTML. Pretty standard stuff really... a grid with 2 columns


</div id="grid">
  <div id="left"></div>

  <div id="right"></div>

</div>

Putting the cosmetic CSS aside, you’ll first need to set display: grid on the parent container (.grid).


<style>
.grid {
  display: grid;
}
</style>

Next, we can define and size the two columns using the grid-template-columns property. We’ll make the left column super narrow, and later increase its width on hover. The right column takes up the rest of the remaining space, thanks to the auto keyword.


<style>
.grid {
  display: grid;
  grid-template-columns: 48px auto;
}
</style>

We know we’re going to animate this thing, so let’s go ahead and throw a transition in there while we’re at it so the change between states is smooth and noticeable.


<style>
.grid {
  display: grid;
  grid-template-columns: 48px auto;
  transition: 300ms; /* Change as needed */
}
</style>

That’s it for the .grid! All that’s left is to apply the hover state. Specifically, we’re going to override the grid-template-columns property so that the left column takes up a greater amount of space on hover.

This alone isn’t all that interesting, although it’s awesome that animations and transitions are supported now in CSS Grid. What’s more interesting is that we can use the relatively new :has() pseudo-class to style the parent container (.grid) while the child (.left) is hovered.


<style>
.grid:has(.left:hover) {
  /* Hover styles */
}
</style>

In plain English this is saying, “Do something to the .grid container if it contains an element named .left inside of it that is in a hover state.” That’s why :has() is often referred to as a “parent” selector. We can finally select a parent based on the children it contains — no JavaScript required!

So, let’s increase the width of the .left column to 30% when it is hovered. The .right column will continue to take up all the leftover space:


<style>
.grid {
  display: grid;
  transition: 300ms;
  grid-template-columns: 48px auto;
}

.grid:has(.left:hover) {
  grid-template-columns: 30% auto;
}
</style>

We could use CSS variables as well, which may or may not look cleaner depending on your personal preferences (or you might be using CSS variables in your project anyway):


<style>
.grid {
  display: grid;
  transition: 300ms;
  grid-template-columns: var(--left, 48px) auto;
}

.grid:has(.left:hover) {
  --left: 30%;
}
</style>

I love that CSS grids can be animated now, but the fact that we can build this particular example with just nine lines of CSS is even more astounding.

Example 2: Expanding Panels


</div id="grid">
  <div class="left"></div>
	<div class="center"></div>
  <div class="right"></div>
</div>
<style>
body {
  margin: 0;
}

#grid {
  height: 100vh;
  transition: 500ms;
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  gap: 3px;
  background: crimson;
}

:where(.left, .center, .right) {
  background: navajowhite;
  transition: 300ms;
}

:where(.left, .center, .right):hover {
  background: crimson;
}

#grid:has(.left:hover) {
  grid-template-columns: 2fr 0.5fr 0.5fr;
}

#grid:has(.center:hover) {
  grid-template-columns: 0.5fr 2fr 0.5fr;
}
#grid:has(.right:hover) {
  grid-template-columns: 0.5fr 0.5fr 2fr;
}
</style>

This example transitions the grid container (the column widths) but also the individual columns (their background colors). It’s ideal for providing more content on hover.

It’s worth remembering that the repeat() function sometimes produces buggy transitions, which is why I set the width of each column individually (i.e. grid-template-columns: 1fr 1fr 1fr).

Example 3: Adding Rows and Columns


</div id="grid">
  <div class="column"></div>
	<div class="column"></div>
  <div class="column"></div>
</div>
<style>
body {
  margin: 0;
  background: navajowhite;
}

#grid {
  height: 100vh;
  transition: 300ms;
  display: grid;
  grid-template-columns: 1fr 1fr 0fr;
}

#grid:hover {
  grid-template-columns: 1fr 1fr 1fr;
}

.column {
  outline: 1.5px solid rgb(0 0 0 / 10%);
}
</style>

This example animatedly “adds” a column to the grid. However — you guessed it — this scenario has a pitfall too. The requirement is that the “new” column mustn’t be hidden (i.e. set to display: none), and CSS Grid must acknowledge its existence while setting its width to 0fr.

So, for a three-column grid — grid-template-columns: 1fr 1fr 0fr (yes, the unit must be declared even though the value is 0!) transitions into grid-template-columns: 1fr 1fr 1fr correctly, but grid-template-columns: 1fr 1fr doesn’t. In hindsight, this actually makes perfect sense considering what we know about how transitions work.