<!-- Source: https://motion.svelte.page/examples/layout-dependency -->

# layoutDependency

> Gate layout measurement so FLIP only recomputes when a dependency changes.

**Source:** [https://motion.svelte.page/examples/layout-dependency](https://motion.svelte.page/examples/layout-dependency)

**Markdown mirror:** [https://motion.svelte.page/examples/layout-dependency.md](https://motion.svelte.page/examples/layout-dependency.md)

---

This mirror preserves the prose, implementation notes, and runnable Svelte source behind the live example page.

## FIG-001: frequent renders, gated measurement.

Both boxes have `layout` and re-render constantly (their color cycles). The left box re-measures on every render; the right box passes `layoutDependency={dep}`, so it only re-measures when `dep` changes — watch its counter stay flat.

**Metadata:** tag: `LAYOUT` | api: `layoutDependency` | input: `layout` | mode: `live`

### Notes

- Every render recolors both boxes — a stand-in for live text or streaming values that re-render a `layout` element often.
- The gated box passes `layoutDependency={'{dep}'}`, so its measure counter only moves when `dep` changes on Reflow.
- Both still FLIP-animate to their new position on Reflow — gating skips the wasted measurements, not the real ones.

### Source

#### Default.svelte

Source file: [src/lib/examples/layout-dependency/demos/Default.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/layout-dependency/demos/Default.svelte)

```svelte
<script lang="ts">
    import { Gauge, RotateCcw, Zap } from '@lucide/svelte'
    import { motion, styleString } from '@humanspeak/svelte-motion'

    // A high-frequency render driver: every tick recolors both boxes, forcing
    // a re-render. Without `layoutDependency`, that re-renders the layout box
    // AND re-measures it every single time.
    let tick = $state(0)
    let dep = $state(0)
    let shifted = $state(false)
    let defaultMeasures = $state(0)
    let gatedMeasures = $state(0)
    let auto = $state(false)

    const hue = $derived(tick % 360)

    // The hsl recolor is the render-driver visualization — every tick paints a
    // new hue, forcing the re-render that exercises layout measurement.
    const boxStyle = $derived(
        styleString(() => ({
            width: '84px',
            height: '84px',
            display: 'grid',
            placeItems: 'center',
            border: '1px solid var(--brut-ink, #0a0a0a)',
            background: `hsl(${hue} 85% 62%)`,
            color: 'var(--brut-ink, #0a0a0a)',
            fontFamily: 'var(--brut-mono, monospace)',
            fontSize: '22px',
            fontWeight: 700
        }))
    )

    function reflow() {
        shifted = !shifted
        tick += 1
        dep += 1
    }

    function reset() {
        defaultMeasures = 0
        gatedMeasures = 0
    }

    $effect(() => {
        if (!auto) return
        const id = setInterval(() => (tick += 1), 90)
        return () => clearInterval(id)
    })
</script>

<!-- dk-strip: docs-kit positioning shell - stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="toolbar" aria-label="layoutDependency controls">
        <button type="button" class:active={auto} onclick={() => (auto = !auto)}>
            <Zap size={15} />
            {auto ? 'Stop renders' : 'Start renders'}
        </button>
        <button type="button" class="primary" onclick={reflow}>
            <Gauge size={15} />
            Reflow + bump dep
        </button>
        <button type="button" onclick={reset}>
            <RotateCcw size={15} />
            Reset counters
        </button>
    </div>

    <div class="lanes" class:shifted>
        <section class="lane">
            <header>
                <span>// no gate</span>
                <strong>re-measures every render</strong>
            </header>
            <p class="count" data-state={defaultMeasures > 0 ? 'busy' : 'idle'}>
                {defaultMeasures} measures
            </p>
            <div class="track">
                <motion.div
                    class="box"
                    layout
                    style={boxStyle}
                    transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                    onProjectionUpdate={() => (defaultMeasures += 1)}
                >
                    A
                </motion.div>
            </div>
        </section>

        <section class="lane gated">
            <header>
                <span>// layoutDependency</span>
                <strong>measures only on dep change</strong>
            </header>
            <p class="count" data-state={gatedMeasures > 0 ? 'busy' : 'idle'}>
                {gatedMeasures} measures
            </p>
            <div class="track">
                <motion.div
                    class="box"
                    layout
                    layoutDependency={dep}
                    style={boxStyle}
                    transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
                    onProjectionUpdate={() => (gatedMeasures += 1)}
                >
                    B
                </motion.div>
            </div>
        </section>
    </div>
</div>

<style>
    .dk-demo-shell {
        width: 100%;
        height: clamp(520px, calc(100vh - 160px), 600px);
        display: grid;
        grid-template-rows: auto minmax(0, 1fr);
        gap: 16px;
        padding: 20px 26px;
        background: var(--brut-bg, #f8fcfb);
        color: var(--brut-ink, #0a0a0a);
    }

    .toolbar {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 8px;
    }

    button {
        height: 36px;
        display: inline-flex;
        align-items: center;
        gap: 7px;
        padding: 0 12px;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: var(--brut-bg-2, #eef4f1);
        color: var(--brut-ink, #0a0a0a);
        font-family: var(--brut-mono, monospace);
        font-size: 12px;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        cursor: pointer;
    }

    button.primary {
        border-color: var(--brut-accent, #247768);
        background: var(--brut-accent-soft, rgba(36, 119, 104, 0.1));
        color: var(--brut-accent, #247768);
    }

    button.active {
        border-color: var(--brut-ink, #0a0a0a);
        background: var(--brut-ink, #0a0a0a);
        color: var(--brut-accent-ink, #f8fcfb);
    }

    .lanes {
        display: grid;
        grid-template-columns: repeat(2, minmax(0, 1fr));
        gap: 14px;
        min-height: 0;
    }

    .lane {
        position: relative;
        display: grid;
        grid-template-rows: auto auto minmax(0, 1fr);
        gap: 12px;
        padding: 18px 22px;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: var(--brut-bg-2, #eef4f1);
        overflow: hidden;
    }

    .lane.gated {
        border-color: var(--brut-accent, #247768);
    }

    header {
        display: flex;
        align-items: baseline;
        justify-content: space-between;
        gap: 12px;
    }

    header span {
        color: var(--brut-accent, #247768);
        font-family: var(--brut-mono, monospace);
        font-size: 11px;
        font-weight: 700;
        letter-spacing: 0.1em;
        text-transform: uppercase;
    }

    header strong {
        color: var(--brut-ink, #0a0a0a);
        font-size: 15px;
        text-align: right;
    }

    .count {
        margin: 0;
        font-family: var(--brut-mono, monospace);
        font-size: 26px;
        font-weight: 700;
        font-variant-numeric: tabular-nums;
        color: var(--brut-ink-3, #9a9a9a);
    }

    .count[data-state='busy'] {
        color: var(--brut-accent, #247768);
    }

    .track {
        position: relative;
        display: flex;
        align-items: center;
        justify-content: flex-start;
        padding: 10px;
        border: 1px dashed var(--brut-rule-2, #bbc4c0);
        min-height: 0;
    }

    .shifted .track {
        justify-content: flex-end;
    }

    @media (max-width: 760px) {
        .lanes {
            grid-template-columns: 1fr;
        }
    }
</style>
```

## FIG-002: keyed list, what to gate on.

Rows are `layout="position"` in a keyed `{#each}` sorted by activity. Gated on a per-row field, only the bumped row animates and the rows it displaces jump — upstream parity. Gate on the row index and every moved row slides.

**Metadata:** tag: `LAYOUT` | api: `layoutDependency` | input: `keyed each` | mode: `live`

### Notes

- Bumping a row re-sorts the list. With `layoutDependency={'{row.ts}'}` only that row's dependency changed, so only it FLIPs — the displaced rows jump, exactly as upstream `MeasureLayout` behaves.
- Switch to `layoutDependency={'{i}'}`: the index changes for every row whose slot changed, so every moved row animates and untouched rows stay gated.
- Don't need the gate in a list? Omit `layoutDependency` and every reorder animates every row.

### Source

#### KeyedList.svelte

Source file: [src/lib/examples/layout-dependency/demos/KeyedList.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/layout-dependency/demos/KeyedList.svelte)

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

    // A keyed list sorted by "last activity". Bumping a row moves it to the
    // top and displaces every row above it.
    type Row = { id: string; label: string; ts: number }
    let rows = $state<Row[]>([
        { id: 'a', label: 'Design review', ts: 5 },
        { id: 'b', label: 'Sprint planning', ts: 4 },
        { id: 'c', label: 'Bug triage', ts: 3 },
        { id: 'd', label: 'Release notes', ts: 2 },
        { id: 'e', label: 'On-call handoff', ts: 1 }
    ])
    let nextTs = 10

    // 'field': layoutDependency={row.ts} — only the bumped row's dependency
    // changes, so upstream (and we) animate only that row; the displaced rows
    // jump. 'index': layoutDependency={i} — every row whose slot changed has a
    // new dependency and animates.
    let gate = $state<'field' | 'index'>('field')

    const sorted = $derived([...rows].sort((x, y) => y.ts - x.ts))

    function bump(id: string) {
        rows = rows.map((row) => (row.id === id ? { ...row, ts: nextTs++ } : row))
    }

    function reset() {
        rows = rows.map((row, i) => ({ ...row, ts: rows.length - i }))
        nextTs = 10
    }
</script>

<!-- dk-strip: docs-kit positioning shell - stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="toolbar" aria-label="keyed list controls">
        <button
            type="button"
            class="primary"
            class:active={gate === 'index'}
            onclick={() => (gate = gate === 'field' ? 'index' : 'field')}
        >
            <ListOrdered size={15} />
            gate: {gate === 'field' ? 'row.ts (field)' : 'i (index)'}
        </button>
        <button type="button" onclick={reset}>
            <RotateCcw size={15} />
            Reset order
        </button>
    </div>

    <ul class="rows">
        {#each sorted as row, i (row.id)}
            <motion.li
                class="row"
                layout="position"
                layoutDependency={gate === 'field' ? row.ts : i}
                transition={{ duration: 0.45, ease: [0.22, 1, 0.36, 1] }}
            >
                <span class="label">{row.label}</span>
                <span class="meta">ts {row.ts}</span>
                <button
                    type="button"
                    class="bump"
                    aria-label={`Bump ${row.label} to the top`}
                    onclick={() => bump(row.id)}
                >
                    <ArrowUpToLine size={14} />
                </button>
            </motion.li>
        {/each}
    </ul>

    <p class="hint">
        {#if gate === 'field'}
            <code>layoutDependency={'{row.ts}'}</code> — bump a row: it slides up, the rows it
            displaces
            <strong>jump</strong> (their dependency didn't change).
        {:else}
            <code>layoutDependency={'{i}'}</code> — bump a row: every row whose slot changed slides.
        {/if}
    </p>
</div>

<style>
    .dk-demo-shell {
        width: 100%;
        height: clamp(520px, calc(100vh - 160px), 600px);
        display: grid;
        grid-template-rows: auto minmax(0, 1fr) auto;
        gap: 16px;
        padding: 20px 26px;
        background: var(--brut-bg, #f8fcfb);
        color: var(--brut-ink, #0a0a0a);
    }

    .toolbar {
        display: flex;
        flex-wrap: wrap;
        justify-content: center;
        gap: 8px;
    }

    button {
        height: 36px;
        display: inline-flex;
        align-items: center;
        gap: 7px;
        padding: 0 12px;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: var(--brut-bg-2, #eef4f1);
        color: var(--brut-ink, #0a0a0a);
        font-family: var(--brut-mono, monospace);
        font-size: 12px;
        font-weight: 700;
        text-transform: uppercase;
        letter-spacing: 0.04em;
        cursor: pointer;
    }

    button.primary {
        border-color: var(--brut-accent, #247768);
        background: var(--brut-accent-soft, rgba(36, 119, 104, 0.1));
        color: var(--brut-accent, #247768);
    }

    button.active {
        border-color: var(--brut-ink, #0a0a0a);
        background: var(--brut-ink, #0a0a0a);
        color: var(--brut-accent-ink, #f8fcfb);
    }

    .rows {
        list-style: none;
        margin: 0 auto;
        padding: 0;
        width: min(460px, 100%);
        display: grid;
        align-content: start;
        gap: 8px;
        min-height: 0;
    }

    :global(.row) {
        display: grid;
        grid-template-columns: 1fr auto auto;
        align-items: center;
        gap: 12px;
        height: 52px;
        padding: 0 8px 0 16px;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: var(--brut-bg-2, #eef4f1);
    }

    .label {
        font-weight: 700;
        font-size: 14px;
    }

    .meta {
        font-family: var(--brut-mono, monospace);
        font-size: 11px;
        font-weight: 700;
        letter-spacing: 0.08em;
        text-transform: uppercase;
        color: var(--brut-accent, #247768);
    }

    button.bump {
        height: 32px;
        width: 32px;
        padding: 0;
        justify-content: center;
    }

    .hint {
        margin: 0;
        text-align: center;
        font-size: 13px;
        color: var(--brut-ink-2, #3a3a3a);
    }

    .hint code {
        font-family: var(--brut-mono, monospace);
        color: var(--brut-accent, #247768);
    }
</style>
```
