About this snippet

A grid of project cards with a row of filter buttons above it. Pick a category and the cards that stay glide to their new positions while the others fade out.

The filtering is ordinary. Each button toggles the hidden attribute on the cards that do not match. The motion comes from wrapping that update in document.startViewTransition. The browser snapshots the grid, applies the change, snapshots again and animates between the two. Every card carries its own view-transition-name, set once from its id, which lets the browser follow a card from its old spot to its new one instead of cross-fading the whole grid.

Same-document view transitions are in Chrome, Edge and Safari, and Firefox 144 added them in October 2025. The script checks for startViewTransition first and falls back to applying the filter immediately, so the grid still works everywhere.

Size
4.4 KB
Uses
HTML, CSS, JavaScript
Loads
Nothing external
Views
94
<section class="gallery">
  <div class="filters" role="group" aria-label="Filter projects">
    <button type="button" class="filter active" data-filter="all">All</button>
    <button type="button" class="filter" data-filter="web">Web</button>
    <button type="button" class="filter" data-filter="brand">Brand</button>
    <button type="button" class="filter" data-filter="motion">Motion</button>
  </div>
  <div class="grid" id="grid">
    <article class="card" id="p1" data-type="web"><span class="swatch s1"></span><h2>Northwind Storefront</h2><p>Web</p></article>
    <article class="card" id="p2" data-type="brand"><span class="swatch s2"></span><h2>Alder Coffee Identity</h2><p>Brand</p></article>
    <article class="card" id="p3" data-type="motion"><span class="swatch s3"></span><h2>Orbit Launch Film</h2><p>Motion</p></article>
    <article class="card" id="p4" data-type="web"><span class="swatch s4"></span><h2>Fieldnotes Dashboard</h2><p>Web</p></article>
    <article class="card" id="p5" data-type="brand"><span class="swatch s5"></span><h2>Marrow Type Family</h2><p>Brand</p></article>
    <article class="card" id="p6" data-type="motion"><span class="swatch s6"></span><h2>Pulse Onboarding</h2><p>Motion</p></article>
    <article class="card" id="p7" data-type="web"><span class="swatch s7"></span><h2>Lumen Docs Portal</h2><p>Web</p></article>
    <article class="card" id="p8" data-type="brand"><span class="swatch s8"></span><h2>Harbor Packaging</h2><p>Brand</p></article>
  </div>
</section>
*, *::before, *::after {
  box-sizing: border-box;
}

body {
  font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
  margin: 0;
  background: #f1f5f9;
  color: #0f172a;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  padding: 20px;
}

.gallery {
  width: 100%;
  max-width: 760px;
}

.filters {
  display: flex;
  gap: 8px;
  margin-bottom: 20px;
}

.filter {
  font: inherit;
  font-weight: 600;
  font-size: 0.875rem;
  padding: 8px 16px;
  border: 0;
  border-radius: 999px;
  background: #ffffff;
  color: #475569;
  cursor: pointer;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.08);
  transition: background 0.15s ease, color 0.15s ease;
}

.filter:hover {
  color: #0f172a;
}

.filter.active {
  background: #0f172a;
  color: #ffffff;
}

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
  gap: 16px;
}

.card {
  background: #ffffff;
  border-radius: 14px;
  padding: 12px;
  box-shadow: 0 1px 2px rgba(15, 23, 42, 0.05), 0 10px 24px -14px rgba(15, 23, 42, 0.25);
}

.card[hidden] {
  display: none;
}

.swatch {
  display: block;
  aspect-ratio: 4 / 3;
  border-radius: 10px;
  margin-bottom: 12px;
}

.s1 { background: linear-gradient(135deg, #2563eb, #38bdf8); }
.s2 { background: linear-gradient(135deg, #b45309, #fbbf24); }
.s3 { background: linear-gradient(135deg, #7c3aed, #ec4899); }
.s4 { background: linear-gradient(135deg, #0f766e, #34d399); }
.s5 { background: linear-gradient(135deg, #1e293b, #64748b); }
.s6 { background: linear-gradient(135deg, #dc2626, #fb923c); }
.s7 { background: linear-gradient(135deg, #0369a1, #22d3ee); }
.s8 { background: linear-gradient(135deg, #4d7c0f, #a3e635); }

.card h2 {
  font-size: 0.9375rem;
  margin: 0 0 4px;
  line-height: 1.3;
}

.card p {
  margin: 0;
  font-size: 0.8125rem;
  color: #64748b;
}

::view-transition-group(*) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

::view-transition-old(*):only-child {
  animation: fade-out 0.25s ease forwards;
}

::view-transition-new(*):only-child {
  animation: fade-in 0.3s ease 0.1s both;
}

@keyframes fade-out {
  to { opacity: 0; transform: scale(0.9); }
}

@keyframes fade-in {
  from { opacity: 0; transform: scale(0.9); }
}
const buttons = document.querySelectorAll(".filter");
const cards = document.querySelectorAll(".card");

cards.forEach(function(card) {
  card.style.viewTransitionName = card.id;
});

function applyFilter(type) {
  cards.forEach(function(card) {
    card.hidden = type !== "all" && card.dataset.type !== type;
  });
}

buttons.forEach(function(button) {
  button.addEventListener("click", function() {
    buttons.forEach(function(b) {
      b.classList.toggle("active", b === button);
    });
    const type = button.dataset.filter;
    if (!document.startViewTransition) {
      applyFilter(type);
      return;
    }
    document.startViewTransition(function() {
      applyFilter(type);
    });
  });
});
Esc
↑↓ moveEnter select

Start from a template

Open an .html file, or HTML, CSS, JS, Markdown, Less or TypeScript files. You can also drop them here. Styles and scripts go into their panes.

Share

Source

Keyboard shortcuts

Save