Checkbox
Allow users to switch between checked, unchecked, and indeterminate states.
<script lang="ts">
import { Checkbox, Label } from "bits-ui";
import Check from "phosphor-svelte/lib/Check";
import Minus from "phosphor-svelte/lib/Minus";
</script>
<div class="flex items-center space-x-3">
<Checkbox.Root
id="terms"
aria-labelledby="terms-label"
class="peer inline-flex size-[25px] items-center justify-center rounded-md border border-muted bg-foreground transition-all duration-150 ease-in-out active:scale-98 data-[state=unchecked]:border-border-input data-[state=unchecked]:bg-background data-[state=unchecked]:hover:border-dark-40"
name="hello"
>
{#snippet children({ checked })}
<div class="inline-flex items-center justify-center text-background">
{#if checked === true}
<Check class="size-[15px]" weight="bold" />
{:else if checked === "indeterminate"}
<Minus class="size-[15px]" weight="bold" />
{/if}
</div>
{/snippet}
</Checkbox.Root>
<Label.Root
id="terms-label"
for="terms"
class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
Accept terms and conditions
</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;
}
}
Structure
<script lang="ts">
import { Checkbox } from "bits-ui";
</script>
<Checkbox.Root>
{#snippet children({ checked })}
{#if checked === "indeterminate"}
-
{:else if checked}
✅
{:else}
❌
{/if}
{/snippet}
</Checkbox.Root>
Reusable Components
It's recommended to use the Checkbox
primitive to create your own custom checkbox component that can be used throughout your application. In the example below, we're using the Checkbox
and Label
components to create a custom checkbox component.
<script lang="ts">
import { Checkbox, Label, useId, type WithoutChildrenOrChild } from "bits-ui";
let {
id = useId(),
checked = $bindable(false),
ref = $bindable(null),
...restProps
}: WithoutChildrenOrChild<Checkbox.RootProps> & {
labelText: string;
} = $props();
</script>
<Checkbox.Root bind:checked bind:ref {...restProps}>
{#snippet children({ checked })}
{#if checked === "indeterminate"}
-
{:else if checked}
✅
{:else}
❌
{/if}
{/snippet}
</Checkbox.Root>
<Label.Root for={id}>
{labelText}
</Label.Root>
You can then use the MyCheckbox
component in your application like so:
<script lang="ts">
import MyCheckbox from "$lib/components/MyCheckbox.svelte";
</script>
<MyCheckbox labelText="Enable notifications" />
Managing Checked State
The checked
prop is used to determine whether the checkbox is in one of three states: checked, unchecked, or indeterminate. Bits UI provides flexible options for controlling and synchronizing the Checkbox's checked state.
Two-Way Binding
Use the bind:checked
directive for effortless two-way synchronization between your local state and the Checkbox's internal state.
<script lang="ts">
import MyCheckbox from "$lib/components/MyCheckbox.svelte";
let myChecked = $state(false);
</script>
<button onclick={() => (myChecked = false)}> uncheck </button>
<MyCheckbox bind:checked={myChecked} />
This setup enables toggling the Checkbox via the custom button and ensures the local myChecked
state updates when the Checkbox changes through any internal means (e.g., clicking on the checkbox).
Change Handler
You can also use the onCheckedChange
prop to update local state when the Checkbox's checked
state changes. This is useful when you don't want two-way binding for one reason or another, or you want to perform additional logic when the Checkbox changes.
<script lang="ts">
import MyCheckbox from "$lib/components/MyCheckbox.svelte";
let myChecked = $state(false);
</script>
<MyCheckbox
checked={myChecked}
onCheckedChange={(checked) => {
myChecked = checked;
if (checked === "indeterminate") {
// do something different
}
// additional logic here.
}}
/>
Disabled State
You can disable the checkbox by setting the disabled
prop to true
.
<MyCheckbox disabled labelText="Enable notifications" />
HTML Forms
If you set the name
prop, a hidden checkbox input will be rendered to submit the value of the checkbox to a form. By default, the checkbox will be submitted with default checkbox values of on
or off
depending on the checked
prop. You can customize the value by setting the value
prop.
<MyCheckbox name="myCheckbox" value="on" labelText="Enable notifications" />
API Reference
The button component used to toggle the state of the checkbox.
Property | Type | Description |
---|---|---|
checked bindable prop | enum | The checkbox button's checked state. This can be a boolean or the string 'indeterminate', which would typically display a dash in the checkbox. Default: false |
onCheckedChange | function | A callback that is fired when the checkbox button's checked state changes. Default: undefined |
disabled | boolean | Whether or not the checkbox button is disabled. This prevents the user from interacting with it. Default: false |
required | boolean | Whether or not the checkbox is required. Default: false |
name | string | The name of the checkbox. If provided a hidden input will be render to use for form submission. If not provided, the hidden input will not be rendered. Default: undefined |
value | string | The value of the checkbox. This is what is submitted with the form when the checkbox is checked. Default: undefined |
ref bindable prop | 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 delegation docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-state | enum | The checkbox's state of checked, unchecked, or indeterminate. |
data-disabled | '' | Present when the checkbox is disabled. |
data-checkbox-root | '' | Present on the root element. |