---
title: Swap
description: A two-state icon — fade (crossfade), rotate or flip between two children.
category: Data Display
related:
  - /components/theme-controller
  - /components/icon
  - /components/toggle-button
visibility: guest
draft: false
---

# Swap

Two icons, one slot, a transition between them. A sun and a moon, a hamburger and
a close, a play and a pause.

`swap` holds **no state of its own** — you point it at something you already have.
Two components arguing about who is open is worse than one that simply renders
what it is told.

:::preview{title="The three swap effects"}
<x-wirekit::row gap="lg" align="center" wrap>
    <div x-data="{ on: false }">
        <x-wirekit::button intent="neutral" surface="filled" x-on:click="on = !on">
            <x-wirekit::swap effect="fade" expression="on">
                <x-slot:on>Visible</x-slot:on>
                <x-slot:off>Hidden</x-slot:off>
            </x-wirekit::swap>
        </x-wirekit::button>
    </div>
    <div x-data="{ on: false }">
        <x-wirekit::button intent="neutral" surface="filled" x-on:click="on = !on">
            <x-wirekit::swap effect="rotate" expression="on">
                <x-slot:on>Rotated</x-slot:on>
                <x-slot:off>Rotate me</x-slot:off>
            </x-wirekit::swap>
        </x-wirekit::button>
    </div>
    <div x-data="{ on: false }">
        <x-wirekit::button intent="neutral" surface="filled" x-on:click="on = !on">
            <x-wirekit::swap effect="flip" expression="on">
                <x-slot:on>Flipped</x-slot:on>
                <x-slot:off>Flip me</x-slot:off>
            </x-wirekit::swap>
        </x-wirekit::button>
    </div>
</x-wirekit::row>
:::

## Usage

Give it an `on` slot, an `off` slot, and an **`expression`** naming the state:

```blade
<div x-data="{ open: false }">
    <x-wirekit::button x-on:click="open = !open">
        <x-wirekit::swap effect="rotate" expression="open">
            <x-slot:on><x-wirekit::icon name="close" /></x-slot:on>
            <x-slot:off><x-wirekit::icon name="menu" /></x-slot:off>
        </x-wirekit::swap>
    </x-wirekit::button>
</div>
```

`expression` is an Alpine expression, evaluated in whatever scope surrounds the
swap. It needs no `x-data` of its own — Alpine resolves the name up the scope
chain, so `open` here is the button's `open`.

::: warning `expression` is what makes it swap. `active` alone cannot
`active` is a PHP value, resolved once when the page renders. A swap given only
`active` is frozen at whatever the server thought and never moves again — which is
an icon, not a swap.

Use `active` only for a swap the server already knows the answer to and that never
changes on the client. For anything a reader can toggle, you must use `expression`.
:::

## Effects

| `effect` | What happens |
|----------|--------------|
| `fade` | Crossfade. The quiet default. |
| `rotate` | Half a turn — reads well for a sun/moon or a chevron. |
| `flip` | A card flip on the Y axis. |

## Props

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `expression` | `string\|null` | `null` | Alpine expression naming the state. Required for a swap that changes on the client. |
| `active` | `bool` | `false` | Static state, for a server-rendered swap that never changes. Ignored when `expression` is set. |
| `effect` | `string` | `'fade'` | `fade`, `rotate`, `flip`. |
| `scope` | `string\|null` | `null` | Class-scope override. |

## Slots

| Slot | Description |
|------|-------------|
| `on` | Shown when `active` is true. |
| `off` | Shown when `active` is false. Falls back to the default slot. |

## Accessibility

Both children stay in the DOM — you cannot crossfade between an element and
nothing. That has one consequence worth knowing about: the hidden child is
`aria-hidden`, so a screen reader reads only the state that is showing. Without
that, every toggle would announce *both* states ("sun moon").

The swap itself is not a control and takes no focus. Put it inside a real
`<button>` and give **that** the accessible name — a swap between two icons has no
text for a screen reader to read.

Under `prefers-reduced-motion: reduce` the state still changes; it just arrives
instead of traveling. The transform is dropped too, not only the transition — a
half-turn that snaps is still a half-turn nobody asked for.

## Keyboard Interaction

None of its own. Whatever you wrap it in keeps its normal keyboard behavior.
