Grid to List Toggle
Smooth grid ↔ list toggle using the FLIP technique — items physically reposition themselves while labels reveal on the fly.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Cards has 41 effects, including 6 free. Explore the category →
3 usage examples



How it works
The container uses CSS Grid: in grid mode, grid-template-columns: repeat(3, 1fr) arranges 6 items in 2 rows. A click adds the .list-mode class, switching to grid-template-columns: 1fr — one full-width column. The issue: grid-template-columns is not an interpolable property in current browsers, so CSS cannot animate this change. JS takes over with the FLIP technique.
The FLIP (First-Last-Invert-Play) technique operates in four phases on each .grid-list-item: First — read current coordinates with getBoundingClientRect(); Last — apply the .list-mode class (Grid layout recalculates immediately); Invert — compute the δx/δy delta between old and new positions, then force transform: translate(δx, δy) without transition to snap the item back to its original visual position; Play — inside a requestAnimationFrame, re-enable the CSS transition and remove the transform, letting the item slide to its real destination.
Simultaneously, two CSS micro-transitions run on each item: the label (.item-text) goes from opacity: 0; max-width: 0 in grid mode to opacity: 1; max-width: 200px in list mode, unfurling from the icon; the icon (.item-icon) shrinks from 32 × 32 px to 24 × 24 px. Both transitions share the same cubic-bezier(.4, 0, .2, 1) as the FLIP, visually synchronising the text reveal with the item's movement.
The function setGridList(button, mode) is declared as a global (outside any IIFE) so it can be called from onclick HTML attributes. It finds the toggle group via t.parentElement.querySelectorAll('.morph-toggle') to update the .active state — making it possible to wire multiple instances on the same page by pointing each to a distinct target container.
Accessibility
- prefers-reduced-motion absent: neither the CSS nor the JS checks this OS preference — the FLIP animation and
max-width/opacitytransitions run even if the user has enabled Reduce Motion. Must be added before accessible production use. - Toggle buttons are real
<button>HTML elements — keyboard-activatable (Tab, Enter, Space) without extra JS. - No
aria-pressedon the toggle buttons: the active state (class.active) is not exposed to screen readers. - The
.grid-list-containerhas noroleoraria-label— the region's semantics are not declared. - The
#a5b4fccolour of labels visible in list mode against the dark background (~rgba(99,102,241,.15)on#0a0a0f) achieves a contrast ratio ≥ 5:1 — WCAG AA met for normal text.
Browser compatibility
Requires CSS Grid Level 1 and requestAnimationFrame — supported in all modern browsers. grid-template-columns is not natively interpolable: smoothness depends entirely on the JS FLIP technique.
If JavaScript is disabled, the toggle buttons have no effect and the view stays locked in grid mode by default — content remains readable and the layout does not break.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="demo-preview">
<div class="morph-toggle-group">
<button class="morph-toggle active" onclick="setGridList(this, 'grid')">Grid</button>
<button class="morph-toggle" onclick="setGridList(this, 'list')">List</button>
</div>
<div class="grid-list-container" id="gridListDemo">
<div class="grid-list-item">
<div class="item-icon"></div>
<span class="item-text">Label</span>
</div>
<!-- repeat .grid-list-item as many times as needed -->
</div>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| --primary | #6366f1 | CSS variable controlling toggle button colour, item borders and background gradient. Swap for your brand colour. |
| grid-template-columns | repeat(3, 1fr) | Columns in grid mode. Change 3 to 2 (wide items), 4 (compact items) or 6 (icon-only, no text). |
| transition: .5s | 0.5s cubic-bezier(.4, 0, .2, 1) | Duration and easing of the FLIP and max-width/opacity transitions. 0.3s for a snappy feel, 0.8s for a cinematic effect. |
| .item-icon width/height | 32px / 32px (grid), 24px / 24px (list) | Icon size per mode. Go to 40px in grid for more prominent icons, or 20px in list for an ultra-compact view. |
| .item-text max-width | 200px (list mode) | Max width of the label revealed in list mode. From 120px for short tags to 320px for long titles. |
| gap | 8px | Spacing between items (both modes). 12–16px for an airy layout, 4px for an ultra-dense list. |
| .grid-list-item padding | 12px | Internal padding of each item. 16–20px for airy items with large icons, 8px for a compact list. |
| .grid-list-item border-radius | 8px | 4px for a strict rectangular style, 12px for soft cards, 50% (with square items and icon only) for circular badges. |
FAQ
grid-template-columns is not an interpolable property — browsers cannot compute intermediate states between repeat(3, 1fr) and 1fr. The FLIP technique works around this: it measures positions before and after the layout change, instantly snaps each item back with transform: translate(δx, δy), then removes the transform inside a requestAnimationFrame — the CSS transition takes over and creates the illusion of a physical slide.setGridList function hard-codes document.getElementById('gridListDemo'). For multiple instances, duplicate the HTML structure with a distinct id per container, then create a version of the function specific to each id — or adapt it to traverse the DOM from the button: t.closest('.your-wrapper').querySelector('.grid-list-container') before running the rest of the FLIP logic.<button> HTML elements respond to touch events without extra configuration. There is no built-in swipe gesture; if you want to trigger the toggle on a swipe, add a touchstart or pointerdown listener to the container and call setGridList manually.