# Swap

URL: https://ark-ui.com/docs/utilities/swap
Source: https://raw.githubusercontent.com/chakra-ui/ark/refs/heads/main/website/src/content/pages/utilities/swap.mdx

Animate between two visual states with smooth transitions.

---

<ComponentPreview id="Swap" />

## Anatomy

<Anatomy id="swap" />

```tsx
<Swap.Root>
  <Swap.Indicator />
</Swap.Root>
```

## Examples

### Fade

Swap between two icons with a fade animation. Set the `swap` prop to toggle between the `on` and `off` indicators.

**Example: fade**

#### React

```tsx
import { Swap } from '@ark-ui/react/swap'
import { CheckIcon, XIcon } from 'lucide-react'
import { useState } from 'react'
import styles from 'styles/swap.module.css'

export const Fade = () => {
  const [swapped, setSwapped] = useState(false)

  return (
    <button type="button" className={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped}>
        <Swap.Indicator type="on" className={styles.FadeIndicator}>
          <CheckIcon />
        </Swap.Indicator>
        <Swap.Indicator type="off" className={styles.FadeIndicator}>
          <XIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Solid

```tsx
import { Swap } from '@ark-ui/solid/swap'
import { CheckIcon, XIcon } from 'lucide-solid'
import { createSignal } from 'solid-js'
import styles from 'styles/swap.module.css'

export const Fade = () => {
  const [swapped, setSwapped] = createSignal(false)

  return (
    <button type="button" class={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped()}>
        <Swap.Indicator type="on" class={styles.FadeIndicator}>
          <CheckIcon />
        </Swap.Indicator>
        <Swap.Indicator type="off" class={styles.FadeIndicator}>
          <XIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Vue

```vue
<script setup lang="ts">
import { Swap } from '@ark-ui/vue/swap'
import { CheckIcon, XIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import styles from 'styles/swap.module.css'

const swapped = ref(false)
</script>

<template>
  <button type="button" :class="styles.Button" @click="swapped = !swapped">
    <Swap.Root :swap="swapped">
      <Swap.Indicator type="on" :class="styles.FadeIndicator">
        <CheckIcon />
      </Swap.Indicator>
      <Swap.Indicator type="off" :class="styles.FadeIndicator">
        <XIcon />
      </Swap.Indicator>
    </Swap.Root>
  </button>
</template>

```

#### Svelte

```svelte
<script lang="ts">
  import { Swap } from '@ark-ui/svelte/swap'
  import { CheckIcon, XIcon } from 'lucide-svelte'
  import styles from 'styles/swap.module.css'

  let swapped = $state(false)
</script>

<button type="button" class={styles.Button} onclick={() => (swapped = !swapped)}>
  <Swap.Root swap={swapped}>
    <Swap.Indicator type="on" class={styles.FadeIndicator}>
      <CheckIcon />
    </Swap.Indicator>
    <Swap.Indicator type="off" class={styles.FadeIndicator}>
      <XIcon />
    </Swap.Indicator>
  </Swap.Root>
</button>

```

### Flip

Add a 3D flip effect by setting `perspective` on the root and using `rotateY` keyframes on the indicators.

**Example: flip**

#### React

```tsx
import { Swap } from '@ark-ui/react/swap'
import { PauseIcon, PlayIcon } from 'lucide-react'
import { useState } from 'react'
import styles from 'styles/swap.module.css'

export const Flip = () => {
  const [swapped, setSwapped] = useState(false)

  return (
    <button type="button" className={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped} style={{ perspective: '200px' }}>
        <Swap.Indicator type="on" className={styles.FlipIndicator}>
          <PlayIcon />
        </Swap.Indicator>
        <Swap.Indicator type="off" className={styles.FlipIndicator}>
          <PauseIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Solid

```tsx
import { Swap } from '@ark-ui/solid/swap'
import { PauseIcon, PlayIcon } from 'lucide-solid'
import { createSignal } from 'solid-js'
import styles from 'styles/swap.module.css'

export const Flip = () => {
  const [swapped, setSwapped] = createSignal(false)

  return (
    <button type="button" class={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped()} style={{ perspective: '200px' }}>
        <Swap.Indicator type="on" class={styles.FlipIndicator}>
          <PlayIcon />
        </Swap.Indicator>
        <Swap.Indicator type="off" class={styles.FlipIndicator}>
          <PauseIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Vue

```vue
<script setup lang="ts">
import { Swap } from '@ark-ui/vue/swap'
import { PauseIcon, PlayIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import styles from 'styles/swap.module.css'

const swapped = ref(false)
</script>

<template>
  <button type="button" :class="styles.Button" @click="swapped = !swapped">
    <Swap.Root :swap="swapped" :style="{ perspective: '200px' }">
      <Swap.Indicator type="on" :class="styles.FlipIndicator">
        <PlayIcon />
      </Swap.Indicator>
      <Swap.Indicator type="off" :class="styles.FlipIndicator">
        <PauseIcon />
      </Swap.Indicator>
    </Swap.Root>
  </button>
</template>

```

#### Svelte

```svelte
<script lang="ts">
  import { Swap } from '@ark-ui/svelte/swap'
  import { PauseIcon, PlayIcon } from 'lucide-svelte'
  import styles from 'styles/swap.module.css'

  let swapped = $state(false)
</script>

<button type="button" class={styles.Button} onclick={() => (swapped = !swapped)}>
  <Swap.Root swap={swapped} style="perspective: 200px">
    <Swap.Indicator type="on" class={styles.FlipIndicator}>
      <PlayIcon />
    </Swap.Indicator>
    <Swap.Indicator type="off" class={styles.FlipIndicator}>
      <PauseIcon />
    </Swap.Indicator>
  </Swap.Root>
</button>

```

### Rotate

Rotate the indicators in and out with a spin transition.

**Example: rotate**

#### React

```tsx
import { Swap } from '@ark-ui/react/swap'
import { MoonIcon, SunIcon } from 'lucide-react'
import { useState } from 'react'
import styles from 'styles/swap.module.css'

export const Rotate = () => {
  const [swapped, setSwapped] = useState(false)

  return (
    <button type="button" className={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped}>
        <Swap.Indicator type="on" className={styles.RotateIndicator}>
          <SunIcon />
        </Swap.Indicator>
        <Swap.Indicator type="off" className={styles.RotateIndicator}>
          <MoonIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Solid

```tsx
import { Swap } from '@ark-ui/solid/swap'
import { MoonIcon, SunIcon } from 'lucide-solid'
import { createSignal } from 'solid-js'
import styles from 'styles/swap.module.css'

export const Rotate = () => {
  const [swapped, setSwapped] = createSignal(false)

  return (
    <button type="button" class={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped()}>
        <Swap.Indicator type="on" class={styles.RotateIndicator}>
          <SunIcon />
        </Swap.Indicator>
        <Swap.Indicator type="off" class={styles.RotateIndicator}>
          <MoonIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Vue

```vue
<script setup lang="ts">
import { Swap } from '@ark-ui/vue/swap'
import { MoonIcon, SunIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import styles from 'styles/swap.module.css'

const swapped = ref(false)
</script>

<template>
  <button type="button" :class="styles.Button" @click="swapped = !swapped">
    <Swap.Root :swap="swapped">
      <Swap.Indicator type="on" :class="styles.RotateIndicator">
        <SunIcon />
      </Swap.Indicator>
      <Swap.Indicator type="off" :class="styles.RotateIndicator">
        <MoonIcon />
      </Swap.Indicator>
    </Swap.Root>
  </button>
</template>

```

#### Svelte

```svelte
<script lang="ts">
  import { Swap } from '@ark-ui/svelte/swap'
  import { MoonIcon, SunIcon } from 'lucide-svelte'
  import styles from 'styles/swap.module.css'

  let swapped = $state(false)
</script>

<button type="button" class={styles.Button} onclick={() => (swapped = !swapped)}>
  <Swap.Root swap={swapped}>
    <Swap.Indicator type="on" class={styles.RotateIndicator}>
      <SunIcon />
    </Swap.Indicator>
    <Swap.Indicator type="off" class={styles.RotateIndicator}>
      <MoonIcon />
    </Swap.Indicator>
  </Swap.Root>
</button>

```

### Scale

Scale the indicators up and down for a pop-in effect.

**Example: scale**

#### React

```tsx
import { Swap } from '@ark-ui/react/swap'
import { Volume2Icon, VolumeXIcon } from 'lucide-react'
import { useState } from 'react'
import styles from 'styles/swap.module.css'

export const Scale = () => {
  const [swapped, setSwapped] = useState(false)

  return (
    <button type="button" className={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped}>
        <Swap.Indicator type="on" className={styles.ScaleIndicator}>
          <Volume2Icon />
        </Swap.Indicator>
        <Swap.Indicator type="off" className={styles.ScaleIndicator}>
          <VolumeXIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Solid

```tsx
import { Swap } from '@ark-ui/solid/swap'
import { Volume2Icon, VolumeXIcon } from 'lucide-solid'
import { createSignal } from 'solid-js'
import styles from 'styles/swap.module.css'

export const Scale = () => {
  const [swapped, setSwapped] = createSignal(false)

  return (
    <button type="button" class={styles.Button} onClick={() => setSwapped((prev) => !prev)}>
      <Swap.Root swap={swapped()}>
        <Swap.Indicator type="on" class={styles.ScaleIndicator}>
          <Volume2Icon />
        </Swap.Indicator>
        <Swap.Indicator type="off" class={styles.ScaleIndicator}>
          <VolumeXIcon />
        </Swap.Indicator>
      </Swap.Root>
    </button>
  )
}

```

#### Vue

```vue
<script setup lang="ts">
import { Swap } from '@ark-ui/vue/swap'
import { Volume2Icon, VolumeXIcon } from 'lucide-vue-next'
import { ref } from 'vue'
import styles from 'styles/swap.module.css'

const swapped = ref(false)
</script>

<template>
  <button type="button" :class="styles.Button" @click="swapped = !swapped">
    <Swap.Root :swap="swapped">
      <Swap.Indicator type="on" :class="styles.ScaleIndicator">
        <Volume2Icon />
      </Swap.Indicator>
      <Swap.Indicator type="off" :class="styles.ScaleIndicator">
        <VolumeXIcon />
      </Swap.Indicator>
    </Swap.Root>
  </button>
</template>

```

#### Svelte

```svelte
<script lang="ts">
  import { Swap } from '@ark-ui/svelte/swap'
  import { Volume2Icon, VolumeXIcon } from 'lucide-svelte'
  import styles from 'styles/swap.module.css'

  let swapped = $state(false)
</script>

<button type="button" class={styles.Button} onclick={() => (swapped = !swapped)}>
  <Swap.Root swap={swapped}>
    <Swap.Indicator type="on" class={styles.ScaleIndicator}>
      <Volume2Icon />
    </Swap.Indicator>
    <Swap.Indicator type="off" class={styles.ScaleIndicator}>
      <VolumeXIcon />
    </Swap.Indicator>
  </Swap.Root>
</button>

```

## Guides

### How It Works

Swap renders two indicators stacked on top of each other in a 1x1 CSS grid. The `swap` prop controls which indicator is
visible. Each indicator uses the presence system, so you get `data-state="open"` and `data-state="closed"` attributes to
drive your CSS animations.

### Animating Indicators

Target `data-state` on each indicator to define enter and exit animations:

```css
.indicator[data-state='open'] {
  animation: fade-in 200ms ease-out;
}

.indicator[data-state='closed'] {
  animation: fade-out 100ms ease-in;
}

@keyframes fade-in {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

@keyframes fade-out {
  from {
    opacity: 1;
  }
  to {
    opacity: 0;
  }
}
```

You can combine animations for richer effects. For example, scale with fade:

```css
.indicator[data-state='open'] {
  animation:
    scale-in 200ms ease-out,
    fade-in 200ms ease-out;
}

.indicator[data-state='closed'] {
  animation:
    scale-out 100ms ease-in,
    fade-out 100ms ease-in;
}
```

### 3D Flip Animation

For a flip effect, set `perspective` on the root and use `backface-visibility: hidden` on indicators:

```css
.flip-indicator {
  backface-visibility: hidden;
}

.flip-indicator[data-state='open'] {
  animation: flip-in 400ms ease;
}

.flip-indicator[data-state='closed'] {
  animation: flip-out 200ms ease;
}

@keyframes flip-in {
  from {
    transform: rotateY(180deg);
  }
  to {
    transform: rotateY(0deg);
  }
}

@keyframes flip-out {
  from {
    transform: rotateY(0deg);
  }
  to {
    transform: rotateY(180deg);
  }
}
```

### Lazy Mount

Use `lazyMount` and `unmountOnExit` to control when indicators mount and unmount. This keeps the DOM clean when
indicators aren't visible.

```tsx
<Swap.Root swap={swapped} lazyMount unmountOnExit>
  <Swap.Indicator type="on">...</Swap.Indicator>
  <Swap.Indicator type="off">...</Swap.Indicator>
</Swap.Root>
```

## API Reference

### Props

**Component API Reference**

#### React

**Root Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `lazyMount` | `boolean` | No | Whether to enable lazy mounting |
| `swap` | `boolean` | No | Whether the swap is in the "on" state. |
| `unmountOnExit` | `boolean` | No | Whether to unmount on exit. |


**Indicator Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `type` | `'on' | 'off'` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**RootProvider Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `value` | `UseSwapReturn` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


#### Solid

**Root Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `(props: ParentProps<'span'>) => Element` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `lazyMount` | `boolean` | No | Whether to enable lazy mounting |
| `swap` | `boolean` | No | Whether the swap is in the "on" state. |
| `unmountOnExit` | `boolean` | No | Whether to unmount on exit. |


**Indicator Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `type` | `'on' | 'off'` | Yes |  |
| `asChild` | `(props: ParentProps<'span'>) => Element` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**RootProvider Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `value` | `Accessor<UseSwapReturn>` | Yes |  |
| `asChild` | `(props: ParentProps<'span'>) => Element` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


#### Vue

**Root Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `lazyMount` | `boolean` | No | Whether to enable lazy mounting. |
| `swap` | `boolean` | No | Whether the swap is in the "on" state. |
| `unmountOnExit` | `boolean` | No | Whether to unmount on exit. |


**Indicator Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `type` | `'on' | 'off'` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**RootProvider Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `value` | `UseSwapReturn` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


#### Svelte

**Root Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `Snippet<[PropsFn<'span'>]>` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `lazyMount` | `boolean` | No | Whether to enable lazy mounting |
| `ref` | `Element` | No |  |
| `swap` | `boolean` | No | Whether the swap is in the "on" state. |
| `unmountOnExit` | `boolean` | No | Whether to unmount on exit. |


**Indicator Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `type` | `'off' | 'on'` | Yes |  |
| `asChild` | `Snippet<[PropsFn<'span'>]>` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `ref` | `Element` | No |  |


**RootProvider Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `value` | `Accessor<UseSwapReturn>` | Yes |  |
| `asChild` | `Snippet<[PropsFn<'span'>]>` | No | Use the provided child element as the default rendered element, combining their props and behavior. |
| `ref` | `Element` | No |  |


### Context

<ContextType id="swap" />