Skip to content

Animated Collection Updates

Use View Transitions API to animate collection changes: elements smoothly sliding to new positions when sorted, fading out when filtered away, and fading in when filtered back.

When to use

Use this technique for any collection of elements — table rows, card grids, list items, search results — where sorting or filtering controls change which items are visible or their order. The animations provide visual continuity, helping users track element movements and maintain context during data operations.

The pattern

Wrap DOM updates in document.startViewTransition() and assign unique view-transition-name values to each collection element based on stable IDs. The browser automatically captures before and after states, then animates position changes, exits, and entries.

Feature detection ensures graceful fallback for unsupported browsers:

javascript
function updateCollection(callback) {
  if (!document.startViewTransition) {
    callback();
    return;
  }

  document.startViewTransition(() => {
    callback();
  });
}

Assign unique view-transition-name values based on stable identifiers (database IDs, unique keys) — never array indices:

javascript
function renderItem(itemData) {
  const element = document.createElement("div");
  element.className = "collection-item";
  element.style.viewTransitionName = `item-${itemData.id}`;
  return element;
}

The browser provides three default animation behaviors:

ChangeAnimation
Element movesSlides to new position
Element removedFades out
Element addedFades in

Sorting table rows

When sorting tables, rows move to new positions with smooth sliding animations:

javascript
const tableData = [
  { id: 1, name: "Alice Johnson", revenue: 45000, date: "2024-03-15" },
  { id: 2, name: "Bob Smith", revenue: 67000, date: "2024-02-22" },
  { id: 3, name: "Carol White", revenue: 52000, date: "2024-01-10" },
];

let sortColumn = null;
let sortDirection = "asc";

function sortTable(column) {
  updateCollection(() => {
    sortColumn = column;
    sortDirection = sortDirection === "asc" ? "desc" : "asc";

    tableData.sort((a, b) => {
      const aVal = a[column];
      const bVal = b[column];
      const comparison = aVal < bVal ? -1 : aVal > bVal ? 1 : 0;
      return sortDirection === "asc" ? comparison : -comparison;
    });

    renderTable();
  });
}

function renderTable() {
  const tbody = document.querySelector("#data-table tbody");
  tbody.innerHTML = "";

  tableData.forEach(row => {
    const tr = document.createElement("tr");
    tr.style.viewTransitionName = `row-${row.id}`;

    tr.innerHTML = `
      <td>${row.name}</td>
      <td>$${row.revenue.toLocaleString()}</td>
      <td>${row.date}</td>
    `;

    tbody.appendChild(tr);
  });
}

Filtering a card grid

When filtering product cards, cards smoothly fade out (exits) or fade in (entries):

javascript
const products = [
  { id: 1, name: "Laptop", category: "electronics", price: 999 },
  { id: 2, name: "Desk Chair", category: "furniture", price: 299 },
  { id: 3, name: "Monitor", category: "electronics", price: 399 },
];

let categoryFilter = "all";

function filterProducts(category) {
  updateCollection(() => {
    categoryFilter = category;
    renderProducts();
  });
}

function renderProducts() {
  const grid = document.querySelector("#product-grid");
  grid.innerHTML = "";

  const filtered = products.filter(p =>
    categoryFilter === "all" || p.category === categoryFilter
  );

  filtered.forEach(product => {
    const card = document.createElement("div");
    card.className = "product-card";
    card.style.viewTransitionName = `product-${product.id}`;

    card.innerHTML = `
      <h3>${product.name}</h3>
      <p>${product.category}</p>
      <p class="price">$${product.price}</p>
    `;

    grid.appendChild(card);
  });
}

Reordering list items

When drag-and-drop reorders list items, they smoothly slide to new positions:

javascript
const tasks = [
  { id: 1, text: "Review pull requests", priority: 1 },
  { id: 2, text: "Update documentation", priority: 2 },
  { id: 3, text: "Fix bug #123", priority: 3 },
];

function reorderTasks(fromIndex, toIndex) {
  updateCollection(() => {
    const [movedTask] = tasks.splice(fromIndex, 1);
    tasks.splice(toIndex, 0, movedTask);
    renderTasks();
  });
}

function renderTasks() {
  const list = document.querySelector("#task-list");
  list.innerHTML = "";

  tasks.forEach(task => {
    const item = document.createElement("li");
    item.style.viewTransitionName = `task-${task.id}`;
    item.textContent = task.text;
    list.appendChild(item);
  });
}

Search results

When search results update, new results fade in while old ones fade out:

javascript
let searchResults = [];

async function performSearch(query) {
  const results = await fetchSearchResults(query);

  updateCollection(() => {
    searchResults = results;
    renderSearchResults();
  });
}

function renderSearchResults() {
  const container = document.querySelector("#search-results");
  container.innerHTML = "";

  searchResults.forEach(result => {
    const card = document.createElement("div");
    card.className = "result-card";
    card.style.viewTransitionName = `result-${result.id}`;

    card.innerHTML = `
      <h3>${result.title}</h3>
      <p>${result.description}</p>
    `;

    container.appendChild(card);
  });
}

CSS customization

Default animations can be customized using view transition pseudo-elements:

css
::view-transition-old(root),
::view-transition-new(root) {
  animation-duration: 0.3s;
}

Browser support

Chrome 111+, Edge 111+, Safari 18+. Not supported in Firefox as of January 2025. Always include feature detection with the fallback shown above.

Performance considerations

The browser limits the number of elements with view-transition-name to prevent performance issues. For collections with hundreds of items, consider:

  • Only animating visible items
  • Using virtualization techniques
  • Limiting transitions to specific user actions

Validation checklist

Verify each implementation covers these points:

  • Each element has a unique view-transition-name based on stable ID
  • Feature detection prevents errors in unsupported browsers
  • DOM updates wrapped in startViewTransition() callback
  • All three animation types work: position changes, exits, entries
  • Collection remains functional without animations in older browsers

What not to do

  • Don't reuse view-transition-name values across multiple elements simultaneously (causes transition to fail)
  • Don't assign view-transition-name based on array index (breaks when order changes)
  • Don't skip feature detection (causes errors in unsupported browsers)
  • Don't use for collections with thousands of items without considering performance implications
  • Don't forget that transitions are purely visual enhancements — the collection must remain functional without them

See sales-dashboard.html for a complete working example demonstrating sort, filter, and reset with view transitions.