I recently ran into a situation where I needed to take a one-column menu and have a pop-out submenu; nothing too unusual or fancy, really. However, the way the design worked made sense to use CSS Grid, and in initial testing this performed really well. We had a conditional value that changed our grid from a single column to two, and back again[1]. It allowed us to quickly control wrapping elements and layout in a way that was pretty intuitive and well supported. But then we were asked to make the second column "slide out", which, to be fair, is a fairly common and pretty simple animation, in many ways. However, I ran into a few hurdles getting this to actually work, so figured I'd make a note of what went wrong and what went well 😉
Can You Animate grid-template-columns
?
Our first attempt was to simply stick a global transition onto our element. Something like transition: all 0.3s ease
. My hope had been that this would just magically work and force the change in the grid template to interpolate its position as it was modified, but alas, no such luck. But was this failure because grid templates are not supported by CSS animations/transitions? Or was there something else at work?
Searching around online was inconclusive. There are plenty of "helpful" Stack Overflow questions and Reddit threads that are pretty clear: the CSS Grid spec supports animation across most layout parameters, but few browsers have implemented that part of the spec. That feels cut and dry: Grid cannot be animated. But almost all of these answers are from four or more years ago. Checking CanIUse gives a different picture of current support:
So it would seem that almost all modern browsers have pretty good support for Grid animations 🙌 Good news, but then why won't my grid animate?
The Code
I'm not going to write up a full, working version of the menu I was building (sorry! 😅), but here's a skeleton version of the layout that demonstrates the issue (FYI there's a CodePen at the end of the article if you just want to compare the before/after 😉):
section { display: grid; grid-template-columns: 1fr; max-width: max-content; background-color: rebeccapurple; transition: all 1s ease; } div { min-height: 10rem; min-width: 10rem; background-color: cornflowerblue; } .twoColGrid { grid-template-columns: 1fr 1fr; } --- <section> <div></div> </section>
So we have a grid element that contains a child with a minimum height and width, and a single column. Then we have a class that can be added to that grid element, which will add a second column to the grid layout. Some background colours have been used, just to help visualise the grid in its two states (particularly as we have no content).
This should give us a blue square initially, and when the twoColGrid
class is added, it will become a half-blue and half-purple rectangle. The transformation between those two states should take one second, with some slight easing, thanks to the transition
parameter. Something like this:
However, what we actually get is:
Not so nice 😬
The Hidden Tricks
After quite a bit of frustrating trial and error, I finally uncovered a couple of little gotchas:
- The animation is run on the dimension change of the column or row, but not the grid as a whole;
- The units used must conform[1].
That first gotcha was my major stumbling block and even writing it out now doesn't feel all that clear. What I mean is that the animation doesn't care about your grid layout at all. Increasing or decreasing the number of rows or columns in your grid is completely ignored by the animations. But if you increase the height or the width of a grid cell (either individually or as a whole row/column) then the animation will kick in. That means the overall grid can change dimensions considerably without triggering an animation, but any preexisting grid cells that change dimensions will be animated.
Knowing that, it's fairly simple to fix the code we have above. We just need to change the initial grid layout to start with two columns, and set the second column to zero width so it doesn't appear[1]:
section { ... grid-template-columns: 1fr 0px; ... }
But that still doesn't work properly 😑 After some more trial and error, I worked out that second gotcha, and it finally clicked into place. Hopefully this one is a bit more self-explanatory: my issue is now that pixel value. So if your columns in the pre-expanded version of the grid are using fr
units, then the columns in the expanded grid should also be set using fr
units. If they're use px
before, use px
after. Etc. That gave me:
section { ... grid-template-columns: 1fr 0fr; ... }
And suddenly it all works 🎉
If you'd like to play around with the code directly, I created a quick (and fairly dirty 😅) minimum test case over on CodePen; feel free to Fork it or just mess around with it locally: CodePen Example.
Oh, And One More Thing
I'm sure there are plenty of other small idiosyncrasies with animating grids, but the above code got me over the initial hurdle. Still, I did run into one other little irritation once I began adding content to my slide-out column: the content would often appear too early, breaking out of the grid container visually 🤦♂️
There are a couple of potential fixes for this. You can set overflow: hidden
(or, if you want to be more specific, overflow-x: hidden
) but I found that had some unwanted side effects with the rest of my layout once the animation had finished.
An alternative option is to use clip-path
to progressively reveal the content as the new column animates into place. For this, I used a keyframed animation, rather than relying on transitions alone, and I'm pretty happy with the result so figured I'd add it here as a bonus tip 😉
@keyframes wipe { 0% { clip-path: polygon(0 0, 105% 0, 105% 100%, 0 100%); } 10% { clip-path: polygon(0 0, 95% 0, 95% 100%, 0 100%); } 100% { clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%); } }
The trick here is to start the clip-path
outside of the initial grid area, but almost instantly bring it back in. This gives the animation a second to start playing before calculating where the clipped area should be applied, and keeps it locked right up against the outer edge as the animation completes, before removing the clipped area entirely at the end. Waiting until 10% complete may be a bit too generous for most use cases, but I had some padding before my content which made this work well. Another option is to delay the animation briefly, but I found this very tricky to actually line up with the expanding grid, so your mileage may vary.