Animate a React Switch Toggle with Framer Motion

A preview of the animated React switch toggle made with Framer Motion

In this quick tutorial, we’ll show you how to create an animated switch toggle React component using Radix UI, Tailwind CSS and Framer Motion. This toggle animation is inspired to the React Aria switch toggle microinteraction, where the thumb subtly expands on click or touch.

We’ll use Radix UI’s switch primitive and combine it with Framer Motion to add a smooth animations. The result is a visually appealing switch toggle with enhanced user interaction.

Code

"use client";

import * as React from "react";
import * as SwitchPrimitives from "@radix-ui/react-switch";
import { motion, HTMLMotionProps } from "framer-motion";
import { useState } from "react";

const MotionSwitch = motion.create(SwitchPrimitives.Root);
const MotionThumb = motion.create(SwitchPrimitives.Thumb);

const Switch = React.forwardRef<
  React.ElementRef<typeof SwitchPrimitives.Root>,
  React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
    HTMLMotionProps<"button"> & {
      checked?: boolean;
      onCheckedChange?: (checked: boolean) => void;
    }
>(
  (
    { className, checked: controlledChecked, onCheckedChange, ...props },
    ref,
  ) => {
    const [internalChecked, setInternalChecked] = useState(
      props.defaultChecked || false,
    );

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

    const handleCheckedChange = (newChecked: boolean) => {
      if (!isControlled) {
        setInternalChecked(newChecked);
      }
      onCheckedChange?.(newChecked);
    };

    const thumbVariants = {
      tap: {
        width: "24px",
        translateX: checkedState ? "14px" : "2px",
        transition: {
          duration: 0.15,
        },
      },
      checked: {
        translateX: "18px",
        transition: { ease: "circInOut" },
      },
      unchecked: {
        translateX: "2px",
        transition: { ease: "circInOut" },
      },
    };

    return (
      <MotionSwitch
        checked={checkedState}
        onCheckedChange={handleCheckedChange}
        className="inline-flex items-center w-10 h-6 shrink-0 cursor-pointer bg-gray-300 rounded-full relative data-[state=checked]:bg-indigo-500 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50"
        whileTap="tap"
        animate={checkedState ? "checked" : "unchecked"}
        {...props}
        ref={ref}
      >
        <MotionThumb
          initial={{ translateX: "2px" }}
          className="block w-5 h-5 bg-white rounded-full shadow-sm"
          variants={thumbVariants}
        />
      </MotionSwitch>
    );
  },
);
Switch.displayName = SwitchPrimitives.Root.displayName;

export default Switch;

A GIF showing the microinteraction

How it works

At its core, this switch toggle combines the accessibility of Radix UI with Framer Motion animations. When you interact with the switch, the thumb subtly expands, giving a satisfying tactile feel. We’ve used the whileTap gesture for that instant feedback when you click or touch the switch, and the animate prop to handle the transition between checked and unchecked states.

Also, the switch is designed to be flexible, supporting both controlled and uncontrolled usage. This means you can decide to manage state yourself or let the component handle it.

Hope you enjoyed this new component! Feel free to tweak the colors, sizes, and animation parameters to make this switch your own.