Skip to content
Lenso UI

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: :hover or [data-hovered="true"]
  • Focus: :focus-visible or [data-focus-visible="true"] (shows focus ring)
  • Disabled: :disabled or [aria-disabled="true"] (reduced opacity, no pointer events)
  • Pressed: :active or [data-pressed="true"]

API Reference

Switch

Inherits from React Aria Switch.

PropTypeDefaultDescription
size'sm' | 'md' | 'lg''md'The size of the switch
isSelectedbooleanfalseWhether the switch is on
defaultSelectedbooleanfalseWhether the switch is on by default (uncontrolled)
isDisabledbooleanfalseWhether the switch is disabled
isInvalidbooleanfalseWhether the switch is invalid
isReadOnlybooleanfalseWhether the switch is read only
isRequiredbooleanfalseWhether 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
namestring-The name of the input element, used when submitting an HTML form
valuestring-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
childrenReact.ReactNode | (values: SwitchRenderProps) => React.ReactNode-Switch content or render prop
renderDOMRenderFunction<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:

PropTypeDescription
isSelectedbooleanWhether the switch is currently on
isHoveredbooleanWhether the switch is hovered
isPressedbooleanWhether the switch is currently pressed
isFocusedbooleanWhether the switch is focused
isFocusVisiblebooleanWhether the switch is keyboard focused
isDisabledbooleanWhether the switch is disabled
isReadOnlybooleanWhether the switch is read only
isInvalidbooleanWhether the switch is invalid
isRequiredbooleanWhether the switch is required
state-State of the switch.

SwitchGroup

PropTypeDefaultDescription
orientation'horizontal' | 'vertical''vertical'The orientation of the switch group
childrenReact.ReactNode-The switch items to render
classNamestring-Additional CSS class names