<!-- Source: https://motion.svelte.page/examples/multi-state-badge -->

# Multi-State Badge

> A badge component that cycles through idle, processing, success, and error states with smooth animated transitions in Svelte Motion.

**Source:** [https://motion.svelte.page/examples/multi-state-badge](https://motion.svelte.page/examples/multi-state-badge)

**Markdown mirror:** [https://motion.svelte.page/examples/multi-state-badge.md](https://motion.svelte.page/examples/multi-state-badge.md)

---

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

## FIG-001: multi-state badge.

A badge that cycles `idle → processing → success → error → idle`. Click to advance. Each state swap exits and enters its icon + label inside `AnimatePresence`, with blur + scale carrying the transitions.

**Metadata:** tag: `ANIMATE-PRESENCE` | pattern: `state-cycle`

### Notes

- The badge holds an enum state (`idle`, `processing`, `success`, `error`) — clicking advances to the next via `getNextState()`. State change drives every animation downstream.
- Icon + label both live inside `AnimatePresence` keyed by state. Exit blurs and shrinks the outgoing content; enter blurs and grows the incoming content. Layout stays put because the badge chrome wraps the presence.
- The composition lives in `$lib/examples/multi-state-badge/Badge.svelte` alongside icon helpers. The demo file is a thin wrapper that owns the state cycle — the animation lives in the badge components.

### Source

#### Default.svelte

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

```svelte
<script lang="ts">
    import { styleString } from '@humanspeak/svelte-motion'
    import Badge from '../Badge.svelte'
    import { getNextState, styles, type BadgeState } from '../constants'

    // Badge that cycles idle → processing → success → error → idle.
    // Each state swaps an icon and a label inside `AnimatePresence` —
    // exits and enters share a layout so the chrome stays put while
    // the contents fly through with blur + scale.
    // Ported from: https://examples.motion.dev/react/multi-state-badge
    //
    // The badge component (its #f5f5f5 pill, 999 radius, AnimatePresence
    // modes) is intentionally unchanged — only the surrounding docs shell
    // wears the brutalist strip chrome.

    let badgeState = $state<BadgeState>('idle')
</script>

<!-- dk-strip: docs-kit positioning shell — stripped from the published code. -->
<div class="dk-demo-shell">
    <div class="strip">
        <div class="strip-head">
            <span class="micro">// multi-state badge</span>
            <span class="micro state">state: {badgeState}</span>
        </div>

        <div class="stage">
            <div style={styleString(() => styles.container)}>
                <button
                    onclick={() => {
                        badgeState = getNextState(badgeState)
                    }}
                    style="background: none; border: none; cursor: pointer; padding: 0;"
                >
                    <Badge state={badgeState} />
                </button>
            </div>
        </div>

        <div class="strip-foot">
            <span class="micro">click badge to advance</span>
            <span class="micro">4 states / animate-presence</span>
        </div>
    </div>
</div>

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

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

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

    .state {
        color: var(--brut-accent, #247768);
    }

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

    .strip-foot {
        border-bottom: none;
        border-top: 1px dashed var(--brut-rule-2, #bbc4c0);
        padding-top: 0.75rem;
        padding-bottom: 0;
    }

    .stage {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 1rem;
        border: 1px solid var(--brut-rule-2, #bbc4c0);
        background: var(--brut-bg-2, #eef4f1);
        box-shadow: 6px 6px 0 var(--brut-rule, #d6dedb);
    }
</style>
```

#### Badge.svelte

Source file: [src/lib/examples/multi-state-badge/Badge.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/multi-state-badge/Badge.svelte)

```svelte
<script lang="ts">
    import { motion, styleString } from '@humanspeak/svelte-motion'
    import { animate } from 'motion'
    import { styles, type BadgeState } from './constants'
    import Icon from './Icon.svelte'
    import Label from './Label.svelte'

    type Props = {
        state: BadgeState
    }

    const { state: badgeState }: Props = $props()

    let badgeElement: HTMLElement | null = $state(null)

    // Trigger shake/scale animations on state change
    $effect(() => {
        if (!badgeElement) return

        if (badgeState === 'error') {
            animate(
                badgeElement,
                { x: [0, -6, 6, -6, 0] },
                {
                    duration: 0.3,
                    ease: 'easeInOut',
                    times: [0, 0.25, 0.5, 0.75, 1],
                    repeat: 0,
                    delay: 0.1
                }
            )
        } else if (badgeState === 'success') {
            animate(
                badgeElement,
                { scale: [1, 1.2, 1] },
                {
                    duration: 0.3,
                    ease: 'easeInOut',
                    times: [0, 0.5, 1],
                    repeat: 0
                }
            )
        }
    })
</script>

<motion.div
    bind:ref={badgeElement}
    style={styleString(() => ({
        ...styles.badge,
        gap: badgeState === 'idle' ? 0 : 8
    }))}
>
    <Icon state={badgeState} />
    <Label {badgeState} />
</motion.div>
```

#### Icon.svelte

Source file: [src/lib/examples/multi-state-badge/Icon.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/multi-state-badge/Icon.svelte)

```svelte
<script lang="ts">
    import { motion, AnimatePresence, styleString } from '@humanspeak/svelte-motion'
    import { SPRING_CONFIG, styles, type BadgeState } from './constants'
    import Check from './Check.svelte'
    import Loader from './Loader.svelte'
    import XIcon from './XIcon.svelte'

    type Props = {
        state: BadgeState
    }

    const { state: badgeState }: Props = $props()
</script>

<motion.span
    style={styleString(() => styles.iconContainer)}
    animate={{
        width: badgeState === 'idle' ? 0 : 20
    }}
    transition={SPRING_CONFIG}
>
    <AnimatePresence>
        <motion.span
            key={badgeState}
            data-debug="icon-motion"
            style={styleString(() => styles.icon)}
            initial={{
                y: -40,
                scale: 0.5,
                filter: 'blur(6px)'
            }}
            animate={{
                y: 0,
                scale: 1,
                filter: 'blur(0px)'
            }}
            exit={{
                y: 40,
                scale: 0.5,
                filter: 'blur(6px)'
            }}
            transition={{
                duration: 0.15,
                ease: 'easeInOut'
            }}
        >
            {#if badgeState === 'processing'}
                <Loader />
            {:else if badgeState === 'success'}
                <Check />
            {:else if badgeState === 'error'}
                <XIcon />
            {/if}
        </motion.span>
    </AnimatePresence>
</motion.span>
```

#### Check.svelte

Source file: [src/lib/examples/multi-state-badge/Check.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/multi-state-badge/Check.svelte)

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

<motion.svg {...svgProps}>
    <motion.polyline points="4 12 9 17 20 6" {...animations} />
</motion.svg>
```

#### Loader.svelte

Source file: [src/lib/examples/multi-state-badge/Loader.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/multi-state-badge/Loader.svelte)

```svelte
<script lang="ts">
    import { motion, useTime, useTransform, styleString } from '@humanspeak/svelte-motion'
    import { ICON_SIZE, svgProps, animations } from './constants'

    const time = useTime()
    const rotate = useTransform(time, [0, 1000], [0, 360], { clamp: false })
</script>

<motion.div
    style={styleString(() => ({
        rotate: $rotate,
        display: 'flex',
        alignItems: 'center',
        justifyContent: 'center',
        width: ICON_SIZE,
        height: ICON_SIZE
    }))}
>
    <motion.svg {...svgProps}>
        <motion.path d="M21 12a9 9 0 1 1-6.219-8.56" {...animations} />
    </motion.svg>
</motion.div>
```

#### XIcon.svelte

Source file: [src/lib/examples/multi-state-badge/XIcon.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/multi-state-badge/XIcon.svelte)

```svelte
<script lang="ts">
    import { motion } from '@humanspeak/svelte-motion'
    import { svgProps, animations, secondLineAnimation } from './constants'
</script>

<motion.svg {...svgProps}>
    <motion.line x1="6" y1="6" x2="18" y2="18" {...animations} />
    <motion.line x1="18" y1="6" x2="6" y2="18" {...secondLineAnimation} />
</motion.svg>
```

#### Label.svelte

Source file: [src/lib/examples/multi-state-badge/Label.svelte](https://github.com/humanspeak/svelte-motion/blob/main/docs/src/lib/examples/multi-state-badge/Label.svelte)

```svelte
<script lang="ts">
    import { motion, AnimatePresence, styleString } from '@humanspeak/svelte-motion'
    import { STATES, SPRING_CONFIG, type BadgeState } from './constants'

    type Props = {
        badgeState: BadgeState
    }

    const { badgeState }: Props = $props()

    let labelWidth = $state(0)
    let measureElement: HTMLDivElement | null = $state(null)

    // Measure label width when state changes
    $effect(() => {
        // Access badgeState to create reactive dependency
        void badgeState
        if (measureElement) {
            const { width } = measureElement.getBoundingClientRect()
            labelWidth = width
        }
    })
</script>

<!-- Hidden copy of label to measure width -->
<div
    bind:this={measureElement}
    style={styleString(() => ({
        position: 'absolute',
        visibility: 'hidden',
        whiteSpace: 'nowrap'
    }))}
>
    {STATES[badgeState]}
</div>

<motion.span
    layout
    style={styleString(() => ({
        position: 'relative'
    }))}
    animate={{
        width: labelWidth
    }}
    transition={SPRING_CONFIG}
>
    <AnimatePresence mode="sync" initial={false}>
        <motion.div
            key={badgeState}
            data-debug="label-motion"
            style={styleString(() => ({
                textWrap: 'nowrap'
            }))}
            initial={{
                y: -20,
                opacity: 0,
                filter: 'blur(10px)',
                position: 'absolute'
            }}
            animate={{
                y: 0,
                opacity: 1,
                filter: 'blur(0px)',
                position: 'relative'
            }}
            exit={{
                y: 20,
                opacity: 0,
                filter: 'blur(10px)',
                position: 'absolute'
            }}
            transition={{
                duration: 0.2,
                ease: 'easeInOut'
            }}
        >
            {STATES[badgeState]}
        </motion.div>
    </AnimatePresence>
</motion.span>
```
