Appearance
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:
| Change | Animation |
|---|---|
| Element moves | Slides to new position |
| Element removed | Fades out |
| Element added | Fades 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-namebased 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-namevalues across multiple elements simultaneously (causes transition to fail) - Don't assign
view-transition-namebased 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.