Skip to content
On this page

Vue Contextual Transition

This module makes it easier to provide meaningful cross-browser transitions between pages — or other state changes if desired — for Vue projects. It provides a single opinionated transition that can animate between pages in two ways:

  1. Shared Element Transition: intended for navigating up and down a site's hierarchy, for example, from a blog index to a blog post:

(Example source)

An element can be designated for transitioning like this:

html
<template>
  <div>
    <img
      src="..."
      v-shared-element="{ id: post.slug, role: 'image', type: 'post' }"
    />
    <!--
      The above directive can be used on any descendent of the
      `<ContextualTransition>` -- any component or element, but see
      the "Limitations & Tips" page.
    -->
  </div>
</template>

TIP

This functionality should not be confused with the experimental Chrome feature, View Transitions developed along with this draft specification which was once called "shared element transitions." Nuxt has experimental support for View Transitions.

  1. Relative Slide Transition: intended for navigating laterally in a site's hierarchy, that is, between pages of like-content such as navigating from a current blog post to an older blog post.

Lorem Ipsum Dolor Sit Amet

Nisi deserunt exercitation elit officia labore adipisicing. Ex ea culpa in ullamco eu laboris Lorem sit magna quis officia.

(Example source)

A page view can be designated for transitioning like this:

html
<template>
  <div v-relative-slide="{ value: post.sortOrder, type: 'post' }">
    <!--
      This container is the view that will be transitioning, i.e.,
      the entering or exiting child of the
      `<ContextualTransition>`. The container's contents go here.
    -->
  </div>
</template>

In both cases, the animation is determined by simple directives which declare the relationships the page and certain elements on the page have with other pages.

Although we are differentiating these transitions, they are really two parts of the same transition triggered under different conditions. Typically both parts won't be triggered at the same time, but the same content may trigger either part depending on its relationship to the content the user is navigating to or from.

Source for the Demos

Note that, unlike these demos, the typical use of this module will be for transitioning RouterViews.

Source for Contextual Transition

vue
<template>
  <div class="simple-demo">
    <ContextualTransition group="demo">
      <div v-if="selectedPost === undefined" class="thumbnails">
        <a
          v-for="post in posts"
          :key="post.slug"
          @click="toggleExpanded(post.slug)"
        >
          <div
            v-shared-element="{
              id: post.slug,
              role: 'img',
            }"
            class="thumbnail"
            :style="`background-color: ${post.color};`"
          />
          <p
            v-shared-element="{
              id: post.slug,
              role: 'title',
            }"
          >
            {{ post.title }}
          </p>
        </a>
      </div>
      <div v-else class="post">
        <button class="close" @click="toggleExpanded">&times;</button>
        <div
          v-shared-element="{
            id: selectedPost.slug,
            role: 'img',
          }"
          class="header"
          :style="`background-color: ${selectedPost.color};`"
        />
        <h3
          v-shared-element="{
            id: selectedPost.slug,
            role: 'title',
          }"
        >
          {{ selectedPost.title }}
        </h3>
        <div class="text" v-html="selectedPost.content" />
      </div>
    </ContextualTransition>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import posts from '../data/sampleData';
const expanded = ref<string | false>(false);

function toggleExpanded(slug: string | false) {
  if (slug !== false) {
    expanded.value = slug;
  } else {
    expanded.value = false;
  }
}

const selectedPost = computed(() => {
  return posts.find((p) => p.slug === expanded.value);
});
</script>

<style></style>

Source for Slide Transition

vue
<template>
  <div class="simple-demo">
    <ContextualTransition group="demo2" :duration="500">
      <div
        :key="selectedPost.slug"
        v-relative-slide="{ value: selectedPost.index, type: 'posts' }"
        class="post"
      >
        <div
          class="header"
          :style="`background-color: ${selectedPost.color};`"
        />
        <h3>{{ selectedPost.title }}</h3>
        <div class="text" v-html="selectedPost.content" />
      </div>
    </ContextualTransition>
    <button v-if="selectedPost.index > 0" style="float: left" @click="previous">
      Previous
    </button>
    <button
      v-if="selectedPost.index < posts.length - 1"
      style="float: right"
      @click="next"
    >
      Next
    </button>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';
import posts from '../data/sampleData';
const expanded = ref<string>(posts[0].slug);

function setActive(slug: string) {
  expanded.value = slug;
}

function next() {
  const i = posts.findIndex((p) => p.slug === expanded.value);

  if (i >= 0 && i < posts.length - 1) {
    setActive(posts[i + 1].slug);
  }
}

function previous() {
  const i = posts.findIndex((p) => p.slug === expanded.value);

  if (i >= 1) {
    setActive(posts[i - 1].slug);
  }
}

const selectedPost = computed(() => {
  const index = posts.findIndex((p) => p.slug === expanded.value);
  if (index >= 0) {
    return {
      ...posts[index],
      index,
    };
  } else {
    return undefined;
  }
});
</script>

<style></style>