Component
Toggles / Switch
Use toggles for binary settings that take immediate effect. Nexus provides Toggle for Desktop/Tablet and MobileToggle for Mobile Application, both wired to the --components-toggle-switch-* tokens.
Playground
Value
State
Size
Show Icon
Container
Label text
Helper content
Helper text
Preview
JSX
<Toggle
value={false}
size="sm"
label="Toggle/Switch Title"
helperText="Helper text write here..."
onChange={(val) => setValue(val)}
/>Props
The full Toggle API for Desktop / Tablet.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | boolean | — | Toggle state — off (false) or on (true) |
size | "sm" | "md" | "sm" | Track and knob dimensions |
type | "default" | "hover" | "focus" | "disable" | "default" | Interaction state |
label | string | "Toggle/Switch Title" | Label title displayed to the right of the toggle |
showIcon | boolean | true | Show / hide the toggle control (track + knob) itself |
showToggleContainer | boolean | true | Show / hide the label + helper text container |
showHelperContent | boolean | true | Show / hide the helper text |
helperText | string | "Helper text write here..." | Supporting helper text below the label |
onChange | (value: boolean) => void | — | Called with the new boolean value on toggle |
Usage guidelines — Desktop / Tablet
When to use Toggle
- Use for binary settings that take immediate effect without requiring a form submission.
- Use for enabling / disabling features like notifications, dark mode, permissions.
- Use for turning integrations on or off.
- Ensure the toggle conveys state independently of color for color-blind users.
When not to use Toggle
- Do not use for form fields that require submission — use Checkbox instead.
- Do not use for mutually exclusive multi-option choices — use RadioButton.
- Do not use for destructive or irreversible actions — use a confirmation Button pattern.
- Do not use type='focus' unless programmatically managing keyboard focus states.
Usage guidelines — Mobile Application
When to use MobileToggle
- Use for binary settings on mobile that take immediate effect.
- Always provide the os prop ('android' or 'ios').
- Consider adding an aria-live region nearby to announce the result instantly on press.
- Use type='hover' only for touch-pressed states.
When not to use MobileToggle
- Do not use Desktop Toggle for mobile screens.
- Do not attempt to add focus shadows; mobile has no keyboard focus ring.
- Do not reduce dimensions below the minimum 44x44px touch target recommendation.
- Never pass a string 'true' / 'false' or a number to the value prop.
nexus/components/toggle.tsx
export type ToggleType = "default" | "hover" | "focus" | "disable";
export type ToggleSize = "sm" | "md";
export interface ToggleProps {
/** Toggle state — off or on */
value: boolean;
/** Interaction state */
type?: ToggleType;
/** Track and knob dimensions */
size?: ToggleSize;
/** Label title displayed to the right of the toggle */
label?: string;
/** Supporting helper text below the label */
helperText?: string;
/** Show / hide the helper text */
showHelperContent?: boolean;
/** Show / hide the label + helper text container */
showToggleContainer?: boolean;
/** Show / hide the toggle control (track + knob) itself */
showIcon?: boolean;
/** Called with the new boolean value on toggle */
onChange?: (value: boolean) => void;
/** Additional classes */
className?: string;
}
// Static literal maps — Tailwind can only emit utilities it sees as full strings in source.
// Token values verified against Figma nodes 431:1978 / 431:1964 / 431:1974 / 431:1967 / 431:1969.
const TRACK_TOKENS: Record<"off" | "on", Record<ToggleType, string>> = {
off: {
default: "bg-[var(--components-toggle-switch-color-background-subtle)] border-[var(--components-toggle-switch-color-border-light)]",
hover: "bg-[var(--components-toggle-switch-color-background-primary-subtle)] border-[var(--components-toggle-switch-color-border-medium)]",
focus: "bg-[var(--components-toggle-switch-color-background-subtle)] border-[var(--components-toggle-switch-color-border-light)] shadow-toggle-default",
disable: "bg-[var(--components-toggle-switch-color-background-disabled)] border-transparent",
},
on: {
default: "bg-[var(--components-toggle-switch-color-background-primary)] border-transparent",
hover: "bg-[var(--components-toggle-switch-color-background-on-primary)] border-transparent",
focus: "bg-[var(--components-toggle-switch-color-background-primary)] border-transparent shadow-toggle-focus",
disable: "bg-[var(--components-toggle-switch-color-background-disabled)] border-transparent",
},
};
// SM: 36×20px track, 16px knob, 2px padding → knob travels 36-16-4=16px when ON.
// MD: 44×24px track, 20px knob, 2px padding → knob travels 44-20-4=20px when ON.
// --spacing-44 doesn't exist so MD track width uses an arbitrary px value.
const SIZE_CLASSES: Record<ToggleSize, { track: string; knob: string; translate: string }> = {
sm: { track: "w-36 h-20", knob: "size-16", translate: "translate-x-16" },
md: { track: "w-[44px] h-24", knob: "size-20", translate: "translate-x-20" },
};
/**
* Nexus Toggle — for Desktop/Tablet binary settings.
*/
export const Toggle = ({
value,
type = "default",
size = "sm",
label = "Toggle/Switch Title",
helperText = "Helper text write here...",
showHelperContent = true,
showToggleContainer = true,
showIcon = true,
onChange,
className = "",
}: ToggleProps) => {
const disabled = type === "disable";
const stateTokens = value ? TRACK_TOKENS.on[type] : TRACK_TOKENS.off[type];
const sizeTokens = SIZE_CLASSES[size];
const content = (
// Hug width, max 1600px (Figma node 433:2169/433:2182); no --spacing-1600 token exists.
<label
className={`inline-flex max-w-[1600px] items-start gap-8 ${
disabled ? "cursor-not-allowed" : "cursor-pointer"
} ${className}`}
>
{showIcon && (
<button
type="button"
role="switch"
aria-checked={value}
aria-disabled={disabled}
disabled={disabled}
onClick={() => !disabled && onChange?.(!value)}
className={`shrink-0 flex items-center rounded-full p-2 border-[length:var(--stroke-point-five)] transition-colors ${sizeTokens.track} ${stateTokens}`}
>
<span
className={`rounded-full bg-[var(--components-toggle-switch-color-background-white)] shadow-sm transition-transform ${sizeTokens.knob} ${
value ? sizeTokens.translate : "translate-x-0"
}`}
/>
</button>
)}
{showToggleContainer && (label || (showHelperContent && helperText)) && (
// Container hugs, capped at 1572px with 2px vertical gap (Figma "Container" node).
<span className="flex max-w-[1572px] flex-col gap-2">
{label && (
<span
className={`${
size === "sm" ? "typography-b2-medium-14" : "typography-b1-medium-16"
} text-[var(--components-toggle-switch-color-text-heading)]`}
>
{label}
</span>
)}
{showHelperContent && helperText && (
<span
className={`${
size === "sm" ? "typography-b2-regular-14" : "typography-b1-regular-16"
} text-[var(--components-toggle-switch-color-text-muted)]`}
>
{helperText}
</span>
)}
</span>
)}
</label>
);
return content;
};nexus/components/mobile-toggle.tsx
export type MobileToggleType = "default" | "hover" | "disable";
export type MobileToggleOs = "android" | "ios";
export interface MobileToggleProps {
/** Toggle state — off or on */
value: boolean;
/** Platform — controls the visual rendering style */
os?: MobileToggleOs;
/** Interaction state (hover = touch-pressed) */
type?: MobileToggleType;
/** Label title displayed to the right of the toggle */
label?: string;
/** Supporting helper text below the label */
helperText?: string;
/** Show / hide the helper text */
showHelperContent?: boolean;
/** Show / hide the label + helper text container */
showToggleContainer?: boolean;
/** Show / hide the toggle control (track + knob) itself */
showIcon?: boolean;
/** Called with the new boolean value on press */
onChange?: (value: boolean) => void;
/** Additional classes */
className?: string;
}
type MobileKey = `${"android" | "ios"}-${"off" | "on"}-${"default" | "hover" | "disable"}`;
// Static literal maps — Tailwind can only emit utilities it sees as full strings in source.
// Android track has a 1px border; iOS track has none. Token values match the Figma node (436:3186).
const TRACK_CLASSES: Record<MobileKey, string> = {
"android-off-default": "bg-[var(--components-toggle-switch-color-background-subtle)] border-[length:var(--stroke-1)] border-[var(--components-toggle-switch-color-border-bold)]",
"android-off-hover": "bg-[var(--components-toggle-switch-color-background-primary-subtle)] border-[length:var(--stroke-1)] border-[var(--components-toggle-switch-color-border-medium)]",
"android-off-disable": "bg-[var(--components-toggle-switch-color-background-light)] border-[length:var(--stroke-1)] border-[var(--components-toggle-switch-color-border-default)]",
"android-on-default": "bg-[var(--components-toggle-switch-color-background-primary)] border-[length:var(--stroke-1)] border-transparent",
"android-on-hover": "bg-[var(--components-toggle-switch-color-background-on-primary)] border-[length:var(--stroke-1)] border-transparent",
"android-on-disable": "bg-[var(--components-toggle-switch-color-background-light)] border-[length:var(--stroke-1)] border-[var(--components-toggle-switch-color-border-default)]",
"ios-off-default": "bg-[var(--components-toggle-switch-color-background-subtle)]",
"ios-off-hover": "bg-[var(--components-toggle-switch-color-background-primary-subtle)]",
"ios-off-disable": "bg-[var(--components-toggle-switch-color-background-light)]",
"ios-on-default": "bg-[var(--components-toggle-switch-color-background-primary)]",
"ios-on-hover": "bg-[var(--components-toggle-switch-color-background-on-primary)]",
"ios-on-disable": "bg-[var(--components-toggle-switch-color-background-light)]",
};
// The knob is a circle centered inside a 40px "State" frame (Figma nodes 436:3174–3185).
// Size + fill + shadow are platform-specific:
// • Android OFF → 16px slate (background-neutral) circle, no shadow.
// • Android ON → 24px white circle, no shadow.
// • iOS OFF/ON → 28px circle (no --spacing-28 token; derive from --base-number-28),
// always shadow-md.
// • Disabled → Android uses background-disabled; iOS uses background-subtle.
// Full literal strings so Tailwind can emit every variant.
const CIRCLE_CLASSES: Record<MobileKey, string> = {
"android-off-default": "size-16 bg-[var(--components-toggle-switch-color-background-neutral)]",
"android-off-hover": "size-16 bg-[var(--components-toggle-switch-color-background-neutral)]",
"android-off-disable": "size-16 bg-[var(--components-toggle-switch-color-background-disabled)]",
"android-on-default": "size-24 bg-[var(--components-toggle-switch-color-background-white)]",
"android-on-hover": "size-24 bg-[var(--components-toggle-switch-color-background-white)]",
"android-on-disable": "size-24 bg-[var(--components-toggle-switch-color-background-disabled)]",
"ios-off-default": "size-[var(--base-number-28)] bg-[var(--components-toggle-switch-color-background-white)] shadow-md",
"ios-off-hover": "size-[var(--base-number-28)] bg-[var(--components-toggle-switch-color-background-white)] shadow-md",
"ios-off-disable": "size-[var(--base-number-28)] bg-[var(--components-toggle-switch-color-background-subtle)] shadow-md",
"ios-on-default": "size-[var(--base-number-28)] bg-[var(--components-toggle-switch-color-background-white)] shadow-md",
"ios-on-hover": "size-[var(--base-number-28)] bg-[var(--components-toggle-switch-color-background-white)] shadow-md",
"ios-on-disable": "size-[var(--base-number-28)] bg-[var(--components-toggle-switch-color-background-subtle)] shadow-md",
};
/**
* Nexus MobileToggle — for Mobile Application binary settings.
* Track: 52×32px, overflow-visible. Knob: a circle centered in a 40px State frame —
* Android 16px (OFF) / 24px (ON), iOS 28px (both, shadow-md).
* OFF: knob center at left-16 (16px). ON: knob center at left-36 (36px). Travel = 20px.
*/
export const MobileToggle = ({
value,
os = "android",
type = "default",
label = "Toggle/Switch Title",
helperText = "Helper text write here...",
showHelperContent = true,
showToggleContainer = true,
showIcon = true,
onChange,
className = "",
}: MobileToggleProps) => {
const disabled = type === "disable";
const valueKey = value ? "on" : "off";
const key: MobileKey = `${os}-${valueKey}-${type}`;
const trackClasses = TRACK_CLASSES[key];
const circleClasses = CIRCLE_CLASSES[key];
const content = (
// Hug width, max 1600px (Figma node 436:3263); no --spacing-1600 token exists.
<label
className={`inline-flex max-w-[1600px] items-start gap-8 ${
disabled ? "cursor-not-allowed" : "cursor-pointer"
} ${className}`}
data-os={os}
>
{showIcon && (
// Track: 52px wide (no --spacing-52 token), 32px tall, overflow-visible so the knob shadow shows.
<button
type="button"
role="switch"
aria-checked={value}
aria-disabled={disabled}
disabled={disabled}
onClick={() => !disabled && onChange?.(!value)}
className={`relative shrink-0 h-32 w-[52px] rounded-full overflow-visible transition-colors ${trackClasses}`}
>
{/* Knob circle centered in the 40px State frame: center slides left-16 (OFF) ↔ left-36 (ON). */}
<span
className={`absolute rounded-full top-1/2 -translate-y-1/2 -translate-x-1/2 transition-all ${value ? "left-36" : "left-16"} ${circleClasses}`}
/>
</button>
)}
{showToggleContainer && (label || (showHelperContent && helperText)) && (
// Container hugs, capped at 1572px with 2px vertical gap (Figma "Container" node 436:3265).
<span className="flex max-w-[1572px] flex-col gap-2">
{label && (
<span className="typography-mobile-h4-semibold-16 text-[var(--components-toggle-switch-color-text-heading)]">
{label}
</span>
)}
{showHelperContent && helperText && (
<span className="typography-mobile-h4-regular-16 text-[var(--components-toggle-switch-color-text-muted)]">
{helperText}
</span>
)}
</span>
)}
</label>
);
return content;
};