Todos os componentes

Animated Toggle

Animated Toggle é um componente para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

SmoothUI MIT other

O que é

Animated Toggle é um componente para React e Tailwind CSS, da biblioteca SmoothUI, com licença MIT. Copia e cola no teu projeto.

Como usar

Abre o componente no catálogo, carrega em copiar, e cola no teu projeto. O prompt copiado leva o código e as regras para o teu agente o reproduzir sem alterar nada.

Abrir no catálogo

Cinco cópias grátis por mês. Sem cartão.

Precisa de

npm i motion

Licença

Este componente vem de SmoothUI e é redistribuído sob a licença MIT, que permite uso comercial. O aviso de copyright viaja com o código.

Animated Toggle

index.tsx
"use client";

import { cn } from "@/lib/utils";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import {
  type KeyboardEvent,
  type ReactNode,
  useCallback,
  useState,
} from "react";

export interface AnimatedToggleProps {
  /** Controlled checked state */
  checked?: boolean;
  /** Additional CSS classes */
  className?: string;
  /** Default checked state for uncontrolled mode */
  defaultChecked?: boolean;
  /** Whether the toggle is disabled */
  disabled?: boolean;
  /** Icons for on/off states (only used with icon variant) */
  icons?: { on: ReactNode; off: ReactNode };
  /** Accessible label for the toggle */
  label?: string;
  /** Callback when checked state changes */
  onChange?: (checked: boolean) => void;
  /** Size of the toggle */
  size?: "sm" | "md" | "lg";
  /** Visual variant of the toggle */
  variant?: "default" | "morph" | "icon";
}

const SPRING = {
  bounce: 0.1,
  duration: 0.25,
  type: "spring" as const,
};

const SIZES = {
  lg: {
    icon: "size-3.5",
    thumb: "size-6",
    thumbTranslate: 24,
    track: "w-[52px] h-7",
  },
  md: {
    icon: "size-3",
    thumb: "size-5",
    thumbTranslate: 20,
    track: "w-11 h-6",
  },
  sm: {
    icon: "size-2.5",
    thumb: "size-4",
    thumbTranslate: 16,
    track: "w-9 h-5",
  },
};

const AnimatedToggle = ({
  checked: controlledChecked,
  defaultChecked = false,
  onChange,
  variant = "default",
  icons,
  size = "md",
  disabled = false,
  label,
  className,
}: AnimatedToggleProps) => {
  const shouldReduceMotion = useReducedMotion();

  const [internalChecked, setInternalChecked] = useState(defaultChecked);

  const isControlled = controlledChecked !== undefined;
  const checked = isControlled ? controlledChecked : internalChecked;

  const handleToggle = useCallback(() => {
    if (disabled) {
      return;
    }

    const newValue = !checked;
    if (!isControlled) {
      setInternalChecked(newValue);
    }
    onChange?.(newValue);
  }, [checked, disabled, isControlled, onChange]);

  const handleKeyDown = useCallback(
    (event: KeyboardEvent<HTMLButtonElement>) => {
      if (event.key === " " || event.key === "Enter") {
        event.preventDefault();
        handleToggle();
      }
    },
    [handleToggle]
  );

  const sizeConfig = SIZES[size];

  const getThumbBorderRadius = () => {
    if (variant !== "morph" || shouldReduceMotion) {
      return 9999;
    }
    return checked ? 9999 : 6;
  };

  const getThumbTransform = () => {
    const translateX = checked ? sizeConfig.thumbTranslate : 0;
    return translateX;
  };

  return (
    <button
      aria-checked={checked}
      aria-label={label}
      className={cn(
        "relative inline-flex shrink-0 cursor-pointer items-center rounded-full p-0.5 transition-colors",
        "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background",
        checked ? "bg-brand" : "bg-muted-foreground/30",
        disabled && "cursor-not-allowed opacity-50",
        sizeConfig.track,
        className
      )}
      disabled={disabled}
      onClick={handleToggle}
      onKeyDown={handleKeyDown}
      role="switch"
      type="button"
    >
      <motion.span
        animate={
          shouldReduceMotion
            ? {
                x: getThumbTransform(),
              }
            : {
                borderRadius: getThumbBorderRadius(),
                x: getThumbTransform(),
              }
        }
        className={cn(
          "pointer-events-none flex items-center justify-center rounded-full border border-border bg-background shadow-sm",
          sizeConfig.thumb
        )}
        initial={false}
        style={{
          borderRadius: getThumbBorderRadius(),
        }}
        transition={shouldReduceMotion ? { duration: 0 } : SPRING}
      >
        {variant === "icon" && icons && (
          <AnimatePresence initial={false} mode="wait">
            <motion.span
              animate={
                shouldReduceMotion
                  ? { opacity: 1 }
                  : { opacity: 1, rotate: 0, scale: 1 }
              }
              className={cn(
                "flex items-center justify-center text-muted-foreground",
                sizeConfig.icon
              )}
              exit={
                shouldReduceMotion
                  ? { opacity: 0, transition: { duration: 0 } }
                  : { opacity: 0, rotate: -90, scale: 0.5 }
              }
              initial={
                shouldReduceMotion
                  ? { opacity: 0 }
                  : { opacity: 0, rotate: 90, scale: 0.5 }
              }
              key={checked ? "on" : "off"}
              transition={shouldReduceMotion ? { duration: 0 } : SPRING}
            >
              {checked ? icons.on : icons.off}
            </motion.span>
          </AnimatePresence>
        )}
      </motion.span>
    </button>
  );
};

export default AnimatedToggle;

Componentes parecidos