Every state change the library makes is a React transition. That is what lets React keep old content on screen, and it is also what lets <ViewTransition> animate the change.
transition propEach part whose mount, unmount or content is driven by library state has a transition prop. When you pass it, the part renders a <ViewTransition default={transition}> as the outermost node of its output; without it, the part renders no boundary at all. The value has exactly React's type: "auto", "none", a View Transition class, or a map from transition type to class.
Treat transition as static. Changing it between undefined and a value remounts the part (development warns); switching between "none" and a class does not. Any value, "none" and a type map that does not match included, has a cost: when the part mounts or is revealed in a transition (a tab switch with keepMounted false or true, a part inside a dialog that opens), React starts a document view transition. The browser then cross-fades the whole page for 250 ms, and the next transition commit waits for it.
<Tabs.Panel value="billing" transition="fade">...</Tabs.Panel>
<Tabs.Panel
value="billing"
transition={{ "fsui-tabs-next": "slide-left", "fsui-tabs-prev": "slide-right", default: "none" }}
>
...
</Tabs.Panel>::view-transition-old(.slide-left) { animation: 160ms ease-in both slide-out-left; }
::view-transition-new(.slide-left) { animation: 160ms ease-out both slide-in-right; }
@media (prefers-reduced-motion: reduce) {
::view-transition-group(*),
::view-transition-old(*),
::view-transition-new(*) { animation: none !important; }
}The library ships no animation, so there is nothing to turn off for reduced motion; keep the media query above in your own stylesheet.
If you want only your own named animations, you can opt out of the page cross-fade with the CSS below. It is optional and applies to every view transition on the page, including your router's. The last rule keeps clicks working during a transition in Firefox, which otherwise suppresses hit testing.
::view-transition-old(root),
::view-transition-new(root) { animation: none; }
/* or: :root { view-transition-name: none; } */
::view-transition { pointer-events: none; }Each dispatch adds fsui-<component> and, where the change has a direction or kind, fsui-<component>-<change> through addTransitionType:
| Component | Types |
|---|---|
| Toggle | fsui-toggle, fsui-toggle-press, fsui-toggle-release |
| Tabs | fsui-tabs, fsui-tabs-next, fsui-tabs-prev (DOM order of the new tab relative to the committed one) |
| Accordion | fsui-accordion, fsui-accordion-expand, fsui-accordion-collapse |
| Tooltip | none (no transitions; see below) |
| Dialog | fsui-dialog, fsui-dialog-open, fsui-dialog-close |
| Switch | fsui-switch, fsui-switch-on, fsui-switch-off |
| Checkbox | fsui-checkbox, fsui-checkbox-check, fsui-checkbox-uncheck |
| RadioGroup | fsui-radio-group |
| CheckboxGroup | fsui-checkbox-group, fsui-checkbox-group-check, fsui-checkbox-group-uncheck (a reset can carry both) |
Use them in a transition map, or in CSS with :active-view-transition-type(fsui-tabs-next). Types you add yourself with addTransitionType in the synchronous part of an action join the same transition.
Types are attached only to the commit that ends a dispatched action. A rollback after a rejection, or a controlled parent snapping the value back, carries no fsui-* type: a type map needs a default entry if rollbacks should animate.
React starts a view transition only for commits made entirely of transition lanes. The optimistic flip is a synchronous commit, so it never animates through <ViewTransition>:
| The part renders | Animate with | Examples |
|---|---|---|
| optimistic state | a CSS transition on the data attribute | Toggle [data-pressed], Tabs.Tab [data-selected], Accordion.Trigger [data-open], Tabs.Indicator (anchor insets) |
| committed state | the transition prop | Tabs.Panel swaps, Accordion.Item / Panel in library mode |
| pending ending, a snap-back | the transition prop | a spinner rendered from pending disappearing |
| browser-owned state | CSS: @starting-style, transition-behavior: allow-discrete | Tooltip popup, Accordion in JS-free mode (::details-content) |
Tabs.Indicator follows the optimistic tab with CSS Anchor Positioning; a plain CSS transition on its inset properties makes it slide on click and slide back on rollback.
document.startViewTransition and never uses flushSync.<ViewTransition> renders no DOM and is safe in server-rendered trees; hydration is not a transition, so nothing animates on first paint.keepMounted) switch their <Activity> mode inside the library's transition, so nested <ViewTransition> boundaries get enter and exit animations.