Glrk UI

Checkbox

A wrapper for Base UI Checkbox and CheckboxGroup with label/description support, select-all parent checkbox, orientation control, and controlled state.

Checkbox
Single checkbox — label + description + disabled
indeterminate — controlled tri-state
CheckboxWrapper
options — list with descriptions and disabled
parentLabel — select-all with indeterminate state
value + onValueChange — controlled group

Selected: email, sms

orientation: horizontal
CheckboxGroup primitive
Manual composition

Installation

npx shadcn@latest add @glrk-ui/checkbox

If you haven't set up the prerequisites yet, check out Prerequest section.

Copy and paste the following code into your project.

ui/checkbox.tsx
'use client'

import * as React from 'react'
import { CheckboxGroup as CheckboxGroupPrimitive } from '@base-ui/react/checkbox-group'
import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'
import { CheckIcon, MinusIcon } from 'lucide-react'

import { cn, getKey, getLabel, getValue } from '@/lib/utils'

function CheckboxIndicator({ className, ...props }: CheckboxPrimitive.Root.Props) {
  return (
    <CheckboxPrimitive.Root
      data-slot="checkbox"
      className={cn(
        'peer group/checkbox relative flex size-4.5 shrink-0 items-center justify-center rounded border border-input transition-colors outline-none group-has-disabled/field:opacity-50 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-indeterminate:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground dark:data-indeterminate:bg-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 /> : <CheckIcon />}
          </span>
        )}
      />
    </CheckboxPrimitive.Root>
  )
}

type CheckboxProps = {
  label: React.ReactNode
  description?: React.ReactNode
  wrapperCls?: string
  as?: React.ElementType
} & CheckboxPrimitive.Root.Props

function Checkbox({ label, description, wrapperCls, className, as: Comp = 'label', ...props }: CheckboxProps) {
  return (
    <Comp className={cn('flex cursor-pointer select-none items-start gap-2 has-[:disabled]:cursor-not-allowed', wrapperCls)}>
      <CheckboxIndicator className={cn(className)} {...props} />
      <p className="grid">
        <span className="text-sm font-medium leading-none">{label}</span>
        {description && <span className="mt-0.5 text-xs text-muted-foreground">{description}</span>}
      </p>
    </Comp>
  )
}

function CheckboxGroup({ className, ...props }: CheckboxGroupPrimitive.Props) {
  return (
    <CheckboxGroupPrimitive
      data-slot="checkbox-group"
      className={cn('flex flex-col gap-2', className)}
      {...props}
    />
  )
}

type checkboxOptionT = allowedPrimitiveT | (optionT & { description?: React.ReactNode })

type CheckboxWrapperProps = {
  options: checkboxOptionT[]
  parentLabel?: React.ReactNode
  orientation?: 'horizontal' | 'vertical'
  itemCls?: string
  as?: React.ElementType
} & Omit<CheckboxGroupPrimitive.Props, 'children'>

function CheckboxWrapper({
  options,
  parentLabel,
  orientation = 'vertical',
  itemCls,
  className,
  as,
  ...props
}: CheckboxWrapperProps) {
  const allValues = options.map(opt => String(getValue(opt)))

  return (
    <CheckboxGroup
      allValues={parentLabel ? allValues : undefined}
      className={cn(orientation === 'horizontal' && 'flex-row flex-wrap', className)}
      {...props}
    >
      {parentLabel && (
        <Checkbox label={parentLabel} wrapperCls={itemCls} data-parent />
      )}
      {options.map((opt, i) => (
        <Checkbox
          key={getKey(opt, i)}
          value={String(getValue(opt))}
          label={getLabel(opt)}
          description={typeof opt === 'object' ? opt.description : undefined}
          disabled={typeof opt === 'object' ? opt.disabled : undefined}
          wrapperCls={itemCls}
          as={as}
        />
      ))}
    </CheckboxGroup>
  )
}

export {
  CheckboxIndicator,
  Checkbox,
  CheckboxGroup,
  CheckboxWrapper,
  type CheckboxProps,
  type checkboxOptionT,
  type CheckboxWrapperProps,
}

Usage

Single checkbox — Checkbox

import { Checkbox } from "@/components/ui/checkbox"

<Checkbox label="Accept terms and conditions" />
<Checkbox
  label="Email notifications"
  description="Receive updates about your account."
/>
<Checkbox label="Disabled" disabled />

Group — CheckboxWrapper

import { CheckboxWrapper } from "@/components/ui/checkbox"

<CheckboxWrapper
  defaultValue={["email"]}
  options={[
    { value: "email", label: "Email", description: "Notify via email." },
    { value: "sms", label: "SMS" },
    { value: "push", label: "Push notifications" },
    { value: "locked", label: "Cannot change", disabled: true },
  ]}
/>

Select-all parent checkbox

Set parentLabel to add a parent checkbox that shows indeterminate when partially selected:

<CheckboxWrapper
  defaultValue={["email"]}
  parentLabel="All notifications"
  options={[
    { value: "email", label: "Email" },
    { value: "sms", label: "SMS" },
    { value: "push", label: "Push" },
  ]}
/>

Controlled

const [value, setValue] = useState<string[]>(["email"])

<CheckboxWrapper
  value={value}
  onValueChange={(v) => setValue(v)}
  parentLabel="Select all"
  options={[
    { value: "email", label: "Email" },
    { value: "sms", label: "SMS" },
  ]}
/>

Horizontal orientation

<CheckboxWrapper
  orientation="horizontal"
  defaultValue={["react"]}
  options={[
    { value: "react", label: "React" },
    { value: "vue", label: "Vue" },
    { value: "svelte", label: "Svelte" },
  ]}
/>

Reference

checkboxOptionT

Prop

Type

Checkbox

Prop

Type

CheckboxWrapper

Prop

Type