Switch
A toggle switch component for boolean states
Usage
import { Switch, SwitchGroup, Label } from '@lenso/ui';"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";
export function Basic() { return ( <Switch> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Enable notifications </Switch.Content> </Switch> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Anatomy
import { Switch, Label, Description } from '@lenso/ui';
export default () => ( <Switch> <Switch.Control> <Switch.Thumb> <Switch.Icon/> {/* Optional */} </Switch.Thumb> </Switch.Control> <Switch.Content> <Label /> <Description /> {/* Optional */} </Switch.Content> </Switch>);For grouping multiple switches, use the SwitchGroup component:
import { Switch, SwitchGroup, Label } from '@lenso/ui';
export default () => ( <SwitchGroup> <Switch> <Switch.Control> <Switch.Thumb /> </Switch.Control> <Label>Option 1</Label> </Switch> <Switch> <Switch.Control> <Switch.Thumb /> </Switch.Control> <Label>Option 2</Label> </Switch> </SwitchGroup>);Examples
Sizes
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { display: "flex", gap: "1.5rem" } });export function Sizes() { return ( <div {...stylex.props(styles.root)}> <Switch size="sm"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Small </Switch.Content> </Switch> <Switch size="md"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Medium </Switch.Content> </Switch> <Switch size="lg"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Large </Switch.Content> </Switch> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Icons
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { BellFill, BellSlash, Check, Microphone, MicrophoneSlash, Moon, Power, Sun, VolumeFill, VolumeSlashFill,} from "@gravity-ui/icons";import { Switch } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { display: "flex", gap: ".75rem" }, icon: { width: ".75rem", height: ".75rem", color: "inherit", opacity: 1 }, offIcon: { opacity: 0.7 }, green: { backgroundColor: { default: "rgb(34 197 94 / .8)", ":is([data-slot='switch'][data-checked] *)": "rgb(34 197 94 / .8)", ":is([data-slot='switch'][data-checked]:hover *)": "rgb(34 197 94 / .8)", ":is([data-slot='switch'][data-checked]:active *)": "rgb(34 197 94 / .8)", }, }, red: { backgroundColor: { default: "rgb(239 68 68 / .8)", ":is([data-slot='switch'][data-checked] *)": "rgb(239 68 68 / .8)", ":is([data-slot='switch'][data-checked]:hover *)": "rgb(239 68 68 / .8)", ":is([data-slot='switch'][data-checked]:active *)": "rgb(239 68 68 / .8)", }, }, purple: { backgroundColor: { default: "rgb(168 85 247 / .8)", ":is([data-slot='switch'][data-checked] *)": "rgb(168 85 247 / .8)", ":is([data-slot='switch'][data-checked]:hover *)": "rgb(168 85 247 / .8)", ":is([data-slot='switch'][data-checked]:active *)": "rgb(168 85 247 / .8)", }, }, blue: { backgroundColor: { default: "rgb(59 130 246 / .8)", ":is([data-slot='switch'][data-checked] *)": "rgb(59 130 246 / .8)", ":is([data-slot='switch'][data-checked]:hover *)": "rgb(59 130 246 / .8)", ":is([data-slot='switch'][data-checked]:active *)": "rgb(59 130 246 / .8)", }, },});const icons = { check: { off: Power, on: Check, selectedControl: styles.green }, darkMode: { off: Moon, on: Sun, selectedControl: undefined }, microphone: { off: Microphone, on: MicrophoneSlash, selectedControl: styles.red }, notification: { off: BellSlash, on: BellFill, selectedControl: styles.purple }, volume: { off: VolumeFill, on: VolumeSlashFill, selectedControl: styles.blue },};export function WithIcons() { return ( <div {...stylex.props(styles.root)}> {Object.entries(icons).map(([key, value]) => ( <Switch key={key} defaultChecked aria-label={key} size="lg" render={(props, { checked }) => ( <span {...props}> <Switch.Content> <Switch.Control xstyle={checked && value.selectedControl}> <Switch.Thumb> <Switch.Icon> {checked ? ( <value.on {...stylex.props(styles.icon)} aria-hidden="true" /> ) : ( <value.off {...stylex.props(styles.icon, styles.offIcon)} aria-hidden="true" /> )} </Switch.Icon> </Switch.Thumb> </Switch.Control> </Switch.Content> </span> )} /> ))} </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Disabled
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";export function Disabled() { return ( <Switch disabled> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Enable notifications </Switch.Content> </Switch> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Without Label
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";export function WithoutLabel() { return ( <Switch aria-label="Enable notifications"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> </Switch.Content> </Switch> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
With Description
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Description, Switch, TextField } from "@lenso/ui";import { switchSupportingStyles } from "@lenso/tokens/switch";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { maxWidth: "24rem" } });export function WithDescription() { return ( <div {...stylex.props(styles.root)}> <TextField> <Switch aria-describedby="public-profile-help"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Public profile </Switch.Content> <Description id="public-profile-help" xstyle={switchSupportingStyles.direct}> Allow others to see your profile information </Description> </Switch> </TextField> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Default Selected
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";export function DefaultSelected() { return ( <Switch defaultChecked> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Enable notifications </Switch.Content> </Switch> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Controlled
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";import { useState } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { display: "flex", flexDirection: "column", gap: "1rem" }, status: { fontSize: ".875rem", color: "var(--muted)" },});export function Controlled() { const [isSelected, setIsSelected] = useState(false); return ( <div {...stylex.props(styles.root)}> <Switch checked={isSelected} onCheckedChange={setIsSelected}> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Enable notifications </Switch.Content> </Switch> <p {...stylex.props(styles.status)}>Switch is {isSelected ? "on" : "off"}</p> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Label Position
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { display: "flex", flexDirection: "column", gap: "1rem" } });export function LabelPosition() { return ( <div {...stylex.props(styles.root)}> <Switch> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Label after </Switch.Content> </Switch> <Switch> <Switch.Content> Label before <Switch.Control> <Switch.Thumb /> </Switch.Control> </Switch.Content> </Switch> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Group
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch, SwitchGroup } from "@lenso/ui";export function Group() { return ( <SwitchGroup> <Switch name="notifications"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Allow Notifications </Switch.Content> </Switch> <Switch name="marketing"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Marketing emails </Switch.Content> </Switch> <Switch name="social"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Social media updates </Switch.Content> </Switch> </SwitchGroup> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Group Horizontal
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch, SwitchGroup } from "@lenso/ui";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ root: { overflowX: "auto" } });export function GroupHorizontal() { return ( <SwitchGroup xstyle={styles.root} orientation="horizontal"> <Switch name="notifications"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Notifications </Switch.Content> </Switch> <Switch name="marketing"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Marketing </Switch.Content> </Switch> <Switch name="social"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Social </Switch.Content> </Switch> </SwitchGroup> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Form Integration
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Button, Switch, SwitchGroup } from "@lenso/ui";import type { FormEvent } from "react";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ form: { display: "flex", flexDirection: "column", gap: "1rem" }, submit: { marginTop: "1rem" },});export function Form() { const handleSubmit = (e: FormEvent<HTMLFormElement>) => { e.preventDefault(); const formData = new FormData(e.currentTarget); alert( `Form submitted with:\n${Array.from(formData.entries()) .map(([key, value]) => `${key}: ${value}`) .join("\n")}`, ); }; return ( <form {...stylex.props(styles.form)} onSubmit={handleSubmit}> <SwitchGroup> <Switch name="notifications" value="on"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Enable notifications </Switch.Content> </Switch> <Switch defaultChecked name="newsletter" value="on"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Subscribe to newsletter </Switch.Content> </Switch> <Switch name="marketing" value="on"> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Receive marketing updates </Switch.Content> </Switch> </SwitchGroup> <Button xstyle={styles.submit} size="sm" type="submit" variant="primary"> Submit </Button> </form> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Render Props
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";export function RenderProps() { return ( <Switch render={(props, { checked }) => ( <span {...props}> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> {checked ? "Enabled" : "Disabled"} </Switch.Content> </span> )} /> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Render Function
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Switch } from "@lenso/ui";export function RenderFunction() { return ( <Switch render={(props) => <div {...props} data-custom="foo" />}> <Switch.Content> <Switch.Control> <Switch.Thumb /> </Switch.Control> Enable notifications </Switch.Content> </Switch> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Customization
Tailwind CSS
"use client";/** Adapted from HeroUI v3.2.6. SPDX-License-Identifier: Apache-2.0 */import { Description, Label, Switch, TextField } from "@lenso/ui";import { switchSupportingStyles } from "@lenso/tokens/switch";import * as stylex from "@stylexjs/stylex";const styles = stylex.create({ control: { "--switch-control-bg-checked": "var(--success)", "--switch-control-bg-checked-hover": "var(--success)", }, copy: { display: "flex", flexDirection: "column", gap: ".125rem" },});export function CustomStyles() { return ( <TextField> <Switch id="autosave" aria-describedby="autosave-help"> <Switch.Content> <Switch.Control xstyle={styles.control}> <Switch.Thumb /> </Switch.Control> <div {...stylex.props(styles.copy)}> <Label>Auto-save drafts</Label> <Description id="autosave-help" xstyle={switchSupportingStyles.direct}> Changes are saved as you type. </Description> </div> </Switch.Content> </Switch> </TextField> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
Global CSS
To customize the Switch component classes, you can use the @layer components directive.
Learn more.
@layer components { .switch { @apply inline-flex gap-3 items-center; }
.switch__control { @apply h-5 w-8 bg-gray-400 data-[selected=true]:bg-blue-500; }
.switch__thumb { @apply bg-white shadow-sm; }
.switch__content { @apply flex flex-col gap-1; }
.switch__icon { @apply h-3 w-3 text-current; }}Styling Reference
HeroUI follows the BEM methodology to ensure component variants and states are reusable and easy to customize.
CSS Classes
Switch Classes [!toc]
The Switch component uses these CSS classes (View source styles):
.switch- Base switch container.switch__content- Optional content container.switch__control- Switch control track.switch__thumb- Switch thumb that moves.switch__icon- Optional icon inside the thumb.switch--sm- Small size variant.switch--md- Medium size variant (default).switch--lg- Large size variant
SwitchGroup Classes [!toc]
The SwitchGroup component uses these CSS classes (View source styles):
.switch-group- Switch group container.switch-group__items- Container for switch items.switch-group--horizontal- Horizontal layout.switch-group--vertical- Vertical layout (default)
Interactive States
The switch supports both CSS pseudo-classes and data attributes for flexibility:
- Selected:
[data-selected="true"](thumb position and background color change) - Hover:
:hoveror[data-hovered="true"] - Focus:
:focus-visibleor[data-focus-visible="true"](shows focus ring) - Disabled:
:disabledor[aria-disabled="true"](reduced opacity, no pointer events) - Pressed:
:activeor[data-pressed="true"]
API Reference
Switch
Inherits from React Aria Switch.
| Prop | Type | Default | Description |
|---|---|---|---|
size | 'sm' | 'md' | 'lg' | 'md' | The size of the switch |
isSelected | boolean | false | Whether the switch is on |
defaultSelected | boolean | false | Whether the switch is on by default (uncontrolled) |
isDisabled | boolean | false | Whether the switch is disabled |
isInvalid | boolean | false | Whether the switch is invalid |
isReadOnly | boolean | false | Whether the switch is read only |
isRequired | boolean | false | Whether the switch must be selected |
validate | (value: boolean) => ValidationError | true | null | undefined | - | Custom validation function |
validationBehavior | 'native' | 'aria' | 'native' | Whether to use native HTML form validation or ARIA |
name | string | - | The name of the input element, used when submitting an HTML form |
value | string | - | The value of the input element, used when submitting an HTML form |
onChange | (isSelected: boolean) => void | - | Handler called when the switch value changes |
onPress | (e: PressEvent) => void | - | Handler called when the switch is pressed |
children | React.ReactNode | (values: SwitchRenderProps) => React.ReactNode | - | Switch content or render prop |
render | DOMRenderFunction<keyof React.JSX.IntrinsicElements, SwitchRenderProps> | - | Overrides the default DOM element with a custom render function. |
Render Props
When using the render prop pattern, these values are provided:
| Prop | Type | Description |
|---|---|---|
isSelected | boolean | Whether the switch is currently on |
isHovered | boolean | Whether the switch is hovered |
isPressed | boolean | Whether the switch is currently pressed |
isFocused | boolean | Whether the switch is focused |
isFocusVisible | boolean | Whether the switch is keyboard focused |
isDisabled | boolean | Whether the switch is disabled |
isReadOnly | boolean | Whether the switch is read only |
isInvalid | boolean | Whether the switch is invalid |
isRequired | boolean | Whether the switch is required |
state | - | State of the switch. |
SwitchGroup
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | 'horizontal' | 'vertical' | 'vertical' | The orientation of the switch group |
children | React.ReactNode | - | The switch items to render |
className | string | - | Additional CSS class names |