Swap
A shadcn-style swap component built with Ark UI primitives.
ONOFF
import { Swap, SwapIndicator } from "@/components/ui/swap";
const SwapDemo = () => (
<Swap
className="inline-flex h-10 w-16 items-center justify-center rounded-lg border border-border bg-muted"
defaultChecked
>
<SwapIndicator
className="font-medium text-sm data-[state=unchecked]:hidden"
type="on"
>
ON
</SwapIndicator>
<SwapIndicator
className="font-medium text-sm data-[state=checked]:hidden"
type="off"
>
OFF
</SwapIndicator>
</Swap>
);
export default SwapDemo;
Installation
npx shadcn@latest add @ark-cn/swapInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/swap.tsx
"use client";
import { Swap as SwapPrimitive } from "@ark-ui/react/swap";
import { cn } from "@/lib/utils";
export type SwapProps = SwapPrimitive.RootProps;
export const Swap = ({ className, ...props }: SwapProps) => (
<SwapPrimitive.Root data-slot="swap" className={cn(className)} {...props} />
);
export type SwapIndicatorProps = SwapPrimitive.IndicatorProps;
export const SwapIndicator = ({ className, ...props }: SwapIndicatorProps) => (
<SwapPrimitive.Indicator
data-slot="swap-indicator"
className={cn(className)}
{...props}
/>
);
export type SwapRootProviderProps = SwapPrimitive.RootProviderProps;
export const SwapRootProvider = ({
className,
...props
}: SwapRootProviderProps) => (
<SwapPrimitive.RootProvider
data-slot="swap-root-provider"
className={cn(className)}
{...props}
/>
);
export type { UseSwapProps, UseSwapReturn } from "@ark-ui/react/swap";
export { useSwap, useSwapContext } from "@ark-ui/react/swap";
Add the following CSS to your stylesheet (e.g. styles.css):
@keyframes swap-fade-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes swap-fade-out { from { opacity: 1; } to { opacity: 0; } }
@keyframes swap-rotate-in { from { transform: rotate(-90deg); } to { transform: rotate(0deg); } }
@keyframes swap-rotate-out { from { transform: rotate(0deg); } to { transform: rotate(90deg); } }
@keyframes swap-scale-in { from { transform: scale(0); } to { transform: scale(1); } }
@keyframes swap-scale-out { from { transform: scale(1); } to { transform: scale(0); } }
@keyframes swap-flip-in { from { transform: rotateY(180deg); } to { transform: rotateY(0deg); } }
@keyframes swap-flip-out { from { transform: rotateY(0deg); } to { transform: rotateY(180deg); } }
@keyframes swap-blur-in { from { filter: blur(2px); } to { filter: blur(0); } }
@keyframes swap-blur-out { from { filter: blur(0); } to { filter: blur(2px); } }
.swap-indicator-fade[data-state="open"] {
animation: swap-fade-in 200ms ease-out, swap-blur-in 200ms ease-out;
}
.swap-indicator-fade[data-state="closed"] {
animation: swap-fade-out 100ms ease-in, swap-blur-out 100ms ease-in;
}
.swap-indicator-flip { backface-visibility: hidden; }
.swap-indicator-flip[data-state="open"] {
animation: swap-flip-in 400ms ease, swap-blur-in 400ms ease;
}
.swap-indicator-flip[data-state="closed"] {
animation: swap-flip-out 200ms ease, swap-blur-out 200ms ease;
}
.swap-indicator-rotate[data-state="open"] {
animation: swap-rotate-in 250ms ease-out, swap-fade-in 250ms ease-out, swap-blur-in 250ms ease-out;
}
.swap-indicator-rotate[data-state="closed"] {
animation: swap-rotate-out 100ms ease-in, swap-fade-out 100ms ease-in, swap-blur-out 100ms ease-in;
}
.swap-indicator-scale[data-state="open"] {
animation: swap-scale-in 200ms ease-out, swap-fade-in 200ms ease-out, swap-blur-in 200ms ease-out;
}
.swap-indicator-scale[data-state="closed"] {
animation: swap-scale-out 100ms ease-in, swap-fade-out 100ms ease-in, swap-blur-out 100ms ease-in;
}
@media (prefers-reduced-motion: reduce) {
.swap-indicator-fade, .swap-indicator-flip, .swap-indicator-rotate, .swap-indicator-scale {
animation: none !important;
}
}Update import aliases to match your project setup.
Usage
import * as Swap from "@/components/ui/swap"Read exported parts in src/components/ui/swap.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Fade
import { CheckIcon, XIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Swap, SwapIndicator } from "@/components/ui/swap";
const SwapFadeDemo = () => {
const [swapped, setSwapped] = useState(false);
return (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setSwapped((prev) => !prev)}
aria-label={swapped ? "Show error icon" : "Show success icon"}
>
<Swap swap={swapped}>
<SwapIndicator className="swap-indicator-fade" type="on">
<CheckIcon />
</SwapIndicator>
<SwapIndicator className="swap-indicator-fade" type="off">
<XIcon />
</SwapIndicator>
</Swap>
</Button>
);
};
export default SwapFadeDemo;
Flip
import { PauseIcon, PlayIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Swap, SwapIndicator } from "@/components/ui/swap";
const SwapFlipDemo = () => {
const [swapped, setSwapped] = useState(false);
return (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setSwapped((prev) => !prev)}
aria-label={swapped ? "Show pause" : "Show play"}
>
<Swap swap={swapped} style={{ perspective: "200px" }}>
<SwapIndicator className="swap-indicator-flip" type="on">
<PlayIcon />
</SwapIndicator>
<SwapIndicator className="swap-indicator-flip" type="off">
<PauseIcon />
</SwapIndicator>
</Swap>
</Button>
);
};
export default SwapFlipDemo;
Rotate
import { MoonIcon, SunIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Swap, SwapIndicator } from "@/components/ui/swap";
const SwapRotateDemo = () => {
const [swapped, setSwapped] = useState(false);
return (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setSwapped((prev) => !prev)}
aria-label={swapped ? "Show moon" : "Show sun"}
>
<Swap swap={swapped}>
<SwapIndicator className="swap-indicator-rotate" type="on">
<SunIcon />
</SwapIndicator>
<SwapIndicator className="swap-indicator-rotate" type="off">
<MoonIcon />
</SwapIndicator>
</Swap>
</Button>
);
};
export default SwapRotateDemo;
Scale
import { Volume2Icon, VolumeXIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Swap, SwapIndicator } from "@/components/ui/swap";
const SwapScaleDemo = () => {
const [swapped, setSwapped] = useState(false);
return (
<Button
type="button"
variant="outline"
size="icon"
onClick={() => setSwapped((prev) => !prev)}
aria-label={swapped ? "Mute" : "Unmute"}
>
<Swap swap={swapped}>
<SwapIndicator className="swap-indicator-scale" type="on">
<Volume2Icon />
</SwapIndicator>
<SwapIndicator className="swap-indicator-scale" type="off">
<VolumeXIcon />
</SwapIndicator>
</Swap>
</Button>
);
};
export default SwapScaleDemo;
Lazy Mount
Off — unmounted when hidden
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Swap, SwapIndicator } from "@/components/ui/swap";
const SwapLazyMountDemo = () => {
const [swapped, setSwapped] = useState(false);
return (
<div className="flex flex-col gap-3">
<Button
type="button"
variant="outline"
onClick={() => setSwapped((prev) => !prev)}
>
Toggle (lazyMount + unmountOnExit)
</Button>
<Swap lazyMount swap={swapped} unmountOnExit>
<SwapIndicator className="swap-indicator-fade" type="on">
<span className="text-sm">On — mounted when first shown</span>
</SwapIndicator>
<SwapIndicator className="swap-indicator-fade" type="off">
<span className="text-sm">Off — unmounted when hidden</span>
</SwapIndicator>
</Swap>
</div>
);
};
export default SwapLazyMountDemo;
API reference
This component mirrors the upstream Ark UI primitive.
See the ARK UI documentation for the full API.