Checkbox
Vue formStandalone or in-form binary toggle, built on reka-ui. Supports indeterminate state for tri-state lists, sizes, and proper keyboard / screen-reader behavior. Pair with Label for clickable text.
Also available for React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/checkbox.json$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/checkbox.json$ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/checkbox.json$ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/checkbox.json
Or with the named registry:
npx shadcn-vue@latest add @uipkge/checkbox
Examples
Schema
Type aliases from this item's source — use them to shape the data you pass in.
CheckboxOption interface CheckboxOption {
label: string
value: string
disabled?: boolean
} Dependencies
Used by
Files (3)
-
app/components/ui/checkbox/Checkbox.vue 9.8 kB
<script setup lang="ts"> import type { CheckboxRootProps } from 'reka-ui' import type { ComputedRef, HTMLAttributes } from 'vue' import { computed, getCurrentInstance, inject } from 'vue' import { reactiveOmit } from '@vueuse/core' import { Check, Minus, Loader2 } from 'lucide-vue-next' import { CheckboxIndicator, CheckboxRoot, useForwardPropsEmits } from 'reka-ui' import { cn } from '@/lib/utils' // CLAUDE.md mandate: `interface Props extends ReakUiX` bails in Vue 3.5+ // because reka-ui's package.json lacks an `exports.types` condition. The // `defineProps<External & Extra>()` intersection delegates type extraction // back to the installed TypeScript and is the documented workaround. export type CheckboxProps = Omit<CheckboxRootProps<boolean | string>, 'defaultValue' | 'modelValue'> & { class?: HTMLAttributes['class'] /** The controlled checked state of the checkbox. Can be binded with v-model. */ modelValue?: boolean | 'indeterminate' | null /** The value given as data when submitted with a name. */ value?: string /** Id of the element */ id?: string /** The value used when the checkbox is checked. Defaults to `true`. */ trueValue?: boolean | string /** The value used when the checkbox is unchecked. Defaults to `false`. */ falseValue?: boolean | string /** Size of the checkbox */ size?: 'sm' | 'md' | 'lg' /** Custom color for the checked state - matches Vuetify color system */ color?: 'primary' | 'secondary' | 'success' | 'warning' | 'error' | 'info' | string /** Label text displayed next to the checkbox */ label?: string /** Hint text shown below the checkbox */ hint?: string /** Error message to display */ errorMessages?: string | string[] /** Whether to show error state */ error?: boolean /** When `true`, prevents the user from interacting with the checkbox */ disabled?: boolean /** When `true`, the checkbox is in readonly state */ readonly?: boolean /** When `true`, shows an indeterminate state */ indeterminate?: boolean /** Density of the checkbox - affects spacing */ density?: 'compact' | 'default' | 'comfortable' /** When `true`, the icon is hidden */ hideIcon?: boolean /** Loading state - shows a spinner */ loading?: boolean /** Ripple effect on click */ ripple?: boolean /** Custom icon to show when checked */ checkedIcon?: any /** Custom icon to show when indeterminate */ indeterminateIcon?: any /** Label position - before or after the checkbox */ labelPosition?: 'before' | 'after' /** Whether the checkbox appears flat (no elevation) */ flat?: boolean /** Whether the checkbox has a background color */ bgColor?: boolean /** Inline text style */ inline?: boolean /** Name attribute for form submission */ name?: string } const props = withDefaults(defineProps<CheckboxProps>(), { size: 'md', density: 'default', color: 'primary', trueValue: true, falseValue: false, hideIcon: false, ripple: true, labelPosition: 'after', flat: false, bgColor: false, inline: false, }) const emits = defineEmits<{ 'update:modelValue': [value: boolean | 'indeterminate'] change: [value: boolean | 'indeterminate'] focus: [event: FocusEvent] blur: [event: FocusEvent] }>() const delegatedProps = reactiveOmit( props, 'class', 'size', 'color', 'label', 'hint', 'errorMessages', 'error', 'disabled', 'readonly', 'indeterminate', 'density', 'hideIcon', 'loading', 'ripple', 'checkedIcon', 'indeterminateIcon', 'labelPosition', 'flat', 'bgColor', 'inline', 'trueValue', 'falseValue', 'modelValue', 'name', ) const forwarded = useForwardPropsEmits(delegatedProps, emits) // Injected group context const groupContext = inject<{ name?: ComputedRef<string | undefined> } | null>('checkboxGroupContext', null) const actualName = computed(() => props.name ?? groupContext?.name?.value) // Size classes const sizeClasses = { sm: 'size-3.5', md: 'size-4', lg: 'size-5', } const iconSizes = { sm: 'size-2.5', md: 'size-3.5', lg: 'size-4', } // Color classes for checked state const colorClasses: Record<string, string> = { primary: 'data-[state=checked]:bg-primary data-[state=checked]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:border-primary', secondary: 'data-[state=checked]:bg-secondary data-[state=checked]:border-secondary data-[state=indeterminate]:bg-secondary data-[state=indeterminate]:border-secondary', success: 'data-[state=checked]:bg-[var(--success)] data-[state=checked]:border-[var(--success)] data-[state=indeterminate]:bg-[var(--success)] data-[state=indeterminate]:border-[var(--success)]', warning: 'data-[state=checked]:bg-[var(--warning)] data-[state=checked]:border-[var(--warning)] data-[state=indeterminate]:bg-[var(--warning)] data-[state=indeterminate]:border-[var(--warning)]', error: 'data-[state=checked]:bg-destructive data-[state=checked]:border-destructive data-[state=indeterminate]:bg-destructive data-[state=indeterminate]:border-destructive', info: 'data-[state=checked]:bg-[var(--info)] data-[state=checked]:border-[var(--info)] data-[state=indeterminate]:bg-[var(--info)] data-[state=indeterminate]:border-[var(--info)]', } // Density classes const densityClasses = { compact: 'gap-1', default: 'gap-2', comfortable: 'gap-3', } // Build error state const hasError = computed(() => { if (props.error) return true if ( props.errorMessages && (typeof props.errorMessages === 'string' ? props.errorMessages : props.errorMessages.length > 0) ) return true return false }) // Determine if the checkbox is in an indeterminate state const isIndeterminate = computed(() => { return props.indeterminate || props.modelValue === 'indeterminate' }) // Detect whether the parent bound v-model (looks for an onUpdate:modelValue // listener) and whether they passed an initial modelValue prop at all (Vue // auto-coerces Boolean-union props to `false` when not provided, so a prop // check alone can't distinguish controlled vs unbound usage). const instance = getCurrentInstance() const isControlled = computed(() => Boolean(instance?.vnode?.props?.['onUpdate:modelValue'])) const userPassedModelValue = computed(() => { const raw = instance?.vnode?.props return Boolean(raw && ('modelValue' in raw || 'model-value' in raw)) }) // Bind model-value only when actually controlled. Otherwise seed Reka's // internal state via default-value so click toggles work. const checkboxStateBindings = computed(() => { if (isIndeterminate.value) return { 'model-value': 'indeterminate' as const } if (isControlled.value) return { 'model-value': props.modelValue } if (userPassedModelValue.value) return { 'default-value': Boolean(props.modelValue) } return { 'default-value': false } }) // Build checkbox classes const checkboxClasses = computed(() => { return cn( 'peer border-input data-[state=checked]:text-primary-foreground data-[state=indeterminate]:text-primary-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive shrink-0 rounded-[4px] border shadow-xs transition-colors duration-200 outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50', sizeClasses[props.size], colorClasses[props.color] || colorClasses.primary, hasError.value && 'border-destructive data-[state=checked]:!bg-destructive data-[state=checked]:!border-destructive data-[state=indeterminate]:!bg-destructive data-[state=indeterminate]:!border-destructive', props.flat && 'shadow-none', props.class, ) }) </script> <template> <div class="flex items-start" :class="[densityClasses[density], inline ? 'inline-flex' : 'flex-col']"> <label v-if="label && labelPosition === 'before'" :for="id" class="mr-2 cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" :class="[hasError ? 'text-destructive' : '', disabled && 'cursor-not-allowed opacity-50']" > {{ label }} </label> <div class="flex items-center"> <CheckboxRoot v-bind="{ 'data-slot': 'checkbox', ...forwarded, ...checkboxStateBindings }" :id="id" v-slot="slotProps" :value="value" :disabled="disabled" :name="actualName" :class="checkboxClasses" @update:model-value="(val: boolean | 'indeterminate') => emits('update:modelValue', val)" > <CheckboxIndicator data-uipkge data-slot="checkbox-indicator" class="grid place-content-center text-current transition-none" :force-mount="isIndeterminate || hideIcon" > <slot v-bind="slotProps"> <Loader2 v-if="loading" :class="[iconSizes[size], 'animate-spin']" /> <Minus v-else-if="isIndeterminate" :class="iconSizes[size]" /> <Check v-else-if="!hideIcon" :class="iconSizes[size]" /> </slot> </CheckboxIndicator> </CheckboxRoot> <label v-if="label && labelPosition === 'after'" :for="id" class="ml-2 cursor-pointer text-sm leading-none font-medium select-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" :class="[hasError ? 'text-destructive' : '', disabled && 'cursor-not-allowed opacity-50']" > {{ label }} </label> </div> <p v-if="hint && !hasError" class="text-muted-foreground mt-1 text-xs"> {{ hint }} </p> <div v-if="hasError" class="mt-1 flex flex-col gap-0.5"> <p v-if="typeof errorMessages === 'string'" class="text-destructive text-xs"> {{ errorMessages }} </p> <template v-else> <p v-for="(msg, i) in errorMessages" :key="i" class="text-destructive text-xs"> {{ msg }} </p> </template> </div> </div> </template> -
app/components/ui/checkbox/CheckboxGroup.vue 3.5 kB
<script setup lang="ts"> import { computed, provide } from 'vue' import type { ComputedRef, HTMLAttributes } from 'vue' import { reactiveOmit } from '@vueuse/core' import { CheckboxGroupRoot, useForwardProps } from 'reka-ui' import { cn } from '@/lib/utils' import Checkbox from './Checkbox.vue' export interface CheckboxOption { label: string value: string disabled?: boolean } export interface CheckboxGroupProps { class?: HTMLAttributes['class'] /** The value of the checkbox items that should be checked when initially rendered. */ defaultValue?: string[] /** When `true`, prevents the user from interacting with the checkboxes. */ disabled?: boolean /** Specifies whether the checkbox group is in an error state */ error?: boolean /** Error messages to display */ errorMessages?: string | string[] /** Label for the group */ label?: string /** Hint text for the group */ hint?: string /** The orientation of the checkboxes */ orientation?: 'horizontal' | 'vertical' /** Whether to show a border around the group */ bordered?: boolean /** Density of the checkboxes */ density?: 'compact' | 'default' | 'comfortable' /** Options to render as checkboxes automatically */ options?: (string | CheckboxOption)[] /** Name attribute for all checkboxes in the group */ name?: string /** Inline layout (alias for horizontal) */ inline?: boolean } const props = withDefaults(defineProps<CheckboxGroupProps>(), { orientation: 'vertical', density: 'default', bordered: false, }) const modelValue = defineModel<string[]>() const forwarded = useForwardProps( reactiveOmit( props, 'class', 'label', 'hint', 'error', 'errorMessages', 'density', 'bordered', 'options', 'name', 'inline', ), ) const actualOrientation = computed(() => (props.inline ? 'horizontal' : props.orientation)) provide('checkboxGroupContext', { name: computed(() => props.name), }) </script> <template> <CheckboxGroupRoot v-bind="forwarded" :model-value="modelValue" @update:model-value="modelValue = $event" :name="name" :orientation="actualOrientation" class="flex flex-col gap-2" :class="[ actualOrientation === 'horizontal' ? 'flex-row items-center' : 'flex-col', bordered && 'rounded-lg border p-4', props.class, ]" > <label v-if="label" class="text-sm font-medium"> {{ label }} </label> <p v-if="hint && !error" class="text-muted-foreground text-xs"> {{ hint }} </p> <div class="flex gap-4" :class="[actualOrientation === 'horizontal' ? 'flex-row flex-wrap items-center' : 'flex-col']" > <template v-if="options && options.length > 0"> <Checkbox v-for="(option, i) in options" :key="i" :value="typeof option === 'string' ? option : option.value" :label="typeof option === 'string' ? option : option.label" :disabled="typeof option === 'string' ? undefined : option.disabled" :name="name" :density="density" /> </template> <slot v-else :model-value="modelValue" /> </div> <div v-if="error || errorMessages" class="flex flex-col gap-0.5"> <p v-if="typeof errorMessages === 'string'" class="text-destructive text-xs"> {{ errorMessages }} </p> <template v-else> <p v-for="(msg, i) in errorMessages" :key="i" class="text-destructive text-xs"> {{ msg }} </p> </template> </div> </CheckboxGroupRoot> </template> -
app/components/ui/checkbox/index.ts 0.1 kB
export { default as Checkbox } from './Checkbox.vue' export { default as CheckboxGroup } from './CheckboxGroup.vue'
Raw manifest: https://uipkge.dev/r/vue/checkbox.json