Skip to content

Animation

Mirror parts hand you a transition name and leave the timing to your CSS. Read this for the names and for what stays in the DOM.

View source View as Markdown

Parts that appear and disappear hand you a Vue transition name, and all the timing lives in your CSS under that name, so a duration is never a prop.

.mirror-select-popup-enter-active,
.mirror-select-popup-leave-active {
  transition: opacity 120ms ease-out;
}

.mirror-select-popup-enter-from,
.mirror-select-popup-leave-to {
  opacity: 0;
}

The transition names

PartWhere it is setDefault
Checkbox.Indicatoroptions.transition on Checkbox.Rootmirror-checkbox-indicator
Radio.IndicatorThe transition prop on Radio.Indicatormirror-radio-indicator
Select.Contentoptions.transition.popup on Select.Rootmirror-select-popup
Select.Contentoptions.transition.backdrop on Select.Rootmirror-select-backdrop
Combobox.Contentoptions.transition.popup on Combobox.Rootmirror-combobox-popup
Combobox.Contentoptions.transition.backdrop on Combobox.Rootmirror-combobox-backdrop
Autocomplete.Contentoptions.transition.popup on Autocomplete.Rootmirror-autocomplete-popup
Autocomplete.Contentoptions.transition.backdrop on Autocomplete.Rootmirror-autocomplete-backdrop
Popover.Contentoptions.transition.popup on Popover.Rootnone
Popover.Contentoptions.transition.backdrop on Popover.Rootnone
Tooltip.Contentoptions.transition.popup on Tooltip.Rootmirror-tooltip-popup
Tooltip.Contentoptions.transition.backdrop on Tooltip.Rootmirror-tooltip-backdrop
PreviewCard.Contentoptions.transition.popup on PreviewCard.Rootnone
PreviewCard.Contentoptions.transition.backdrop on PreviewCard.Rootnone
Tabs.ContentThe transition prop on Tabs.Contentnone
ScrollArea.ScrollbarThe transition prop on ScrollArea.Scrollbarnone

Pass your own name to use your own classes.

<template>
  <Select.Root :options="{ transition: { popup: 'app-popup' } }">
    <!-- … -->
  </Select.Root>
</template>

Popover, PreviewCard, Tabs and ScrollArea ship no default name, so those popups appear and vanish, panels swap and scrollbars leave instantly until you pass one.

What unmounts

A part that is not present is removed from the DOM, so a checkbox indicator exists only while the box is checked or indeterminate, a select popup only while it is open, and Field.Error only while the field is invalid.

While they are mounted, popups and panels also write data-state as open or closed, so a CSS animation can read the state directly instead of going through a transition class.

.app-popup[data-state='closed'] {
  animation: fade-out 120ms ease-in;
}

Keeping a part mounted

forceMount keeps a part in the DOM permanently. Reach for it when something outside Mirror has to measure the node, or when your animation library owns the enter and leave itself.

<template>
  <Select.Content force-mount class="data-[state=closed]:pointer-events-none" />
</template>

Set options.forceMount on the root and it answers for every part under that root at once. On Select, Combobox and Autocomplete the option is keyed by part instead, so you write options.forceMount.popup rather than a plain boolean.

Where a part also takes a forceMount prop, the prop wins whenever you set it, false included, so one instance can stay unmounted under a root that keeps everything else. Leave the prop off and the option answers. A part that appears more than once under one root takes the prop alone, with no option behind it, and defaults to false: Field.Error, Radio.Indicator, Tabs.Content and ScrollArea.Scrollbar.

The parts that take it: Radio.Indicator, Field.Error, Select.Content, Combobox.Content, Autocomplete.Content, Popover.Content, Tooltip.Content, PreviewCard.Content, Dialog.Popup, Dialog.Backdrop, AlertDialog.Popup, AlertDialog.Backdrop, Collapsible.Content, Tabs.Content, ScrollArea.Scrollbar, Select.ItemIndicator and Combobox.ItemIndicator. Checkbox.Indicator is not among them: one root has one indicator, so options.forceMount on Checkbox.Root answers alone.

A kept panel also leaves the accessibility tree: an inactive kept panel is rendered with the hidden attribute, while a panel that is currently leaving is not, so its transition can finish.

<template>
  <Tabs.Content force-mount value="tokens">…</Tabs.Content>
</template>

Respect reduced motion

Timing lives in your CSS, so the media query does too. Write it next to the transition it switches off.

@media (prefers-reduced-motion: reduce) {
  .mirror-select-popup-enter-active,
  .mirror-select-popup-leave-active {
    transition: none;
  }
}

Further reading