ReUI
Components

Switch

A control that allows the user to toggle between checked and unchecked states.

Installation

Install ReUI

Refer to the Installation Guide for detailed instructions on setting up ReUI dependencies in your project.

Install dependencies

npm install @radix-ui/react-switch

Add component

Copy and paste the following code into your project’s components/ui/switch.tsx file.

'use client';

import * as React from 'react';
import * as SwitchPrimitives from '@radix-ui/react-switch';
import { cva, type VariantProps } from 'class-variance-authority';
import { cn } from '@/lib/utils';

// Define a context for `permanent` state
const SwitchContext = React.createContext<{ permanent: boolean }>({
  permanent: false,
});

const useSwitchContext = () => {
  const context = React.useContext(SwitchContext);
  if (!context) {
    throw new Error('SwitchIndicator must be used within a Switch component');
  }
  return context;
};

// Define classes for variants
const switchVariants = cva(
  `
    relative peer inline-flex shrink-0 cursor-pointer items-center rounded-full transition-colors 
    focus-visible:outline-hidden 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 data-[state=unchecked]:bg-input
    aria-invalid:border aria-invalid:border-destructive/60 aria-invalid:ring-destructive/10 dark:aria-invalid:border-destructive dark:aria-invalid:ring-destructive/20
    [[data-invalid=true]_&]:border [[data-invalid=true]_&]:border-destructive/60 [[data-invalid=true]_&]:ring-destructive/10  dark:[[data-invalid=true]_&]:border-destructive dark:[[data-invalid=true]_&]:ring-destructive/20
  `,
  {
    variants: {
      shape: {
        pill: 'rounded-full',
        square: 'rounded-md',
      },
      size: {
        sm: 'h-5 w-8',
        md: 'h-6 w-10',
        lg: 'h-8 w-14',
        xl: 'h-9 w-16',
      },
      variant: {
        primary: '',
        mono: '',
      },
      permanent: {
        true: 'bg-input',
        false: '',
      },
    },
    compoundVariants: [
      {
        variant: 'primary',
        permanent: false,
        className: 'data-[state=checked]:bg-primary',
      },
      {
        variant: 'mono',
        permanent: false,
        className: 'data-[state=checked]:bg-mono',
      },
    ],
    defaultVariants: {
      shape: 'pill',
      permanent: false,
      size: 'md',
      variant: 'primary',
    },
  },
);

const switchThumbVariants = cva(
  'pointer-events-none block bg-white w-1/2 h-[calc(100%-4px)] shadow-lg ring-0 transition-transform start-0 data-[state=unchecked]:translate-x-[2px] data-[state=checked]:translate-x-[calc(100%-2px)] rtl:data-[state=unchecked]:-translate-x-[2px] rtl:data-[state=checked]:-translate-x-[calc(100%-2px)]',
  {
    variants: {
      shape: {
        pill: 'rounded-full',
        square: 'rounded-md',
      },
      size: {
        xs: '',
        sm: '',
        md: '',
        lg: '',
        xl: '',
      },
    },
    compoundVariants: [
      {
        shape: 'square',
        size: 'xs',
        className: 'rounded-sm',
      },
    ],
    defaultVariants: {
      shape: 'pill',
      size: 'md',
    },
  },
);

const switchIndicatorVariants = cva(
  'text-sm font-medium absolute mx-[2px] top-1/2 w-1/2 -translate-y-1/2 flex pointer-events-none items-center justify-center text-center transition-transform duration-300 [transition-timing-function:cubic-bezier(0.16,1,0.3,1)]',
  {
    variants: {
      state: {
        on: 'start-0',
        off: 'end-0',
      },
      permanent: {
        true: '',
        false: '',
      },
    },
    compoundVariants: [
      {
        state: 'on',
        permanent: false,
        className:
          'text-primary-foreground peer-data-[state=unchecked]:invisible peer-data-[state=unchecked]:translate-x-full rtl:peer-data-[state=unchecked]:-translate-x-full',
      },
      {
        state: 'off',
        permanent: false,
        className:
          'peer-data-[state=checked]:invisible -translate-x-full rtl:translate-x-full peer-data-[state=unchecked]:translate-x-0',
      },
      {
        state: 'on',
        permanent: true,
        className: 'start-0',
      },
      {
        state: 'off',
        permanent: true,
        className: 'end-0',
      },
    ],
    defaultVariants: {
      state: 'off',
      permanent: false,
    },
  },
);

function SwitchWrapper({
  className,
  children,
  permanent = false,
  ...props
}: React.HTMLAttributes<HTMLDivElement> & { permanent?: boolean }) {
  return (
    <SwitchContext.Provider value={{ permanent }}>
      <div
        data-slot="switch-wrapper"
        className={cn('relative inline-flex items-center', className)}
        {...props}
      >
        {children}
      </div>
    </SwitchContext.Provider>
  );
}

function Switch({
  className,
  thumbClassName = '',
  variant,
  shape,
  size,
  ...props
}: React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root> &
  VariantProps<typeof switchVariants> & { thumbClassName?: string }) {
  // Retrieve context or use default if not wrapped in SwitchWrapper
  const context = useSwitchContext();
  const permanent = context?.permanent ?? false;

  return (
    <SwitchPrimitives.Root
      data-slot="switch"
      className={cn(
        switchVariants({ shape, variant, size, permanent }),
        className,
      )}
      {...props}
    >
      <SwitchPrimitives.Thumb
        className={cn(switchThumbVariants({ shape, size }), thumbClassName)}
      />
    </SwitchPrimitives.Root>
  );
}

function SwitchIndicator({
  className,
  state,
  ...props
}: React.HTMLAttributes<HTMLSpanElement> &
  VariantProps<typeof switchIndicatorVariants>) {
  const context = useSwitchContext();
  const permanent = context?.permanent ?? false;

  return (
    <span
      data-slot="switch-indicator"
      className={cn(switchIndicatorVariants({ state, permanent }), className)}
      {...props}
    />
  );
}

export { Switch, SwitchIndicator, SwitchWrapper };

Examples

Default

Loading

Mono

Loading

Disabled

Loading

Square

Loading

Size

Loading

Indicator

Loading

Icon

Loading

Button

Loading

Advanced Label

Loading

Form

Loading

API Reference

This component is built using Radix UI Switch primitives. For detailed information, please visit the full API reference.

Switch

This component is based on the Switch.Root primitive and includes the following custom props:

PropTypeDefault
shape enum "pill"
size enum "md"
thumbClassName stringundefined

SwitchWrapper

This is a custom component that provides context for permanent state and layout.

PropTypeDefault
permanent booleanfalse
className stringundefined

SwitchIndicator

This is a custom component that provides visual indicators for the switch states.

PropTypeDefault
state enum "off"
className stringundefined