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

# arc()

> Curve keyframes and layout animations with transition.path, including tangent-following rotation.

**Source:** [https://motion.svelte.page/examples/arc](https://motion.svelte.page/examples/arc)

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

---

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

## FIG-001: straight endpoints, curved travel.

Click the stage to send the box between fixed endpoints. Strength controls the bend while direction can stay automatic or lock clockwise/counter-clockwise.

**Metadata:** tag: `PATH` | api: `arc()` | input: `transition.path` | mode: `live`

### Notes

- `arc()` replaces straight x/y interpolation with a quadratic bend.
- Auto direction remembers a stable screen-space side across reversals.
- The endpoints stay exact; only the route between them changes.

### Source

#### Default.svelte

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

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

    let right = $state(false)
    let strength = $state(0.5)
    let direction = $state<'cw' | 'ccw' | undefined>(undefined)

    // arc() keeps auto-direction continuity state, so recreate the path only
    // when an option changes instead of evaluating a fresh path per render.
    const transition = $derived({
        duration: 0.9,
        ease: 'easeInOut' as const,
        path: arc({ strength, direction })
    })

    const directions = [undefined, 'cw', 'ccw'] as const
    const cycleDirection = () => {
        const index = directions.indexOf(direction)
        direction = directions[(index + 1) % directions.length]
    }
</script>

<!-- dk-strip: docs-kit positioning shell — stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="strip">
        <header>
            <span class="micro">// transition.path</span>
            <span class="micro readout">strength {strength} · {direction ?? 'auto'}</span>
        </header>

        <button
            class="stage"
            type="button"
            aria-label="Move the box along an arc"
            onclick={() => (right = !right)}
        >
            <motion.div
                class="box"
                initial={{ x: 0, y: 0 }}
                animate={{ x: right ? 260 : 0, y: 0 }}
                {transition}
            />
        </button>

        <div class="controls">
            <span class="micro">strength</span>
            {#each [0.25, 0.5, 1] as value (value)}
                <button
                    type="button"
                    class:active={strength === value}
                    onclick={() => (strength = value)}
                >
                    {value}
                </button>
            {/each}
            <button type="button" class="direction" onclick={cycleDirection}>
                direction: {direction ?? 'auto'}
            </button>
        </div>

        <footer>
            <span class="micro">click the stage</span>
            <span class="micro">quadratic arc</span>
        </footer>
    </div>
</div>

<style>
    .dk-demo-shell {
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 310px;
        padding: 1.5rem;
    }

    .strip {
        display: flex;
        width: 100%;
        max-width: 390px;
        flex-direction: column;
        gap: 0.75rem;
    }

    header,
    footer,
    .controls {
        display: flex;
        align-items: center;
        gap: 0.5rem;
    }

    header,
    footer {
        justify-content: space-between;
        border-bottom: 1px dashed var(--brut-rule-2, #bbc4c0);
        padding-bottom: 0.5rem;
    }

    footer {
        border-top: 1px dashed var(--brut-rule-2, #bbc4c0);
        border-bottom: 0;
        padding-top: 0.5rem;
        padding-bottom: 0;
    }

    .micro {
        color: var(--brut-ink-3, #6f7975);
        font-family: var(--brut-mono, monospace);
        font-size: 0.6875rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
    }

    .readout {
        color: var(--brut-accent, #247768);
        font-variant-numeric: tabular-nums;
        text-transform: none;
    }

    .stage {
        position: relative;
        height: 180px;
        border: 1px solid var(--brut-ink, #0a0a0a);
        background:
            linear-gradient(
                90deg,
                transparent 49.8%,
                var(--brut-rule, #d6dedb) 50%,
                transparent 50.2%
            ),
            var(--brut-bg-2, #eef4f1);
        box-shadow: 6px 6px 0 var(--brut-rule, #d6dedb);
        cursor: pointer;
    }

    :global(.box) {
        position: absolute;
        top: 66px;
        left: 24px;
        width: 48px;
        height: 48px;
        border: 1px solid var(--brut-ink, #0a0a0a);
        background: var(--brut-accent, #247768);
    }

    .controls {
        flex-wrap: wrap;
    }

    .controls button {
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        padding: 0.3rem 0.55rem;
        background: var(--brut-bg, #fff);
        color: var(--brut-ink, #0a0a0a);
        font-family: var(--brut-mono, monospace);
        font-size: 0.7rem;
        cursor: pointer;
    }

    .controls button.active {
        border-color: var(--brut-accent, #247768);
        background: var(--brut-accent, #247768);
        color: white;
    }

    .controls .direction {
        margin-left: auto;
    }
</style>
```

## FIG-002: path tangent, visible heading.

The arrow follows the curve and rotates with its tangent. Path rotation uses a separate channel, so authored rotate values stay additive.

**Metadata:** tag: `ROTATE` | api: `arc()` | input: `transition.path` | mode: `live`

### Notes

- `rotate: true` follows the full path tangent.
- A value from `0` to `1` scales the tangent rotation.
- Path rotation is additive to the element's own `rotate` channel.

### Source

#### Rotate.svelte

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

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

    let right = $state(false)
    const path = arc({ strength: 0.8, rotate: true })
    const transition = { duration: 1, ease: 'easeInOut' as const, path }
</script>

<!-- dk-strip: docs-kit positioning shell — stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="strip">
        <header>
            <span class="micro">// tangent following</span>
            <span class="micro readout">rotate: true</span>
        </header>

        <button
            class="stage"
            type="button"
            aria-label="Move the arrow along an arc"
            onclick={() => (right = !right)}
        >
            <motion.div
                class="arrow"
                initial={{ x: 0, y: 0 }}
                animate={{ x: right ? 260 : 0, y: 0 }}
                {transition}
            >
                ➜
            </motion.div>
        </button>

        <footer>
            <span class="micro">click the stage</span>
            <span class="micro">pathRotation + rotate</span>
        </footer>
    </div>
</div>

<style>
    .dk-demo-shell {
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 280px;
        padding: 1.5rem;
    }

    .strip {
        display: flex;
        width: 100%;
        max-width: 390px;
        flex-direction: column;
        gap: 0.75rem;
    }

    header,
    footer {
        display: flex;
        align-items: center;
        justify-content: space-between;
        border-bottom: 1px dashed var(--brut-rule-2, #bbc4c0);
        padding-bottom: 0.5rem;
    }

    footer {
        border-top: 1px dashed var(--brut-rule-2, #bbc4c0);
        border-bottom: 0;
        padding-top: 0.5rem;
        padding-bottom: 0;
    }

    .micro {
        color: var(--brut-ink-3, #6f7975);
        font-family: var(--brut-mono, monospace);
        font-size: 0.6875rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
    }

    .readout {
        color: var(--brut-accent, #247768);
        text-transform: none;
    }

    .stage {
        position: relative;
        height: 180px;
        border: 1px solid var(--brut-ink, #0a0a0a);
        background: var(--brut-bg-2, #eef4f1);
        box-shadow: 6px 6px 0 var(--brut-rule, #d6dedb);
        cursor: pointer;
    }

    :global(.arrow) {
        position: absolute;
        top: 62px;
        left: 24px;
        display: grid;
        width: 52px;
        height: 52px;
        place-items: center;
        border: 1px solid var(--brut-ink, #0a0a0a);
        background: var(--brut-accent, #247768);
        color: white;
        font-size: 2rem;
        line-height: 1;
    }
</style>
```

## FIG-003: shared element, curved FLIP.

A single layoutId bubble swaps between two slots. transition.layout.path bends the projection animation without changing the destination layout.

**Metadata:** tag: `LAYOUT` | api: `arc()` | input: `transition.layout.path` | mode: `live`

### Notes

- The same path works for shared `layoutId` transitions.
- Place it at `transition.layout.path` to configure FLIP motion.
- Layout shifts under 20px stay straight to avoid a noisy detour.

### Source

#### Layout.svelte

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

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

    let right = $state(false)
    const path = arc({ strength: 0.5 })
    const transition = { layout: { duration: 0.8, ease: 'easeInOut' as const, path } }
</script>

<!-- dk-strip: docs-kit positioning shell — stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="strip">
        <header>
            <span class="micro">// shared layout</span>
            <span class="micro readout">layoutId="arc-bubble"</span>
        </header>

        <button
            class="stage"
            type="button"
            aria-label="Move the shared bubble"
            onclick={() => (right = !right)}
        >
            <span class="slot left">
                {#if !right}
                    <motion.span layoutId="arc-bubble" class="bubble" {transition} />
                {/if}
            </span>
            <span class="slot right">
                {#if right}
                    <motion.span layoutId="arc-bubble" class="bubble" {transition} />
                {/if}
            </span>
        </button>

        <footer>
            <span class="micro">click to swap slots</span>
            <span class="micro">transition.layout.path</span>
        </footer>
    </div>
</div>

<style>
    .dk-demo-shell {
        display: flex;
        align-items: center;
        justify-content: center;
        min-height: 280px;
        padding: 1.5rem;
    }

    .strip {
        display: flex;
        width: 100%;
        max-width: 390px;
        flex-direction: column;
        gap: 0.75rem;
    }

    header,
    footer {
        display: flex;
        align-items: center;
        justify-content: space-between;
        border-bottom: 1px dashed var(--brut-rule-2, #bbc4c0);
        padding-bottom: 0.5rem;
    }

    footer {
        border-top: 1px dashed var(--brut-rule-2, #bbc4c0);
        border-bottom: 0;
        padding-top: 0.5rem;
        padding-bottom: 0;
    }

    .micro {
        color: var(--brut-ink-3, #6f7975);
        font-family: var(--brut-mono, monospace);
        font-size: 0.6875rem;
        letter-spacing: 0.08em;
        text-transform: uppercase;
    }

    .readout {
        color: var(--brut-accent, #247768);
        text-transform: none;
    }

    .stage {
        display: grid;
        height: 200px;
        grid-template-columns: 1fr 1fr;
        gap: 120px;
        border: 1px solid var(--brut-ink, #0a0a0a);
        padding: 20px;
        background: var(--brut-bg-2, #eef4f1);
        box-shadow: 6px 6px 0 var(--brut-rule, #d6dedb);
        cursor: pointer;
    }

    .slot {
        display: flex;
        align-items: center;
        border: 1px dashed var(--brut-rule-2, #bbc4c0);
        padding: 8px;
    }

    .slot.left {
        justify-content: flex-start;
    }

    .slot.right {
        justify-content: flex-end;
    }

    :global(.bubble) {
        display: block;
        width: 52px;
        height: 52px;
        border: 1px solid var(--brut-ink, #0a0a0a);
        border-radius: 999px;
        background: var(--brut-accent, #247768);
    }
</style>
```
