LayoutGroup
Group layout animations across components, and scope shared layout animations with an id.
Layout animations run when a layout namespace measures its elements before a change. Elements in other components, rendered through namespaces of their own, aren't measured. When their layout changes as a side effect, they jump.
LayoutGroup groups components that don't render together, but affect each other's layout. It also namespaces layoutIds, so a component with a shared layout animation can be rendered more than once.
Usage
Import
import { LayoutGroup } from "motion-sv"; Group layout animations
Take this sidebar, which handles its own state. Its namespace animates it when it opens and closes:
<!-- side-nav.svelte -->
<script lang="ts">
import { createLayoutMotion } from "motion-sv";
const links = ["Home", "Search", "Library"];
let isOpen = $state(false);
const layout = createLayoutMotion(() => isOpen);
const toggle = () => (isOpen = !isOpen);
</script>
<layout.nav class={["side-nav", { open: isOpen }]}>
<layout.button layout="position" onclick={toggle}>{isOpen ? "Close" : "Menu"}</layout.button>
{#each links as link (link)}
<layout.div layout="position">
<span class="icon"></span>
{#if isOpen}{link}{/if}
</layout.div>
{/each}
</layout.nav> Next to it, the page's content takes up the remaining width. It renders its elements through a namespace too, but it has no state of its own, so nothing tells it a change is coming:
<!-- content.svelte -->
<script lang="ts">
import { createLayoutMotion } from "motion-sv";
const layout = createLayoutMotion();
</script>
<layout.main class="content">
<layout.div class="line" />
<layout.div class="line" />
</layout.main> When the sidebar opens, the content has no way of knowing, so it jumps to its new size. Wrap both in a LayoutGroup, and when the sidebar's namespace measures its elements, the content's elements are measured too:
<script lang="ts">
import { LayoutGroup } from "motion-sv";
import Content from "./content.svelte";
import SideNav from "./side-nav.svelte";
</script>
<LayoutGroup>
<SideNav />
<Content />
</LayoutGroup> <script lang="ts">
import { LayoutGroup } from "motion-sv";
import Content from "./sidebar/content.svelte";
import SideNav from "./sidebar/side-nav.svelte";
</script>
<div class="window">
<LayoutGroup>
<SideNav />
<Content />
</LayoutGroup>
</div>
<style>
.window {
display: flex;
gap: 8px;
width: 460px;
height: 200px;
}
.window :global(.side-nav) {
display: flex;
flex-direction: column;
gap: 8px;
width: 80px;
padding: 10px;
background: rgb(153 17 255 / 0.15);
box-sizing: border-box;
overflow: hidden;
}
.window :global(.side-nav.open) {
width: 180px;
}
.window :global(.toggle) {
padding: 6px 0;
border: none;
border-radius: 8px;
background: #9911ff;
color: white;
font: inherit;
font-size: 14px;
cursor: pointer;
}
.window :global(.link) {
display: flex;
align-items: center;
gap: 10px;
height: 32px;
padding: 0 8px;
font-size: 14px;
white-space: nowrap;
}
.window :global(.icon) {
flex: none;
width: 16px;
height: 16px;
border-radius: 5px;
background: #9911ff;
}
.window :global(.content) {
display: flex;
flex: 1;
flex-direction: column;
gap: 12px;
padding: 20px;
background: rgb(255 62 0 / 0.15);
}
.window :global(.line) {
height: 14px;
background: rgb(255 62 0 / 0.5);
}
</style> When any layout element in a LayoutGroup is measured before a change, every layout element in the group is. That covers elements rendered through a layout namespace, and elements with a layoutId.
Namespace layoutId
Elements with the same layoutId perform shared layout animations with each other. layoutId is global across the page, so when a component that uses one is rendered more than once, its instances animate between each other.
For instance, each tab row here renders an underline with layoutId="underline":
<!-- tab-row.svelte -->
<script lang="ts">
import { motion } from "motion-sv";
let { tabs }: { tabs: string[] } = $props();
let selected = $state(0);
</script>
<nav>
{#each tabs as tab, index (tab)}
<button onclick={() => (selected = index)}>
{tab}
{#if index === selected}
<motion.div class="underline" layoutId="underline" />
{/if}
</button>
{/each}
</nav> With two tab rows on the page, selecting a tab in one row would animate the underline from the other row. Give each row's tabs a LayoutGroup with a unique id. It prefixes the layoutIds inside it, so each underline only animates within its own row:
<!-- tab-row.svelte -->
<script lang="ts">
import { LayoutGroup, motion } from "motion-sv";
let { id, tabs }: { id: string; tabs: string[] } = $props();
let selected = $state(0);
</script>
<LayoutGroup {id}>
<nav>
<!-- ... -->
</nav>
</LayoutGroup> <TabRow id="fruit" tabs={["Apple", "Banana", "Cherry"]} />
<TabRow id="vegetables" tabs={["Carrot", "Leek", "Pea"]} /> <script lang="ts">
import TabRow from "./tab-rows/tab-row.svelte";
</script>
<div class="rows">
<TabRow id="fruit" tabs={["Apple", "Banana", "Cherry"]} />
<TabRow id="vegetables" tabs={["Carrot", "Leek", "Pea"]} />
</div>
<style>
.rows {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
</style> Nested groups
A LayoutGroup inside another one inherits from it by default: its id is added to the parent's, and its elements are measured together with the parent's. Set inherit to choose what it inherits:
<LayoutGroup id="sidebar">
<!-- layoutIds are prefixed with "sidebar" and "tabs", and measured on their own -->
<LayoutGroup id="tabs" inherit="id">
<!-- ... -->
</LayoutGroup>
</LayoutGroup> Props
id
A string that prefixes the layoutId of every element inside the group, so they only perform shared layout animations with each other.
inherit
Default: true
What a nested LayoutGroup inherits from the LayoutGroup around it:
true: the parent'sid, which its ownidis added to, and its measurement group."id": only the parent'sid. Its elements are measured separately from the parent's."group": only the measurement group. Itsidisn't combined with the parent's.false: nothing. It works like a top-levelLayoutGroup.