DocsBlog

Get Started

Components

Data Grid

Signals

Styling

Theming

↑↓Navigate
↵Select
EscClose
  • 1.8.0

  • Day

    Night

    Preview

    Switch mode
  • cerberus

    acheron

    elysium

    oceanus

Get Started
Components
Data Grid
Signals
Styling
Theming

Concepts

OverviewCompositionCerberus ContextTesting

Layout

Aspect RatioBleedBoxCenterContainerContainer QueryDividerFlexFloatGridGroupLink OverlayScrollableStackWrap

Components

AccordionAdmonitionAvatarButtonCarouselCheckboxClipboardCollapsibleComboboxConfirm ModalCTA ModalDate PickerDialogFieldFieldsetFile UploaderIconButtonInputLoading StatesMarqueeMenuNotificationsNumber InputPaginationPin InputPopoverProgress IndicatorsPrompt ModalRadioRatingSelectSliderSplit ButtonSwitchTableTabsTagTextTextareaToggleTooltip

Utilities

Client OnlyDownload TriggerEnvironmentFeature FlagsFocus TrapForFormat ByteFormat NumberFormat Relative TimeFormat Relative TimeFrameHighlightJSON Tree ViewLocalePortalPresenceShowSwapsplitPropsTheme

Swap

Helps control the rendering and unmounting of your content based on a given state.

  • npm
View as Markdown
Open this page in Markdown
Anthropic
Open in Claude
Ask questions about this page
OpenAI
Open in ChatGPT
Ask questions about this page

Import

import { Swap } from '@cerberus/react'

Anatomy

<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.

'use client'
 
import { Button, Swap } from '@cerberus/react'
import { useSignal } from '@cerberus/signals'
import { CheckIcon, XIcon } from 'lucide-react'
import styles from 'styles/swap.module.css'
 
export const Fade = () => {
  const [swapped, setSwapped] = useSignal(false)
 
  return (
    <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>
  )
}

Flip

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

import { Button, Swap } from '@cerberus/react'
import { useSignal } from '@cerberus/signals'
import { PauseIcon, PlayIcon } from 'lucide-react'
import styles from 'styles/swap.module.css'
 
export const Flip = () => {
  const [swapped, setSwapped] = useState(false)
 
  return (
    <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>
  )
}

Rotate

Rotate the indicators in and out with a spin transition.

import { Swap } from '@cerberus/react'
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>
  )
}

Scale

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

import { Swap } from '@cerberus/react'
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>
  )
}

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:

.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:

.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:

.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.

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

API Reference

Root

Props

PropTypeRequiredDefaultDescription
asChildbooleanfalseundefinedUse the provided child element as the default rendered element, combining their props and behavior.
hideModeHideModefalse'display-none'How to hide content when mounted but not present.
- 'display-none': HTML hidden attribute. Effects stay alive.
- 'activity': React 19 <Activity mode="hidden">. Effects pause. Requires React 19+.
lazyMountbooleanfalsefalseWhether to enable lazy mounting
swapbooleanfalsefalseWhether the swap is in the "on" state.
unmountOnExitbooleanfalsefalseWhether to unmount on exit.

Indicator

Props

PropTypeRequiredDefaultDescription
type'on' | 'off'trueundefinedundefined
asChildbooleanfalseundefinedUse the provided child element as the default rendered element, combining their props and behavior.

RootProvider

Props

PropTypeRequiredDefaultDescription
valueUseSwapReturntrueundefinedundefined
asChildbooleanfalseundefinedUse the provided child element as the default rendered element, combining their props and behavior.

Context

On this page

  • Import
  • Anatomy
  • Examples
    • Fade
    • Flip
    • Rotate
    • Scale
  • Guides
    • How It Works
    • Animating Indicators
    • 3D Flip Animation
    • Lazy Mount
  • API Reference
    • Root
      • Props
    • Indicator
      • Props
    • RootProvider
      • Props
    • Context
Edit this page on Github