Switch
A toggle control enabling users to switch between "on" and "off" states.
<script lang="ts">
import { Label, Switch } from "bits-ui";
</script>
<div class="flex items-center space-x-3">
<Switch.Root
id="dnd"
name="hello"
class="peer inline-flex h-[36px] min-h-[36px] w-[60px] shrink-0 cursor-pointer items-center rounded-full px-[3px] transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-foreground data-[state=unchecked]:bg-dark-10 data-[state=unchecked]:shadow-mini-inset dark:data-[state=checked]:bg-foreground"
>
<Switch.Thumb
class="pointer-events-none block size-[30px] shrink-0 rounded-full bg-background transition-transform data-[state=checked]:translate-x-6 data-[state=unchecked]:translate-x-0 data-[state=unchecked]:shadow-mini dark:border dark:border-background/30 dark:bg-foreground dark:shadow-popover dark:data-[state=unchecked]:border"
/>
</Switch.Root>
<Label.Root for="dnd" class="text-sm font-medium">Do not disturb</Label.Root>
</div>
import typography from "@tailwindcss/typography";
import animate from "tailwindcss-animate";
import { fontFamily } from "tailwindcss/defaultTheme";
/** @type {import('tailwindcss').Config} */
export default {
darkMode: "class",
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
container: {
center: true,
screens: {
"2xl": "1440px",
},
},
extend: {
colors: {
border: {
DEFAULT: "hsl(var(--border-card))",
input: "hsl(var(--border-input))",
"input-hover": "hsl(var(--border-input-hover))",
},
background: {
DEFAULT: "hsl(var(--background) / <alpha-value>)",
alt: "hsl(var(--background-alt) / <alpha-value>)",
},
foreground: {
DEFAULT: "hsl(var(--foreground) / <alpha-value>)",
alt: "hsl(var(--foreground-alt) / <alpha-value>)",
},
muted: {
DEFAULT: "hsl(var(--muted) / <alpha-value>)",
foreground: "hsl(var(--muted-foreground))",
},
dark: {
DEFAULT: "hsl(var(--dark) / <alpha-value>)",
4: "hsl(var(--dark-04))",
10: "hsl(var(--dark-10))",
40: "hsl(var(--dark-40))",
},
accent: {
DEFAULT: "hsl(var(--accent) / <alpha-value>)",
foreground: "hsl(var(--accent-foreground) / <alpha-value>)",
},
destructive: {
DEFAULT: "hsl(var(--destructive) / <alpha-value>)",
},
contrast: {
DEFAULT: "hsl(var(--contrast) / <alpha-value>)",
},
},
fontFamily: {
sans: ["Inter", ...fontFamily.sans],
mono: ["Source Code Pro", ...fontFamily.mono],
alt: ["Courier", ...fontFamily.sans],
},
fontSize: {
xxs: "10px",
},
borderWidth: {
6: "6px",
},
borderRadius: {
card: "16px",
"card-lg": "20px",
"card-sm": "10px",
input: "9px",
button: "5px",
"5px": "5px",
"9px": "9px",
"10px": "10px",
"15px": "15px",
},
height: {
input: "3rem",
"input-sm": "2.5rem",
},
boxShadow: {
mini: "var(--shadow-mini)",
"mini-inset": "var(--shadow-mini-inset)",
popover: "var(--shadow-popover)",
kbd: "var(--shadow-kbd)",
btn: "var(--shadow-btn)",
card: "var(--shadow-card)",
"date-field-focus": "var(--shadow-date-field-focus)",
},
opacity: {
8: "0.08",
},
scale: {
80: ".80",
98: ".98",
99: ".99",
},
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--bits-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--bits-accordion-content-height)" },
to: { height: "0" },
},
"caret-blink": {
"0%,70%,100%": { opacity: "1" },
"20%,50%": { opacity: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
"caret-blink": "caret-blink 1.25s ease-out infinite",
},
},
plugins: [typography, animate],
};
@import url("https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,400;0,500;0,600;0,700;1,400;1,500;1,600;1,700&display=swap");
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* Colors */
--background: 0 0% 100%;
--background-alt: 0 0% 100%;
--foreground: 0 0% 9%;
--foreground-alt: 0 0% 32%;
--muted: 240 5% 96%;
--muted-foreground: 0 0% 9% / 0.4;
--border: 240 6% 10%;
--border-input: 240 6% 10% / 0.17;
--border-input-hover: 240 6% 10% / 0.4;
--border-card: 240 6% 10% / 0.1;
--dark: 240 6% 10%;
--dark-10: 240 6% 10% / 0.1;
--dark-40: 240 6% 10% / 0.4;
--dark-04: 240 6% 10% / 0.04;
--accent: 204 94% 94%;
--accent-foreground: 204 80% 16%;
--destructive: 347 77% 50%;
/* black */
--constrast: 0 0% 0%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.04) inset;
--shadow-popover: 0px 7px 12px 3px hsla(var(--dark-10));
--shadow-kbd: 0px 2px 0px 0px rgba(0, 0, 0, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.03);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.04);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(24, 24, 27, 0.17);
}
.dark {
/* Colors */
--background: 0 0% 5%;
--background-alt: 0 0% 8%;
--foreground: 0 0% 95%;
--foreground-alt: 0 0% 70%;
--muted: 240 4% 16%;
--muted-foreground: 0 0% 100% / 0.4;
--border: 0 0% 96%;
--border-input: 0 0% 96% / 0.17;
--border-input-hover: 0 0% 96% / 0.4;
--border-card: 0 0% 96% / 0.1;
--dark: 0 0% 96%;
--dark-40: 0 0% 96% / 0.4;
--dark-10: 0 0% 96% / 0.1;
--dark-04: 0 0% 96% / 0.04;
--accent: 204 90 90%;
--accent-foreground: 204 94% 94%;
--destructive: 350 89% 60%;
/* white */
--constrast: 0 0% 100%;
/* Shadows */
--shadow-mini: 0px 1px 0px 1px rgba(0, 0, 0, 0.3);
--shadow-mini-inset: 0px 1px 0px 0px rgba(0, 0, 0, 0.5) inset;
--shadow-popover: 0px 7px 12px 3px hsla(0deg 0% 0% / 30%);
--shadow-kbd: 0px 2px 0px 0px rgba(255, 255, 255, 0.07);
--shadow-btn: 0px 1px 0px 1px rgba(0, 0, 0, 0.2);
--shadow-card: 0px 2px 0px 1px rgba(0, 0, 0, 0.4);
--shadow-date-field-focus: 0px 0px 0px 3px rgba(244, 244, 245, 0.1);
}
}
@layer base {
* {
@apply border-border;
}
html {
-webkit-text-size-adjust: 100%;
font-variation-settings: normal;
}
body {
@apply bg-background text-foreground;
font-feature-settings:
"rlig" 1,
"calt" 1;
}
/* Mobile tap highlight */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-tap-highlight-color */
html {
-webkit-tap-highlight-color: rgba(128, 128, 128, 0.5);
}
::selection {
background: #fdffa4;
color: black;
}
/* === Scrollbars === */
::-webkit-scrollbar {
@apply w-2;
@apply h-2;
}
::-webkit-scrollbar-track {
@apply !bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply rounded-card-lg !bg-dark-10;
}
::-webkit-scrollbar-corner {
background: rgba(0, 0, 0, 0);
}
/* Firefox */
/* https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-color#browser_compatibility */
html {
scrollbar-color: var(--bg-muted);
}
.antialised {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.step {
counter-increment: step;
}
.step:before {
@apply absolute inline-flex h-9 w-9 items-center justify-center rounded-full border-4 border-background bg-muted text-center -indent-px font-mono text-base font-medium;
@apply ml-[-50px] mt-[-4px];
content: counter(step);
}
}
@layer components {
*:not(body):not(.focus-override) {
outline: none !important;
&:focus-visible {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
}
.link {
@apply inline-flex items-center gap-1 rounded-sm font-medium underline underline-offset-4 hover:text-foreground/80 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
input[type="number"] {
-moz-appearance: textfield;
}
}
Overview
The Switch component provides an intuitive and accessible toggle control, allowing users to switch between two states, typically "on" and "off". This component is commonly used for enabling or disabling features, toggling settings, or representing boolean values in forms. The Switch offers a more visual and interactive alternative to traditional checkboxes for binary choices.
Key Features
- Accessibility: Built with WAI-ARIA guidelines in mind, ensuring keyboard navigation and screen reader support.
- State Management: Internally manages the on/off state, with options for controlled and uncontrolled usage.
- Style-able: Data attributes allow for smooth transitions between states and custom styles.
- HTML Forms: Can render a hidden input element for form submissions.
Architecture
The Switch component is composed of two main parts:
- Root: The main container component that manages the state and behavior of the switch.
- Thumb: The "movable" part of the switch that indicates the current state.
Structure
Here's an overview of how the Switch component is structured in code:
<script lang="ts">
import { Switch } from "bits-ui";
</script>
<Switch.Root>
<Switch.Thumb />
</Switch.Root>
Reusable Components
It's recommended to use the Switch
primitives to create your own custom switch component that can be used throughout your application.
In the example below, we're using the Checkbox
and Label
components to create a custom switch component.
<script lang="ts">
import { Switch, Label, useId, type WithoutChildrenOrChild } from "bits-ui";
let {
id = useId(),
checked = $bindable(false),
ref = $bindable(null),
...restProps
}: WithoutChildrenOrChild<Switch.RootProps> & {
labelText: string;
} = $props();
</script>
<Switch.Root bind:checked bind:ref {id} {...restProps}>
<Switch.Thumb />
</Switch.Root>
<Label.Root for={id}>{labelText}</Label.Root>
You can then use the MySwitch
component in your application like so:
<script lang="ts">
import MySwitch from "$lib/components/MySwitch.svelte";
let notifications = $state(true);
</script>
<MySwitch bind:checked={notifications} labelText="Enable notifications" />
Managing Checked State
Bits UI offers several approaches to manage and synchronize the Switch's checked state, catering to different levels of control and integration needs.
1. Two-Way Binding
For seamless state synchronization, use Svelte's bind:checked
directive. This method automatically keeps your local state in sync with the switch's internal state.
<script lang="ts">
import { Switch } from "bits-ui";
let myChecked = $state(true);
</script>
<button onclick={() => (myChecked = false)}> uncheck </button>
<Switch.Root bind:checked={myChecked} />
Key Benefits
- Simplifies state management
- Automatically updates
myChecked
when the switch changes (e.g., via clicking on the switch) - Allows external control (e.g., checking via a separate button/programmatically)
2. Change Handler
For more granular control or to perform additional logic on state changes, use the onCheckedChange
prop. This approach is useful when you need to execute custom logic alongside state updates.
<script lang="ts">
import { Switch } from "bits-ui";
let myChecked = $state(false);
</script>
<Switch.Root
checked={myChecked}
onCheckedChange={(checked) => {
myChecked = checked;
// additional logic here.
}}
/>
Use Cases
- Implementing custom behaviors on checked/unchecked
- Integrating with external state management solutions
- Triggering side effects (e.g., logging, data fetching)
3. Fully Controlled
For complete control over the switch's checked state, use the controlledChecked
prop. This approach requires you to manually manage the checked state, giving you full control over when and how the component responds to change events.
To implement controlled state:
- Set the
controlledChecked
prop totrue
on theSwitch.Root
component. - Provide a
checked
prop toSwitch.Root
, which should be a variable holding the current state. - Implement an
onCheckedChange
handler to update the state when the internal state changes.
<script lang="ts">
import { Switch } from "bits-ui";
let myChecked = $state(false);
</script>
<Switch.Root controlledChecked checked={myChecked} onCheckedChange={(c) => (myChecked = c)}>
<!-- ... -->
</Switch.Root>
When to Use
- Implementing complex checked/unchecked logic
- Coordinating multiple UI elements
- Debugging state-related issues
Note
While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully.
For more in-depth information on controlled components and advanced state management techniques, refer to our Controlled State documentation.
Disabled State
You can disable the switch by setting the disabled
prop to true
.
<Switch.Root disabled>
<!-- ...-->
</Switch.Root>
HTML Forms
If you pass the name
prop to Switch.Root
, a hidden input element will be rendered to submit the value of the switch to a form.
By default, the input will be submitted with the default checkbox value of 'on'
if the switch is checked.
<Switch.Root name="dnd">
<!-- ... -->
</Switch.Root>
Custom Input Value
If you'd prefer to submit a different value, you can use the value
prop to set the value of the hidden input.
For example, if you wanted to submit a string value, you could do the following:
<Switch.Root name="dnd" value="hello">
<!-- ... -->
<Switch.Thumb />
</Switch.Root>
Required
If you want to make the switch required, you can use the required
prop.
<Switch.Root required>
<!-- ... -->
</Switch.Root>
This will apply the required
attribute to the hidden input element, ensuring that proper form submission is enforced.
API Reference
The root switch component used to set and manage the state of the switch.
Property | Type | Description |
---|---|---|
checked $bindable | boolean | Whether or not the switch is checked. Default: false |
onCheckedChange | function | A callback function called when the checked state of the switch changes. Default: undefined |
controlledChecked | boolean | Whether or not the Default: false |
disabled | boolean | Whether or not the switch is disabled. Default: false |
name | string | The name of the hidden input element, used to identify the input in form submissions. Default: undefined |
required | boolean | Whether or not the switch is required to be checked. Default: false |
value | string | The value of the hidden input element to be used in form submissions when the switch is checked. Default: undefined |
ref $bindable | HTMLButtonElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | '' | The switch's checked state. |
data-checked | '' | Present when the switch is checked. |
data-disabled | '' | Present when the switch is disabled. |
data-switch-root | '' | Present on the root element. |
The thumb on the switch used to indicate the switch's state.
Property | Type | Description |
---|---|---|
ref $bindable | HTMLSpanElement | The underlying DOM element being rendered. You can bind to this to get a reference to the element. Default: undefined |
children | Snippet | The children content to render. Default: undefined |
child | Snippet | Use render delegation to render your own element. See Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | '' | The switch's checked state. |
data-checked | '' | Present when the switch is checked. |
data-switch-thumb | '' | Present on the thumb element. |