<!-- Source: https://motion.svelte.page/docs/layout-animations -->

# Layout Animations

> Animate elements between positions with layout and layoutId for smooth shared layout transitions

**Source:** [https://motion.svelte.page/docs/layout-animations](https://motion.svelte.page/docs/layout-animations)

---

Svelte Motion supports two kinds of layout animation:

- **`layout`** — animate a single element when its own size or position changes (FLIP).
- **`layoutId`** — animate between two different elements that share the same identifier. When one unmounts and another with the same `layoutId` mounts, the new element FLIP-animates from the old one's position.

## Single-element layout animation

Add `layout` to any motion element to have it automatically animate when its position or size changes in the DOM.

```svelte
<motion.div layout>
    <!-- This element will FLIP-animate whenever its bounding rect changes -->
</motion.div>
```

Use `layout="position"` to only animate translation (no scale):

```svelte
<motion.div layout="position">
    <!-- Translates smoothly, does not scale -->
</motion.div>
```

## Shared layout with `layoutId`

The `layoutId` prop enables shared layout animations between completely different elements. When element A with `layoutId="foo"` unmounts and element B with the same `layoutId="foo"` mounts, B automatically animates from A's last known position.

### Tab underline example

The most common use case is a tab indicator that slides between tabs:

```svelte
<script lang="ts">
    import { AnimatePresence, motion } from '@humanspeak/svelte-motion'

    let selectedTab = $state(0)
    const tabs = ['Home', 'About', 'Contact']
</script>

{#each tabs as tab, i (tab)}
    <button onclick={() => (selectedTab = i)}>
        {tab}
        <AnimatePresence>
            {#if selectedTab === i}
                <motion.div
                    key="underline"
                    layoutId="underline"
                    style="position: absolute; bottom: 0; left: 0; right: 0; height: 2px; background: royalblue;"
                    transition={{ type: 'spring', stiffness: 500, damping: 30 }}
                />
            {/if}
        </AnimatePresence>
    </button>
{/each}
```

### How it works

1. Only one element with a given `layoutId` should be mounted at a time (use conditional rendering).
2. When the old element unmounts, its bounding rect is captured.
3. When the new element mounts, it reads the stored rect and performs a FLIP animation from the old position to its own natural position.
4. The animation uses the element's `transition` prop, or falls back to the previous element's transition.

### Full interactive example

> Live example: [/examples/shared-layout-animation](https://motion.svelte.page/examples/shared-layout-animation)

## Inside scroll containers — `layoutScroll`

By default, FLIP measures elements using `getBoundingClientRect()`, which returns viewport-relative coordinates. If a `motion.div` with `layout` lives inside an `overflow: scroll` container and the user scrolls during the animation, the scroll offset shows up as an unwanted translate — the box visibly drifts.

The fix is to mark the scroll container with `layoutScroll`. Descendant `layout` animations then measure rects in that container's coordinate space, so a mid-animation scroll cancels out instead of leaking into the FLIP delta.

```svelte
<motion.div layoutScroll style="overflow: auto; height: 320px;">
    <motion.div layout>Resize me — scroll the container mid-animation</motion.div>
</motion.div>
```

Apply `layoutScroll` on the same `motion.*` element that owns the scrolling — the same element you'd set `overflow: scroll` (or `auto`) on. Nested `layoutScroll` containers stack: a `layout` descendant accounts for the scroll offset of every `layoutScroll` ancestor in its path, so you can wrap a scrollable list inside a scrollable panel and both contribute to the FLIP measurement.

| Prop | Type | Description |
|---|---|---|
| `layoutScroll` | `boolean` | Mark this element as a scroll container so descendant `layout` measurements account for its scroll offset. Nested `layoutScroll` ancestors stack. |

## Scoping shared animations — `<LayoutGroup>`

If two regions of your UI reuse the same `layoutId` values (think two tab strips both using `layoutId="underline"`, or several Kanban columns each with a `layoutId="indicator"`), they'd cross-animate by default — the global registry doesn't know which instance is which, and an unmount in one region can be picked up by a mount in another.

Wrap each region in `<LayoutGroup id="…">` to scope the registry to that subtree:

```svelte
<script>
    import { AnimatePresence, LayoutGroup, motion } from '@humanspeak/svelte-motion'
</script>

<LayoutGroup id="strip-a">
    <Tabs />
</LayoutGroup>

<LayoutGroup id="strip-b">
    <Tabs />
    <!-- same layoutId values inside — but independent of strip-a -->
</LayoutGroup>
```

Nested groups chain by default (`inherit={true}`), so a `<LayoutGroup id="inner">` inside a `<LayoutGroup id="outer">` yields an effective id of `"outer-inner"`. Pass `inherit={false}` to start a fresh scope that ignores any surrounding group — useful for embedded widgets that should not be affected by ambient grouping.

| Prop | Type | Description |
|---|---|---|
| `id` | `string?` | Stable identifier for this group. Combined with any surrounding LayoutGroup's id when `inherit` is `true`. |
| `inherit` | `boolean \| 'id'` (default `true`) | When `true` (or `'id'`), chain onto the parent group's id. When `false`, ignore any outer group and start a fresh scope. `'id'` is accepted for drop-in compatibility with framer-motion examples; it behaves the same as `true` here. |

## API reference

### `layout`

| Value | Behavior |
|---|---|
| `true` | Animate translate + scale on layout change |
| `"position"` | Animate translate only (no scale) |
| `false` / omitted | No layout animation |

### `layoutId`

| Prop | Type | Description |
|---|---|---|
| `layoutId` | `string` | Shared identifier. Elements with matching `layoutId` animate between each other's positions. |

`layoutId` works best inside `AnimatePresence`, which provides the registry that coordinates the snapshot/consume handoff.

## Related

- [AnimatePresence](/docs/animate-presence) — Coordinate enter/exit animations
- [Shared Layout Animation example](/examples/shared-layout-animation) — Full interactive demo
- [LayoutGroup example](/examples/layout-group) — Two sibling tab strips reusing the same `layoutId` without crossing over
