View Transition Types and Classes
A view transition with one or two named elements is easy to style: a selector per name, done. Real interfaces outgrow that quickly. A grid of twenty product cards needs twenty unique names, and writing ::view-transition-group(card-1), ::view-transition-group(card-2), … does not scale. And the same change often needs different motion depending on context — paging forward through a gallery should slide left, paging back should slide right. Two features address these problems: view-transition-class groups many named elements under one styling hook, and transition types tag a whole transition so its styles can vary. This page covers both, for same-document and cross-document transitions. It belongs to View Transitions for CSS Developers in the CSS-Only Micro-Interactions & Animations guide.
Names identify, classes style
Every participating element still needs a unique view-transition-name — that is how the browser pairs an element's old and new state. The class is an additional, non-unique label used only for styling the pseudo-elements. Many elements can share a class; one element can have several classes, separated by spaces.
The complete implementation: classes
A filterable card grid. Each card has a unique name generated from its ID and the shared class card; one set of rules animates all of them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>view-transition-class</title>
<style>
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(10rem, 1fr));
gap: 1rem;
}
.card {
view-transition-class: card;
padding: 1rem;
border-radius: 10px;
background: #eef2ff;
}
/* Unique names, one per card. */
.card[data-id="1"] { view-transition-name: card-1; }
.card[data-id="2"] { view-transition-name: card-2; }
.card[data-id="3"] { view-transition-name: card-3; }
/* One rule for every card group. */
::view-transition-group(*.card) {
animation-duration: 300ms;
animation-timing-function: cubic-bezier(0.2, 0, 0, 1);
}
/* Cards that appear or disappear scale rather than crossfade. */
::view-transition-new(*.card):only-child {
animation: card-in 250ms ease-out both;
}
::view-transition-old(*.card):only-child {
animation: card-out 200ms ease-in both;
}
@keyframes card-in { from { opacity: 0; scale: 0.85; } }
@keyframes card-out { to { opacity: 0; scale: 0.85; } }
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*.card) { animation-duration: 1ms; }
::view-transition-new(*.card):only-child,
::view-transition-old(*.card):only-child {
animation: none;
}
}
</style>
</head>
<body>
<button type="button" id="filter">Show only featured</button>
<div class="grid">
<article class="card" data-id="1">Lake</article>
<article class="card" data-id="2" data-featured>Ridge</article>
<article class="card" data-id="3">Pier</article>
</div>
<script>
document.getElementById('filter').addEventListener('click', () => {
const update = () => {
document.querySelectorAll('.card:not([data-featured])')
.forEach((card) => card.toggleAttribute('hidden'));
};
if (!document.startViewTransition) return update();
document.startViewTransition(update);
});
</script>
</body>
</html>
The :only-child trick is worth understanding. When a card exists in both states, its ::view-transition-image-pair holds both an old and a new image. When a card is removed, only the old image exists, so ::view-transition-old(*.card) is the pair's only child; when a card is added, only the new one. Matching :only-child therefore targets exactly the entering and leaving cards, and lets moving cards keep the default crossfade inside their morphing group. In a real app the per-card names come from the template, for example style="view-transition-name: card-{{ id }}", or from view-transition-name: match-element where it is supported.
Types: one change, different motion
A class says which elements are involved. A type says what kind of transition this is. Types are attached when the transition starts and matched with :active-view-transition-type() on the document root, so every pseudo-element rule can depend on them.
function goTo(index) {
const direction = index > current ? 'forward' : 'backward';
const update = () => { current = index; render(); };
if (!document.startViewTransition) return update();
document.startViewTransition({ update, types: [direction] });
}
html:active-view-transition-type(forward) {
&::view-transition-old(root) { animation: slide-to-left 280ms ease-in both; }
&::view-transition-new(root) { animation: slide-from-right 280ms ease-out both; }
}
html:active-view-transition-type(backward) {
&::view-transition-old(root) { animation: slide-to-right 280ms ease-in both; }
&::view-transition-new(root) { animation: slide-from-left 280ms ease-out both; }
}
@keyframes slide-to-left { to { translate: -30% 0; opacity: 0; } }
@keyframes slide-from-right { from { translate: 30% 0; opacity: 0; } }
@keyframes slide-to-right { to { translate: 30% 0; opacity: 0; } }
@keyframes slide-from-left { from { translate: -30% 0; opacity: 0; } }
Because the selector sits on html, types can also change non-pseudo-element styles for the duration of the transition — for example, disabling pointer events on a toolbar while a slide runs. The type is removed when the transition finishes.
The key technique: baseline first, types and classes on top
Write untyped, unclassed rules as the baseline — the default crossfade or a single set of durations — and add typed and classed rules as refinements. That ordering has two benefits. Browsers that support view transitions but not these newer selectors drop only the refinements and still animate sensibly. And transitions started without a type, such as those triggered by a library you do not control, get the baseline instead of no styling at all.
A second rule of thumb: keep types semantic. forward, backward, reload-data and open-detail describe intent and stay meaningful when the animation changes; slide-left describes a particular animation and becomes a lie the day the design switches to a fade.
Types across documents
For Cross-Document View Transitions, there is no startViewTransition call. Types come from two places: the types descriptor of the @view-transition rule, which applies the same types to every opted-in navigation, and the pageswap and pagereveal events, whose viewTransition object exposes a types set that script can modify. The usual pattern compares the old and new URLs in pagereveal — for instance, the index of two chapters — and adds forward or backward accordingly. The CSS is identical to the same-document version.
Choosing the right hook
Names, classes and types overlap enough to cause hesitation. A quick way to choose:
- Style one specific element — a site header, a hero image — with its name:
::view-transition-group(site-header). It is the most specific hook and reads clearly. - Style a family of elements — cards, list rows, avatars — with a class:
::view-transition-group(*.card). Add a second class for sub-families, such ascard featured, and target*.card.featuredfor the exceptions. - Vary the whole transition by intent — direction, a modal opening, a data refresh — with a type on the root. Types combine with names and classes:
html:active-view-transition-type(forward)::view-transition-group(*.card)styles cards only on forward transitions. - Change ordinary page styles during a transition — with a type selector on
htmlthat targets normal elements, since the type is active for exactly the transition's lifetime.
Debugging classes and types
When a classed rule does not apply, inspect the transition in Chromium's DevTools Animations panel, pause it, and select the pseudo-element in the Elements panel: the ::view-transition tree appears under html with each group's name. If the group is there but unstyled, the class was probably missing from the element when it was captured. Classes, like names, are read from computed style at capture, so a class added by script after the transition starts, or applied only on hover, never reaches the pseudo-element tree. Put view-transition-class in the same rule as the element's ordinary styles so it is always present. For types, log transition.types from the object returned by startViewTransition; an empty set means the options object was not passed or the browser predates types.
Common mistakes
- Forgetting the unique name. A class alone does nothing. An element with
view-transition-class: cardbut noview-transition-namedoes not participate in the transition at all. - Using the class selector without
*.::view-transition-group(.card)is not valid; the argument is a name followed by optional classes, and*stands for any name:::view-transition-group(*.card). - Leaving a type-specific animation without a reduced-motion override. Directional slides are exactly the motion reduced-motion users want removed. Wrap typed rules in
@media (prefers-reduced-motion: no-preference)or override them afterwards; View Transitions and Reduced Motion shows a substitution approach.
Browser support
Same-document view transitions and view-transition-name are supported in Chrome and Edge 111+, Firefox 144+ and Safari 18+. The cross-document @view-transition rule is supported in Chrome and Edge 126+ and Safari 18.2+, not in Firefox. view-transition-class, transition types and :active-view-transition-type() arrived after the core API, so older versions of supporting browsers run the transition but ignore these selectors; the baseline-first approach above keeps those browsers working. Browsers without view transitions at all apply the DOM change instantly through the feature check in the script.
FAQ
What is view-transition-class for?
It lets many elements with different view-transition-name values share one set of animation rules. Instead of writing a selector for card-1, card-2 and card-3, give them all view-transition-class: card and target ::view-transition-group(*.card).
How is a transition type different from a class?
A class describes which elements are involved; a type describes what kind of transition is happening, such as going forward or backward. Types are set when the transition starts and matched on the root with :active-view-transition-type().
How do I set a transition type?
For same-document transitions, pass types to document.startViewTransition({ update, types: ['forward'] }). For cross-document transitions, list them in the @view-transition rule's types descriptor or add them in a pagereveal or pageswap handler.
What happens if a browser does not support types or classes? Rules using the unsupported selectors are ignored, so the transition falls back to your untyped styles or the default crossfade. Write the untyped rules first as a sensible baseline and layer typed rules on top.
Related
- View Transitions for CSS Developers — the parent guide.
- view-transition-name and Shared Elements — the naming rules classes build on.
- Same-Document View Transitions — the startViewTransition basics.
- Cross-Document View Transitions — types in multi-page sites.
Related articles
More pages in the same section.