bottega

primitives

Neumorphic Label Switch

A skeuomorphic/neumorphic toggle switch with always-visible 'On' and 'Off' labels inside the track. The outer shell is a raised pill built from a subtle two-stop gradient and dual box-shadows (dark underside, white toplight). An inner groove panel — rendered via ::before — reverses its gradient and inset-shadow direction on toggle to create the illusion of a sliding thumb pushing across. The active label glows with a primary-mapped text-shadow; the inactive label fades to a muted neutral. State is driven entirely by a visually-hidden native <input type='checkbox' role='switch'> so keyboard, AT, and pointer interaction all work without JavaScript.

Ported from JaydipPrajapati1910 (MIT)


Install

1. Register the namespace (once per project):

json
// components.json — register the @bottega namespace once
{
  "registries": {
    "@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
  }
}

2. Add the component:

bash
npx shadcn add @bottega/neumorphic-label-switch

Usage

Usagetsx
<NeumorphicLabelSwitch />

Props

PropTypeDefaultDescription
idstringnlswitchUnique id wiring the hidden input to the track label. Defaults to "nlswitch".
labelstringToggleAccessible label surfaced to screen readers (e.g. "Dark mode").
checkedbooleanControlled checked state.
onChange(checked: boolean) => voidChange handler — called with the new checked value.
disabledbooleanfalseDisables the toggle (no interaction, reduced-opacity).
uncheckedLabelstringOnText shown on the left when unchecked. Default "On".
checkedLabelstringOffText shown on the right when checked. Default "Off".
classNamestringAdditional class applied to the outermost wrapper.

Source

neumorphic-label-switch.tsxtsx
import type React from "react";
import styles from "./neumorphic-label-switch.module.css";

export type NeumorphicLabelSwitchProps = {
  /** Unique id wiring the hidden input to the track label. Defaults to "nlswitch". */
  id?: string;
  /** Accessible label surfaced to screen readers (e.g. "Dark mode"). */
  label?: string;
  /** Controlled checked state. */
  checked?: boolean;
  /** Change handler — called with the new checked value. */
  onChange?: (checked: boolean) => void;
  /** Disables the toggle (no interaction, reduced-opacity). */
  disabled?: boolean;
  /** Text shown on the left when unchecked. Default "On". */
  uncheckedLabel?: string;
  /** Text shown on the right when checked. Default "Off". */
  checkedLabel?: string;
  /** Additional class applied to the outermost wrapper. */
  className?: string;
};

/**
 * NeumorphicLabelSwitch — server component, static CSS only.
 * Interaction is driven by the native hidden checkbox; all visual state lives in CSS.
 */
export function NeumorphicLabelSwitch({
  id = "nlswitch",
  label = "Toggle",
  checked,
  onChange,
  disabled = false,
  uncheckedLabel = "On",
  checkedLabel = "Off",
  className = "",
}: NeumorphicLabelSwitchProps) {
  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    onChange?.(e.target.checked);
  };

  return (
    <div className={[styles.wrapper, className].filter(Boolean).join(" ")}>
      {/* Visually-hidden native checkbox: carries state + a11y */}
      <input
        type="checkbox"
        role="switch"
        id={id}
        className={styles.input}
        checked={checked}
        onChange={handleChange}
        disabled={disabled}
        aria-label={label}
        aria-checked={checked}
      />

      {/* Visible track — purely decorative, aria-hidden */}
      <label htmlFor={id} className={styles.track} aria-hidden="true">
        {/* The two always-present text labels; CSS drives color + glow */}
        <span className={styles.labelLeft} data-label={uncheckedLabel}>
          {uncheckedLabel}
        </span>
        <span className={styles.labelRight} data-label={checkedLabel}>
          {checkedLabel}
        </span>
      </label>
    </div>
  );
}

Dependencies

  • @bottega/tokens