Animating CSS Grid: The Discrete Value Problem
grid-template-columns won't animate—fr units lack computable intermediate values, breaking smooth transitions
Add transition: all 0.3s to a grid container, then change grid-template-columns from 1fr 1fr to 1fr 2fr. Nothing animates—the layout snaps instantly. The problem: grid-template-columns is a discrete property. Browsers can't interpolate between 1fr values because fractional units depend on available space, which changes during layout. CSS can only animate properties with computable intermediate values. Grid template definitions lack this, breaking the smooth transitions designers expect.
Why Grid Templates Can't Animate
CSS transitions work by calculating intermediate values between a start state and end state. For numeric properties like width or opacity, this is straightforward:
/* width: Animatable (numeric pixels) */
.box {
width: 100px;
transition: width 0.3s;
}
.box:hover {
width: 200px;
}
/* Browser calculates:
Frame 1: width: 100px
Frame 2: width: 110px
Frame 3: width: 120px
...
Frame N: width: 200px
*/But grid-template-columns uses non-interpolatable values:
/* grid-template-columns: NOT animatable */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
transition: grid-template-columns 0.3s; /* Ignored! */
}
.grid.expanded {
grid-template-columns: 1fr 2fr;
}
/* Why it fails:
- "1fr" is a fraction of remaining space
- Remaining space depends on container width
- Container width may change during layout
- No way to compute intermediate "1.5fr" values
- Browser gives up: instant snap instead of transition
*/The Discrete Property Problem
CSS classifies properties as animatable or discrete based on whether intermediate values can be computed. Discrete properties change instantly—no smooth transition possible.
Common discrete properties:
display(block → flex: no intermediate state)visibility(visible → hidden: no partial visibility)grid-template-columns(1fr 1fr → 1fr 2fr: non-computable)grid-template-rows(same reason as columns)grid-template-areas(named grid areas can't interpolate)
Animatable alternatives:
transform(translate, scale, rotate)opacity(0 to 1)width,height(pixels, %, em)gap(grid/flexbox gap is animatable!)
Why 'fr' Units Break Interpolation
The fr unit represents a fraction of remaining space after fixed-size tracks are allocated. This makes it non-deterministic during animation:
/* Example grid: */
.grid {
width: 600px;
grid-template-columns: 100px 1fr 1fr;
}
/* Column widths:
Column 1: 100px (fixed)
Remaining space: 600px - 100px = 500px
Column 2: 500px × (1fr / 2fr) = 250px
Column 3: 500px × (1fr / 2fr) = 250px
*/
/* Now animate to: */
.grid.expanded {
grid-template-columns: 100px 1fr 2fr;
}
/* New column widths:
Column 1: 100px (fixed)
Remaining space: 500px
Column 2: 500px × (1fr / 3fr) = 166.67px
Column 3: 500px × (2fr / 3fr) = 333.33px
Problem: Browser can't interpolate fractional ratios
- Intermediate state "1fr 1.5fr" has no meaning
- Remaining space changes if container resizes during animation
- No way to compute frame-by-frame values
*/Workaround 1: Animate Grid Gap Instead
The gap property is animatable because it's a fixed length value. Use gap changes to create visual motion:
<div class="grid-gap-demo">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
</div>
<style>
.grid-gap-demo {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
transition: gap 0.3s ease-out;
}
.grid-gap-demo:hover {
gap: 32px; /* Smooth expansion */
}
/* Result: Items stay same size, spacing grows smoothly */
</style>Limitation: This only animates spacing, not column widths. Good for subtle hover effects, but doesn't solve layout restructuring.
Workaround 2: Animate Individual Grid Item Widths
Instead of changing the grid template, animate width on grid items. This works because pixel/percentage widths are animatable:
<div class="grid">
<div class="item item-1">Sidebar</div>
<div class="item item-2">Main Content</div>
</div>
<style>
.grid {
display: grid;
grid-template-columns: auto 1fr; /* Sidebar auto-sizes, content fills */
}
.item-1 {
width: 200px;
transition: width 0.3s ease-out;
}
.item-1.collapsed {
width: 60px; /* Smooth collapse */
}
/* Advantages:
- width is animatable (pixels)
- Grid auto-adjusts second column
- Smooth transition
*/
/* Disadvantages:
- Doesn't work with pure "1fr 2fr" layouts
- Requires mixing auto/fixed widths with fr units
*/
</style>Workaround 3: FLIP Technique with Transform
Use the FLIP technique (First, Last, Invert, Play) to animate grid items with transform instead of changing layout properties. This keeps animations compositor-optimized.
The FLIP Approach
<div class="grid" id="animatedGrid">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
</div>
<button onclick="toggleLayout()">Toggle Layout</button>
<style>
.grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 16px;
}
.grid.two-column {
grid-template-columns: 2fr 1fr; /* Third item wraps to new row */
}
.item {
background: hsl(220, 90%, 50%);
padding: 40px;
color: white;
border-radius: 8px;
}
</style>
<script>
function toggleLayout() {
const grid = document.getElementById('animatedGrid');
const items = Array.from(grid.children);
// FIRST: Record current positions
const first = items.map(item => item.getBoundingClientRect());
// LAST: Apply new layout (instant)
grid.classList.toggle('two-column');
const last = items.map(item => item.getBoundingClientRect());
// INVERT + PLAY: Animate each item
items.forEach((item, i) => {
const deltaX = first[i].left - last[i].left;
const deltaY = first[i].top - last[i].top;
const deltaW = first[i].width / last[i].width;
const deltaH = first[i].height / last[i].height;
// Invert: Apply transform to "undo" layout change
item.style.transform = `
translate(${deltaX}px, ${deltaY}px)
scale(${deltaW}, ${deltaH})
`;
item.style.transition = 'none';
// Play: Animate to final position
requestAnimationFrame(() => {
item.style.transition = 'transform 0.4s ease-out';
item.style.transform = 'none';
});
});
}
</script>
/* How FLIP works:
1. FIRST: Measure items at 1fr 1fr 1fr (3 columns)
2. LAST: Instantly change to 2fr 1fr (2 columns)
3. INVERT: Use transform to move items back to original positions
4. PLAY: Animate transform back to identity (final positions)
Result: Smooth animation despite instant grid template change
Performance: Only transform animates (compositor thread)
*/FLIP Performance Characteristics
- Pros: Compositor-safe (GPU accelerated), smooth 60fps, works with any layout change
- Cons: Requires JavaScript, measures layout (getBoundingClientRect cost), slight initial jank on large grids
Workaround 4: CSS Custom Properties with Calc
Define column widths using CSS custom properties with fixed units (pixels or percentages). Animate the custom property value:
<div class="grid-custom-props">
<div class="item">Sidebar</div>
<div class="item">Content</div>
</div>
<style>
.grid-custom-props {
--sidebar-width: 200px;
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
transition: --sidebar-width 0.3s; /* ❌ Doesn't work yet! */
}
.grid-custom-props.collapsed {
--sidebar-width: 60px;
}
/* Problem: CSS custom properties aren't animatable by default
Chrome 128+ supports @property for animatable custom props
*/
</style>
/* Solution: Register custom property with @property */
@property --sidebar-width {
syntax: '<length>';
initial-value: 200px;
inherits: false;
}
.grid-custom-props {
display: grid;
grid-template-columns: var(--sidebar-width) 1fr;
transition: --sidebar-width 0.3s ease-out;
}
.grid-custom-props.collapsed {
--sidebar-width: 60px; /* Now animates smoothly! */
}
/* Browser support: Chrome 85+, Safari 16.4+, Firefox not yet (as of 2025)
Use @supports for progressive enhancement
*/Browser Support Edge Case
/* Progressive enhancement */
@supports (animation-timeline: scroll()) {
/* Modern browsers support @property */
@property --sidebar-width {
syntax: '<length>';
initial-value: 200px;
inherits: false;
}
.grid-custom-props {
transition: --sidebar-width 0.3s;
}
}
@supports not (animation-timeline: scroll()) {
/* Fallback: Instant change for older browsers */
.grid-custom-props {
transition: none;
}
}Workaround 5: Web Animations API with Interpolation
Use the Web Animations API to manually interpolate between grid column values. This requires JavaScript but gives full control:
const grid = document.querySelector('.grid');
// Animate from "200px 1fr" to "400px 1fr"
grid.animate([
{ gridTemplateColumns: '200px 1fr' },
{ gridTemplateColumns: '400px 1fr' }
], {
duration: 300,
easing: 'ease-out',
fill: 'forwards'
});
/* Browser behavior:
- Web Animations API treats gridTemplateColumns as discrete
- Animation snaps at 50% (no smooth interpolation)
- Same limitation as CSS transitions
This ONLY works if column values are fixed units (px, %)
*/
/* For fr units, manual interpolation required: */
function animateGridColumns(grid, fromFr, toFr, duration) {
const start = performance.now();
const containerWidth = grid.offsetWidth;
function animate(currentTime) {
const elapsed = currentTime - start;
const progress = Math.min(elapsed / duration, 1);
const eased = easeOutCubic(progress);
// Interpolate fr ratio
const currentFr = fromFr + (toFr - fromFr) * eased;
// Convert to pixels (assumes 2-column layout)
const col1Width = containerWidth * (1 / (1 + currentFr));
const col2Width = containerWidth - col1Width;
grid.style.gridTemplateColumns = `${col1Width}px ${col2Width}px`;
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
// Usage: Animate from "1fr 1fr" to "1fr 2fr"
animateGridColumns(grid, 1, 2, 300);
/* Disadvantages:
- Requires JavaScript
- Converts fr to px (loses responsive benefits)
- Must recalculate on window resize
- More complex than CSS transitions
*/The Future: interpolate-size and Discrete Transitions
CSS is evolving to support animation of previously discrete properties. The interpolate-size property (CSS Working Draft, 2025) aims to enable smooth transitions for auto and intrinsic sizing values.
/* Future CSS (experimental): */
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
transition: grid-template-columns 0.3s;
interpolate-size: allow-keywords; /* Proposed property */
}
.grid.expanded {
grid-template-columns: 1fr 2fr; /* May animate in future browsers */
}
/* Status: Not yet implemented (2025)
Spec: https://drafts.csswg.org/css-values-5/#interpolate-size
Browser support: None (proposal stage)
*/Why This Is Hard
Animating fr units requires solving complex problems:
- Dynamic container sizing: If container width changes during animation, fr ratios recalculate
- Content intrinsic sizes:
minmax(min-content, 1fr)depends on actual content, which may load asynchronously - Nested grids: Parent grid changes affect child grid fr calculations
- Performance: Recalculating layout every frame is expensive (defeats compositor optimization)
Practical Recommendation: Choose Your Battles
Use Case 1: Collapsible Sidebar
Goal: Animate sidebar from 250px to 60px (icon-only mode).
Best approach: Animate grid item width (Workaround 2)
.grid {
display: grid;
grid-template-columns: auto 1fr;
}
.sidebar {
width: 250px;
transition: width 0.3s ease-out;
}
.sidebar.collapsed {
width: 60px;
}
/* Why: Simple, performant, works everywhere */Use Case 2: Dashboard Layout Toggle
Goal: Switch from 3-column to 2-column layout on button click.
Best approach: FLIP technique (Workaround 3)
/* Use JavaScript to measure, invert, and animate with transform
Keeps animation compositor-safe
Handles arbitrary layout changes
*/Use Case 3: Responsive Breakpoints
Goal: Grid changes from 3 columns to 1 column on mobile.
Best approach: No animation (instant snap)
@media (max-width: 768px) {
.grid {
grid-template-columns: 1fr; /* Instant, no transition */
}
}
/* Why: Responsive layout changes should be instant
Users expect layouts to adapt immediately
Animation would feel sluggish
*/Production Checklist
- Accept instant snaps for most grid changes: Responsive breakpoints shouldn't animate.
- Animate gap instead of columns: If you only need spacing changes,
gapis animatable. - Use FLIP for complex layouts: When grid template changes must animate, use FLIP with transform.
- Avoid animating width/height: These trigger layout (slow). Prefer transform (compositor).
- Test with @property: For fixed-width columns, animate CSS custom properties (Chrome 85+).
- Consider Web Animations API: Only if you need JavaScript control and manual interpolation.
- Document browser support: @property is not supported in Firefox (2025). Provide fallbacks.
- Profile performance: Use Chrome DevTools to verify animations don't cause layout thrashing.
CSS Grid's fr units are powerful for responsive layouts but impossible to animate smoothly. The browser can't compute intermediate values for fractional space allocation. Until interpolate-size ships (if ever), your options are: animate gap instead, use fixed widths with @property, or apply the FLIP technique to fake smooth transitions with transform. Most grid layout changes should snap instantly—save animations for sidebar collapses and dashboard toggles where users expect motion.
Advertisement
Explore these curated resources to deepen your understanding
Official Documentation
MDN: grid-template-columns
CSS Grid template columns syntax and fr unit specification
MDN: CSS Animated Properties
Complete list of animatable vs discrete CSS properties
MDN: @property
Register custom properties for animation (CSS Houdini)
W3C: CSS Values and Units - interpolate-size
Proposed specification for animating intrinsic sizing keywords (draft)
Tools & Utilities
Further Reading
FLIP Your Animations - Paul Lewis
Original article introducing the FLIP technique (2015)
Animating CSS Grid - Chen Hui Jing
Deep dive into grid animation workarounds and limitations (2019)
The Current State of Animating Grid Layout
CSS-Tricks guide to grid animation techniques and FLIP approach
CSS Houdini: @property and Custom Property Animation
Google Web.dev guide to animating custom properties (2023)
Related Insights
Explore related edge cases and patterns
Advertisement