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):
// components.json — register the @bottega namespace once
{
"registries": {
"@bottega": { "url": "https://bottega.ariacode.ca/r/{name}.json" }
}
}2. Add the component:
npx shadcn add @bottega/neumorphic-label-switchUsage
<NeumorphicLabelSwitch />Props
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | nlswitch | Unique id wiring the hidden input to the track label. Defaults to "nlswitch". |
| label | string | Toggle | Accessible label surfaced to screen readers (e.g. "Dark mode"). |
| checked | boolean | — | Controlled checked state. |
| onChange | (checked: boolean) => void | — | Change handler — called with the new checked value. |
| disabled | boolean | false | Disables the toggle (no interaction, reduced-opacity). |
| uncheckedLabel | string | On | Text shown on the left when unchecked. Default "On". |
| checkedLabel | string | Off | Text shown on the right when checked. Default "Off". |
| className | string | Additional class applied to the outermost wrapper. |
Source
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