Orbit Toggle Switch

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

orbit-toggle-switch-preview.jsx

import { useCallback, useState } from "react";
import OrbitToggleSwitch from "@/components/interactions/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 
          aria-label="example"
          checked={checked}
          onChange={handleChange}
          isInsideLabel
        />
        <span>
          Did You Try Turning It {checked ? "Off" : "On"}?
        </span>
      </label>
    </div>
  );
}

export default OrbitToggleSwitchPreview;

Installation

Run the command from your project root directory (the folder that contains package.json).

Terminal / Console

npx mosaicui-cli@latest interactions/orbit-toggle-switch

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

Examples

Discover how supported props transform components to match your needs.

Disabled Switch

orbit-toggle-switch

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

const OrbitToggleSwitchDisabledPreview = () => {
  return (
    <div>
      <OrbitToggleSwitch
        aria-label="example"
        defaultChecked={false}
        disabled
      />
    </div>
  );
}

export default OrbitToggleSwitchDisabledPreview;

Props

Configure the component with the following props:

PropTypeRequiredDefaultDescription
aria-labelstringNoAccessible label that provides a descriptive name for the component. Helps assistive technologies understand the purpose of the toggle switch.
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.
classNamestringNoOptional class name applied to the root container.
styleReact.CSSPropertiesNoInline styles applied to the root container.