Skip to main content
Vuetify0 is now in beta!
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

Presence

Animation-agnostic mount lifecycle for conditional content.

Usage

The Presence component and usePresence composable manage the mount lifecycle of conditional content. They provide a four-state machine that handles lazy mounting, exit animation delay, and unmounting.

v-ifPresence
Vanishes instantly
Animates out, then unmounts
<script setup lang="ts">
  import { Presence } from '@vuetify/v0'
  import { shallowRef } from 'vue'

  const show = shallowRef(true)
</script>

<template>
  <div class="flex flex-col items-center gap-6">
    <button
      class="rounded bg-primary px-4 py-2 text-on-primary"
      @click="show = !show"
    >
      {{ show ? 'Hide Both' : 'Show Both' }}
    </button>

    <table class="w-full border-separate border-spacing-2">
      <thead>
        <tr>
          <th class="text-left text-xs font-medium uppercase tracking-wide text-on-surface-variant">v-if</th>
          <th class="text-left text-xs font-medium uppercase tracking-wide text-on-surface-variant">Presence</th>
        </tr>
      </thead>

      <tbody>
        <tr>
          <td class="w-1/2 align-top">
            <div
              v-if="show"
              class="rounded-lg bg-surface-variant p-4 text-sm"
            >
              Vanishes instantly
            </div>
          </td>

          <td class="w-1/2 align-top">
            <Presence v-slot="{ attrs, done }" v-model="show" :immediate="false">
              <div
                v-bind="attrs"
                class="presence-box rounded-lg bg-surface-variant p-4 text-sm"
                @animationend="done"
              >
                Animates out, then unmounts
              </div>
            </Presence>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<style scoped>
  /* `mounted` is a transient tick before paint — hide so there's no flash */
  .presence-box[data-state="mounted"] {
    opacity: 0;
  }

  /* enter animation runs on the persistent `present` state */
  .presence-box[data-state="present"] {
    animation: fade-in 250ms ease-out;
  }

  .presence-box[data-state="leaving"] {
    animation: fade-out 200ms ease-in;
  }

  @keyframes fade-in {
    from { opacity: 0; transform: translateY(-8px); }
    to { opacity: 1; transform: translateY(0); }
  }

  @keyframes fade-out {
    from { opacity: 1; transform: translateY(0); }
    to { opacity: 0; transform: translateY(-8px); }
  }
</style>

Anatomy

vue
<script setup lang="ts">
  import { Presence } from '@vuetify/v0'
</script>

<template>
  <Presence />
</template>

Architecture

Presence wraps the usePresence composable, which implements a four-state machine:

Presence State Machine

Use controls to zoom and pan. Click outside or press Escape to close.

Presence State Machine
Statedata-stateIn DOM?Purpose
unmountedNoContent removed
mountedmountedYesJust entered DOM — target for enter animations
presentpresentYesActive and visible
leavingleavingYesExit animation running, waiting for done()

The mounted state lasts one tick, giving the browser a frame to apply initial styles before transitioning to present. This is the same principle behind requestAnimationFrame-based enter animations.

Examples

Animated Toast That Exits Cleanly

This toast mounts when you press Show, plays a CSS enter animation, and on dismiss plays an exit animation while staying in the DOM until animationend fires done() — only then does it unmount. Driving it is a single v-model boolean plus :immediate="false", which tells Presence to hold the leaving state until you signal the animation is over. The keyframes are selected entirely by the data-state attribute Presence writes through its slot attrs, so the markup carries no animation logic — CSS owns the motion, Presence owns the lifecycle.

The Lazy mount toggle flips the mount strategy. In eager mode every dismiss unmounts the toast and every show mounts a fresh instance, so the mount counter climbs on each open; in lazy mode the content mounts once on first show and hides via state on subsequent dismisses, so the counter stays at 1. Re-entry is the other behavior worth provoking: dismiss the toast and press Show again before the exit finishes, and Presence cancels the leave and returns to present without an unmount and remount cycle — the in-DOM element continues from wherever it was visually.

Reach for this pattern whenever conditional content needs an exit animation that v-if would cut short, or when expensive content should pay its setup cost once. The tradeoff of :immediate="false" is that you must call done() — forget it and the element is stranded in the leaving state forever; the safety valve is the default immediate mode, which auto-unmounts on the next tick when you do not need an exit animation. Presence is renderless and adds no DOM of its own, so accessibility lives in the content you render. For the composable form used inside custom setup functions, see usePresence; to render the toast in a top-level layer, wrap it with Portal.

FileRole
useToastDemo.tsOwns demo state — visibility, lazy mode, the mount counter — and restarts the demo when the strategy changes
AnimatedToast.vueWraps Presence, binds the data-state attrs to the toast element, and owns the enter and exit keyframes
animated-toast.vueWires the composable to the component and adds the Show button, Lazy toggle, and mount counter
Mounts: 0

Dismiss the toast, then press Show again before the exit animation finishes — Presence cancels the leave and the toast stays mounted. In lazy mode the content mounts once and hides via state, so the counter stays at 1.

Accessibility

Presence is transparent — it adds no DOM elements, ARIA attributes, or keyboard behavior. Accessibility is the responsibility of the content you render inside.

Tip

Ensure animated content respects prefers-reduced-motion. Presence doesn’t enforce motion preferences — your CSS should handle @media (prefers-reduced-motion: reduce).

FAQ

Discord
Need help? Join our community for support and discussions ↗

API Reference

The following API details are for all variations of the Presence component.
Was this page helpful?

Ctrl+/