Shared layout animation
An example of a shared layout animation, where two elements are animated from one to the other, using Motion for Vue's simple layoutId prop.
Source code
<script setup>
import { ref } from 'vue'
import { motion, AnimatePresence } from 'motion-v'
const allIngredients = [
{ icon: "🍅", label: "Tomato" },
{ icon: "🥬", label: "Lettuce" },
{ icon: "🧀", label: "Cheese" },
{ icon: "🥕", label: "Carrot" },
{ icon: "🍌", label: "Banana" },
{ icon: "🫐", label: "Blueberries" },
{ icon: "🥂", label: "Champers?" },
]
const [tomato, lettuce, cheese] = allIngredients
const tabs = [tomato, lettuce, cheese]
const selectedTab = ref(tabs[0])
</script>
<template>
<div class="container">
<nav class="nav">
<ul class="tabs-container">
<motion.li
v-for="item in tabs"
:key="item.label"
tag="li"
class="tab"
:initial="false"
:animate="{
backgroundColor: item.label === selectedTab.label ? '#eee' : '#eee0'
}"
@click="selectedTab = item"
>
{{ `${item.icon} ${item.label}` }}
<motion.div
v-if="item.label === selectedTab.label"
class="underline"
layout-id="underline"
id="underline"
/>
</motion.li>
</ul>
</nav>
<main class="icon-container">
<AnimatePresence mode="wait">
<motion.div
:key="selectedTab ? selectedTab.label : 'empty'"
:initial="{ y: 10, opacity: 0 }"
:animate="{ y: 0, opacity: 1 }"
:exit="{ y: -10, opacity: 0 }"
:transition="{ duration: 0.2 }"
class="icon"
>
{{ selectedTab ? selectedTab.icon : "😋" }}
</motion.div>
</AnimatePresence>
</main>
</div>
</template>
<style>
.container {
width: 480px;
height: 360px;
max-width: calc(100% - 40px);
max-height: calc(100% - 40px);
border-radius: 10px;
background: white;
overflow: hidden;
box-shadow:
0 1px 1px hsl(0deg 0% 0% / 0.075),
0 2px 2px hsl(0deg 0% 0% / 0.075),
0 4px 4px hsl(0deg 0% 0% / 0.075),
0 8px 8px hsl(0deg 0% 0% / 0.075),
0 16px 16px hsl(0deg 0% 0% / 0.075),
0 2px 2px hsl(0deg 0% 0% / 0.075),
0 4px 4px hsl(0deg 0% 0% / 0.075),
0 8px 8px hsl(0deg 0% 0% / 0.075),
0 16px 16px hsl(0deg 0% 0% / 0.075);
display: flex;
flex-direction: column;
}
.nav {
background: #fdfdfd;
padding: 5px 5px 0;
border-radius: 10px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom: 1px solid #eeeeee;
height: 44px;
}
.tabs-container {
list-style: none;
padding: 0;
margin: 0;
font-weight: 500;
font-size: 14px;
display: flex;
width: 100%;
}
.tab {
list-style: none;
padding: 0;
margin: 0;
font-weight: 500;
font-size: 14px;
border-radius: 5px;
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
width: 100%;
padding: 10px 15px;
position: relative;
background: white;
cursor: pointer;
height: 24px;
display: flex;
justify-content: space-between;
align-items: center;
flex: 1;
min-width: 0;
user-select: none;
color: #0f1115;
}
.underline {
position: absolute;
bottom: -2px;
left: 0;
right: 0;
height: 2px;
background: var(--accent);
}
.icon-container {
display: flex;
justify-content: center;
align-items: center;
flex: 1;
}
.icon {
font-size: 128px;
}
</style>Related examples
Latest in Vue
Motion+
Unlock all 400+ examples
- Source code for every Plus example.
- Provide examples direct to your agent via Motion's MCP.
- Lifetime access to new examples and APIs.








