nucleusUI
All components

NucleusSwitch

InputsNew

A toggle switch built on a real <button role='switch'>, with label and hint wiring, controlled and uncontrolled modes and smooth knob physics.

Preview

Reply threads and mentions

Anyone can see your work

Installation

  1. 1shadcn/ui initialized (`npx shadcn@latest init`)
  2. 2Tailwind CSS v4 configured with the nucleusUI theme tokens
npm install lucide-react clsx tailwind-merge

Add to your globals.css

The nucleusUI accent lives in these CSS variables. Without them the component falls back to the default shadcn zinc theme.

app/globals.css
:root {
  --primary: oklch(0.54 0.24 291);
  --primary-foreground: oklch(0.985 0 0);
  --ring: oklch(0.54 0.24 291);
  --accent: oklch(0.96 0.03 291);
  --accent-foreground: oklch(0.4 0.19 291);
}

.dark {
  --primary: oklch(0.68 0.21 291);
  --primary-foreground: oklch(0.145 0 0);
  --ring: oklch(0.68 0.21 291);
  --accent: oklch(0.26 0.06 291);
  --accent-foreground: oklch(0.87 0.09 291);
}

Usage

tsx
import { NucleusSwitch } from "@/components/nucleus/nucleus-switch";

export function Example() {
  return (
    <NucleusSwitch
      defaultChecked
      label="Email notifications"
      hint="Reply threads and mentions"
      onCheckedChange={(checked) => console.log(checked)}
    />
  );
}

Source

Copy this file into components/nucleus/nucleus-switch.tsx in your project, it's yours to own and modify.

components/nucleus/nucleus-switch.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

export interface NucleusSwitchProps
  extends Omit<React.ComponentProps<"button">, "onChange"> {
  /** Controlled checked state. */
  checked?: boolean;
  /** Uncontrolled initial state. */
  defaultChecked?: boolean;
  /** Fires with the next checked value. */
  onCheckedChange?: (checked: boolean) => void;
  /** Label rendered beside the track. */
  label?: string;
  /** Helper text rendered under the label. */
  hint?: string;
}

/**
 * Accessible toggle switch. Renders a real <button role="switch"> so it
 * works with keyboard and screen readers out of the box.
 */
export const NucleusSwitch = React.forwardRef<HTMLButtonElement, NucleusSwitchProps>(
  function NucleusSwitch(
    {
      className,
      checked: checkedProp,
      defaultChecked = false,
      onCheckedChange,
      label,
      hint,
      id,
      disabled,
      ...props
    },
    ref
  ) {
    const autoId = React.useId();
    const switchId = id ?? autoId;
    const [internal, setInternal] = React.useState(defaultChecked);
    const isControlled = checkedProp !== undefined;
    const checked = isControlled ? checkedProp : internal;

    const toggle = () => {
      if (disabled) return;
      const next = !checked;
      if (!isControlled) setInternal(next);
      onCheckedChange?.(next);
    };

    return (
      <div className="flex items-center gap-3" data-slot="nucleus-switch">
        <button
          ref={ref}
          id={switchId}
          type="button"
          role="switch"
          aria-checked={checked}
          aria-label={props["aria-label"] ?? (label ? undefined : "Toggle")}
          disabled={disabled}
          onClick={toggle}
          className={cn(
            "relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border border-border/60 transition-colors duration-200 select-none",
            "focus-visible:ring-3 focus-visible:ring-ring/50 focus-visible:outline-none",
            "disabled:pointer-events-none disabled:opacity-55",
            checked
              ? "bg-primary shadow-[inset_0_0_0_1px_color-mix(in_oklch,var(--primary)_60%,transparent)]"
              : "bg-muted",
            className
          )}
          {...props}
        >
          <span
            aria-hidden
            className={cn(
              "pointer-events-none absolute left-1 flex size-4 items-center justify-center rounded-full bg-background shadow-sm transition-transform duration-200",
              checked && "translate-x-5"
            )}
          />
        </button>
        {(label || hint) && (
          <div className="flex flex-col">
            {label && (
              <label
                htmlFor={switchId}
                className="cursor-pointer text-sm font-medium leading-5"
                onClick={toggle}
              >
                {label}
              </label>
            )}
            {hint && (
              <p className="text-xs text-muted-foreground">{hint}</p>
            )}
          </div>
        )}
      </div>
    );
  }
);

Props

PropTypeDescription
checkedbooleanControlled checked state.
defaultCheckedbooleanUncontrolled initial state.
onCheckedChange(checked: boolean) => voidFires with the next checked value.
labelstringLabel rendered beside the track.
hintstringHelper text rendered under the label.
...restComponentProps<'button'>All native button attributes are forwarded.