Orbit Toggle Switch

A dynamic toggle switch where both the track and thumb rotate smoothly when switching states, creating a visually engaging interaction.

1. Copy the component file

Create a new file called orbit-toggle-switch.jsx in your reusable components folder (for example /src/components/) and paste the following code into it.

orbit-toggle-switch.jsx

import { memo, useState } from "react";
import styles from "./orbit-toggle-switch.module.css";

const OrbitToggleSwitch = (props) => {
  const {
    id,
    checked,
    defaultChecked = false,
    disabled = false,
    onChange,
    isInsideLabel,
    inputProps,
  } = props;

  const isControlled = checked !== undefined;
  
  const [switchChecked, setSwitchChecked] = useState(defaultChecked);

  const isChecked = isControlled ? checked : switchChecked;

  const toggle = () => {
    if (disabled) return;
    if (!isControlled) {
      setSwitchChecked(!isChecked);
    }
    onChange?.(!isChecked);
  };

  const handleKeyDown = (e) => {
    if ([" ", "Enter"].includes(e.key)) {
      e.preventDefault();
      toggle();
    }
  };

  return (
    <div 
      id={id}
      role="checkbox"
      tabIndex={disabled ? -1 : 0}
      aria-checked={isChecked}
      aria-disabled={disabled}
      className={styles["orbit-toggle-switch"]}
      onKeyDown={handleKeyDown}
      {...(!isInsideLabel && ({
        onClick: toggle,
      }))}
    >
      {isInsideLabel && (
        <input 
          type="checkbox" 
          checked={isChecked}
          onChange={toggle}
          aria-hidden={true}
          hidden
          {...inputProps}
        />
      )}
      <div className={styles["thumb"]}></div>
      <div className={styles["track"]}></div>
    </div>
  );
};

export default memo(OrbitToggleSwitch);

2. Copy the CSS module file

In the same folder, create a file called orbit-toggle-switch.module.css and paste the following CSS.

orbit-toggle-switch.module.css

.orbit-toggle-switch {
  --size: 16px;
  --gap: 8px;
  --track-color-unchecked: #bbb;
  --track-color-checked: #5a6be5;
  --thumb-color: white;
  --transition-duration: 250ms;

  @media (prefers-color-scheme: dark) {
    --track-color-unchecked: #888;
    --track-color-checked: #8d98e7;
    --thumb-color: black;
  }

  position: relative;
  display: inline-block;
  cursor: pointer;

  .track {
    display: block;
    width: calc(calc(var(--size) * 2) + calc(var(--gap) * 2));
    height: calc(var(--size) + var(--gap));
    background: var(--track-color-unchecked);
    border-radius: calc(var(--size) * 2);
    z-index: 2;
    transition: all var(--transition-duration) ease-in-out;
  }

  .thumb {
    position: absolute;
    left: var(--gap);
    top: 50%;
    width: var(--size);
    height: var(--size);
    background: var(--thumb-color);
    border-radius: 50%;
    transform: translateY(-50%) rotate(0deg);
    transform-origin: 100% 50%;
    z-index: 3;
    transition: all var(--transition-duration) ease-in-out;
  }

  &[aria-disabled="true"] {
    cursor: initial;
    pointer-events: none;
    opacity: 0.65;
  }

  &[aria-checked="true"] {
    .track {
      background: var(--track-color-checked);
      transform: rotate(180deg);
    }

    .thumb {
      transform: translateY(-50%) rotate(-180deg);
    }
  }
}

3. Use the component

Now you can import and use the component anywhere in your project.

orbit-toggle-switch-preview.jsx

import { useCallback, useState } from "react";
import OrbitToggleSwitch from "@/components/essentials/orbit-toggle-switch/orbit-toggle-switch";

const labelStyles = {
  display: "flex",
  alignItems: "center",
  gap: "8px",
};

const OrbitToggleSwitchPreview = () => {
  const [checked, setChecked] = useState(false);
  const handleChange = useCallback((checked) => {
    setChecked(checked);
  });
  return (
    <div>
      <label style={labelStyles}>
        <OrbitToggleSwitch 
          checked={checked}
          onChange={handleChange}
          isInsideLabel
        />
        <span>
          Did You Try Turning It {checked ? "Off" : "On"}?
        </span>
      </label>
    </div>
  );
}

export default OrbitToggleSwitchPreview;

Disabled Switch

orbit-toggle-switch

import OrbitToggleSwitch from "@/components/essentials/orbit-toggle-switch/orbit-toggle-switch";

const OrbitToggleSwitchDisabledPreview = () => {
  return (
    <div>
      <OrbitToggleSwitch 
        defaultChecked={false}
        disabled
      />
    </div>
  );
}

export default OrbitToggleSwitchDisabledPreview;

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
idstringNoAssigned to the toggle switch control element.
checkedbooleanNoControls the checked state (controlled mode).
defaultCheckedbooleanNofalseSets the initial checked state (uncontrolled mode).
disabledbooleanNofalseDisables the toggle and prevents interaction.
onChange(checked: boolean) => voidNoCallback triggered when the toggle state changes. Receives the new true/false value.
isInsideLabelbooleanNofalseSet to true when used inside a <label> to enable native label click accessibility behavior.
inputPropsReact.InputHTMLAttributes<HTMLInputElement>NoAdditional props passed to the native input when isInsideLabel is true.