Skip to content
edmi-ui

Checkbox

Toggle one option on or off; checked state is a solid primary fill, with an indeterminate state for partial selections.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/checkbox
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/checkbox
Terminal window
yarn dlx shadcn@latest add @edmi-ui/checkbox
Terminal window
bunx shadcn@latest add @edmi-ui/checkbox

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/checkbox
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/checkbox
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/checkbox
Terminal window
bunx shadcn-vue@latest add @edmi-ui/checkbox

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/checkbox.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/checkbox.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/checkbox.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/checkbox.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
<div className="flex flex-col gap-4">
<Label className="flex items-start gap-2.5 text-sm">
<Checkbox id="mandate" defaultChecked />
<span className="grid gap-0.5">
<span className="font-medium">Accept the mandate</span>
<span className="text-[12.5px] text-muted-foreground">
Weights and fees lock after launch.
</span>
</span>
</Label>
<Label className="flex items-center gap-2.5 text-sm">
<Checkbox
/>{" "}
Some selected
</Label>
<Label className="flex items-center gap-2.5 text-sm">
<Checkbox disabled /> Disabled
</Label>
</div>
<script setup lang="ts">
import { Checkbox } from "@/components/ui/checkbox";
</script>
<template>
<div class="flex flex-col gap-4">
<label class="flex items-start gap-2.5 text-sm">
<Checkbox id="mandate" default-value />
<span class="grid gap-0.5">
<span class="font-medium">Accept the mandate</span>
<span class="text-[12.5px] text-muted-foreground">Weights and fees lock after launch.</span>
</span>
</label>
<label class="flex items-center gap-2.5 text-sm">
<Checkbox /> Some selected
</label>
<label class="flex items-center gap-2.5 text-sm">
<Checkbox disabled /> Disabled
</label>
</div>
</template>
<script lang="ts">
import { Checkbox } from "$lib/components/ui/checkbox";
</script>
<div class="flex flex-col gap-4">
<label class="flex items-start gap-2.5 text-sm">
<Checkbox id="mandate" checked />
<span class="grid gap-0.5">
<span class="font-medium">Accept the mandate</span>
<span class="text-[12.5px] text-muted-foreground">Weights and fees lock after launch.</span>
</span>
</label>
<label class="flex items-center gap-2.5 text-sm">
<Checkbox indeterminate /> Some selected
</label>
<label class="flex items-center gap-2.5 text-sm">
<Checkbox disabled /> Disabled
</label>
</div>
registry/ui/checkbox.tsx
"use client";
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { cn } from "cn";
import { MinusIcon, CheckIcon } from "@phosphor-icons/react";
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation";
// Flat by default: checked = solid --primary fill. ✦ `elevation` raised/floating: only the checked box rises (bevel).
// Indeterminate uses the same fill.
function Checkbox({
className,
elevation,
...props
}: CheckboxPrimitive.Root.Props & {
/** ✦ depth: raised +1 / floating +2 make the checked box rise. */
elevation?: Elevation;
}) {
const level = useElevation(elevation, "control");
const raised = level === "raised" || level === "floating";
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer relative flex size-[18px] shrink-0 items-center justify-center rounded-[5px] border border-input bg-card text-primary-foreground transition-[box-shadow] outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error data-[invalid]:border-destructive data-[invalid]:shadow-ring-error group-has-disabled/field:opacity-50",
"data-[checked]:border-primary data-[checked]:bg-primary data-[indeterminate]:border-primary data-[indeterminate]:bg-primary",
raised &&
"data-[checked]:border-transparent data-[checked]:[background-image:var(--r1-p-face)] data-[checked]:shadow-btn-raised-primary data-[indeterminate]:border-transparent data-[indeterminate]:[background-image:var(--r1-p-face)] data-[indeterminate]:shadow-btn-raised-primary",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
render={(indicatorProps, state) => (
<span {...indicatorProps}>
{state.indeterminate ? (
<MinusIcon
strokeWidth={3}
/>
) : (
<CheckIcon
strokeWidth={3}
/>
)}
</span>
)}
/>
</CheckboxPrimitive.Root>
);
}
export { Checkbox };
registry/ui/checkbox/Checkbox.vue
<script setup lang="ts">
import type { CheckboxRootEmits, CheckboxRootProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { computed } from "vue"
import { Check, Minus } from "@lucide/vue"
import { reactiveOmit } from "@vueuse/core"
import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from "reka-ui"
import { cn } from "@/registry/edmi/lib/utils"
import { type Elevation, useElevation } from "@/registry/edmi/ui/elevation"
// Flat by default: checked = solid primary. `elevation` ✦ raised/floating: only the checked box rises (bevel).
// Indeterminate uses the same fill.
const props = withDefaults(defineProps<CheckboxRootProps & { class?: HTMLAttributes["class"], elevation?: Elevation }>(), { elevation: undefined })
const emits = defineEmits<CheckboxRootEmits>()
const delegatedProps = reactiveOmit(props, "class", "elevation")
const level = useElevation(() => props.elevation, "control")
const raised = computed(() => level.value === "raised" || level.value === "floating")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
// A bare `default-value` attribute arrives as "" (Reka types it loosely, so Vue does not boolean-cast it);
// read it as true so `<Checkbox default-value />` renders on, like `:default-value="true"`.
const defaultValue = computed(() => ((props.defaultValue as unknown) === "" ? true : props.defaultValue))
</script>
<template>
<CheckboxRoot
v-slot="slotProps"
data-slot="checkbox"
v-bind="forwarded"
:default-value="defaultValue"
:class="
cn(
'peer relative flex size-[18px] shrink-0 items-center justify-center rounded-[5px] border border-input bg-card text-primary-foreground transition-[box-shadow] outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error group-has-disabled/field:opacity-50',
'data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary',
raised && 'data-[state=checked]:border-transparent data-[state=checked]:[background-image:var(--r1-p-face)] data-[state=checked]:shadow-btn-raised-primary data-[state=indeterminate]:border-transparent data-[state=indeterminate]:[background-image:var(--r1-p-face)] data-[state=indeterminate]:shadow-btn-raised-primary',
props.class,
)
"
>
<CheckboxIndicator
data-slot="checkbox-indicator"
class="grid place-content-center text-current transition-none"
>
<slot v-bind="slotProps">
<Minus v-if="slotProps.state === 'indeterminate'" class="size-3.5" :stroke-width="3" />
<Check v-else class="size-3.5" :stroke-width="3" />
</slot>
</CheckboxIndicator>
</CheckboxRoot>
</template>
registry/ui/checkbox/index.ts
export { default as Checkbox } from "./Checkbox.vue";
src/lib/registry/ui/checkbox/checkbox.svelte
<script lang="ts">
import { Checkbox as CheckboxPrimitive } from "bits-ui";
import CheckIcon from "phosphor-svelte/lib/Check";
import MinusIcon from "phosphor-svelte/lib/Minus";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import { type Elevation, useElevation } from "$lib/registry/ui/elevation/index.js";
// Flat by default: checked = solid primary. elevation ✦ raised/floating: only the checked box rises (bevel).
let {
ref = $bindable(null),
checked = $bindable(false),
indeterminate = $bindable(false),
class: className,
elevation = "auto",
...restProps
}: WithoutChildrenOrChild<CheckboxPrimitive.RootProps> & {
/** ✦ depth: raised +1 / floating +2 make the checked box rise. */
elevation?: Elevation;
} = $props();
const level = useElevation(() => elevation, "control");
const raised = $derived(level.current === "raised" || level.current === "floating");
</script>
<CheckboxPrimitive.Root
bind:ref
data-slot="checkbox"
class={cn(
"peer relative flex size-[18px] shrink-0 items-center justify-center rounded-[5px] border border-input bg-card text-primary-foreground transition-[box-shadow] outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:shadow-ring disabled:cursor-not-allowed disabled:opacity-50 data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 aria-invalid:border-destructive aria-invalid:shadow-ring-error group-has-disabled/field:opacity-50",
"data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary",
raised &&
"data-[state=checked]:border-transparent data-[state=checked]:[background-image:var(--r1-p-face)] data-[state=checked]:shadow-btn-raised-primary data-[state=indeterminate]:border-transparent data-[state=indeterminate]:[background-image:var(--r1-p-face)] data-[state=indeterminate]:shadow-btn-raised-primary",
className
)}
bind:checked
bind:indeterminate
{...restProps}
>
{#snippet children({ checked, indeterminate })}
<div
data-slot="checkbox-indicator"
class="grid place-content-center text-current transition-none [&>svg]:size-3.5"
>
{#if checked}
<CheckIcon
stroke-width={3}
/>
{:else if indeterminate}
<MinusIcon
stroke-width={3}
/>
{/if}
</div>
{/snippet}
</CheckboxPrimitive.Root>
src/lib/registry/ui/checkbox/index.ts
import Root from "./checkbox.svelte";
export {
Root,
//
Root as Checkbox,
};

See the Elevation guide for the levels, roles and layered mode.

Only the checked (and indeterminate) box rises: raised (+1) or floating (+2) gives it a bevel. The unchecked box stays flat.

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

Raised (+1)

Flat (0)

Raised (+1)

Flat (0)

Raised (+1)

Built on Base UI Checkbox. Same props as shadcn.

Prop Type Default
checked / defaultChecked boolean false
indeterminate boolean false
onCheckedChange (checked: boolean, event) => void -
disabled boolean false
aria-invalid boolean -
elevation "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
  • Checked and indeterminate use a solid --primary fill (elevation raises the checked box) with a check/minus icon.