Swap Button

Shadcn button that swaps two labels (and optional icons) with a crossfade, with no width jump. Drop-in on top of Button.

Component

Installation

 

Usage

Import

Import along with a boolean for swap state (or use a server action, etc.).

import { useState } from "react";
import { SwapButton } from "@/components/ui/swap-button";

Wire state

Wire swapped to your state. Optional icon / iconSwapped for leading icons per state.

const [on, setOn] = useState(false);
// ...
<SwapButton
  swapped={on}
  label1="Before"
  label2="After"
  onClick={() => setOn((o) => !o)}
/>;

Guidelines

  • Control visibility with the swapped boolean: true shows label2, false shows label1.
  • With icons, the leading icon is icon when not swapped; when swapped, uses iconSwapped if set, else icon.
  • Extends the shadcn Button; pass variant, size, asChild, className, onClick, and other native button props as usual.

Props

All props are optional unless marked required. Use these to customize every aspect of the component.

PropTypeDefaultDescription
swappedrequiredboolean-When true, label2 is visible; when false, label1.
label1string-First label (visible when not swapped).
label2string-Second label (visible when swapped).
iconReact.ReactNode-Leading content when not swapped. Often a lucide icon.
iconSwappedReact.ReactNodeiconLeading content when swapped. Falls back to icon if omitted.
labelClassNamestring-Class names applied to both label spans (text styling).
Button & React.ButtonHTMLAttributes<HTMLButtonElement>-Inherits shadcn Button props: variant, size, asChild, className, disabled, onClick, type, etc.

Examples

Basic

Labels only. Width follows the longer string.

import { useState } from "react";
import { SwapButton } from "@/components/ui/swap-button";

export function SaveExample() {
  const [isSaved, setIsSaved] = useState(false);
  return (
    <SwapButton
      swapped={isSaved}
      label1="Save"
      label2="Saved"
      onClick={() => setIsSaved((s) => !s)}
    />
  );
}

With icons

Optional icon + iconSwapped for different icons per state.

import { useState } from "react";
import { Check, Mail } from "lucide-react";
import { SwapButton } from "@/components/ui/swap-button";

export function NotifyExample() {
  const [subscribed, setSubscribed] = useState(false);
  return (
    <SwapButton
      swapped={subscribed}
      label1="Subscribe"
      label2="Subscribed"
      icon={<Mail className="size-4" />}
      iconSwapped={<Check className="size-4" />}
      onClick={() => setSubscribed((s) => !s)}
    />
  );
}

Outline

Any Button variant (default, outline, ghost, link, …).

import { useState } from "react";
import { SwapButton } from "@/components/ui/swap-button";

export function FollowExample() {
  const [following, setFollowing] = useState(false);
  return (
    <SwapButton
      variant="outline"
      swapped={following}
      label1="Follow"
      label2="Following"
      onClick={() => setFollowing((f) => !f)}
    />
  );
}

Last updated on Apr 26

Made with ❤️ by Pulkit & Cursor :)

© 2026 Pulkit. All rights reserved

DMCA Verified

Last updated: