shadimage

Checkbox

Checkbox and CheckboxGroup components with field integration

registry/preskok/ui/preskok-ui/checkbox.tsx

"use client"

import { twMerge } from "cn"
import { CheckIcon, MinusIcon } from "lucide-react"
import type {
  CheckboxGroupProps,
  CheckboxProps,
} from "react-aria-components/CheckboxGroup"
import {
  CheckboxGroup as CheckboxGroupPrimitive,
  Checkbox as CheckboxPrimitive,
} from "react-aria-components/CheckboxGroup"
import { composeRenderProps } from "react-aria-components/composeRenderProps"

import { cx } from "@/lib/primitive"

import { Label } from "./field"

export function CheckboxGroup({ className, ...props }: CheckboxGroupProps) {
  return (
    <CheckboxGroupPrimitive
      {...props}
      data-slot="control"
      className={cx(
        "space-y-3 has-[[slot=description]]:space-y-6 has-[[slot=description]]:**:data-[slot=label]:font-medium **:[[slot=description]]:block",
        className
      )}
    />
  )
}

export function Checkbox({ className, children, ...props }: CheckboxProps) {
  return (
    <CheckboxPrimitive
      data-slot="control"
      className={cx("group block disabled:opacity-50", className)}
      {...props}
    >
      {composeRenderProps(
        children,
        (
          children,
          { isSelected, isIndeterminate, isFocusVisible, isInvalid }
        ) => {
          const isStringChild = typeof children === "string"
          const indicator = isIndeterminate ? (
            <MinusIcon data-slot="check-indicator" />
          ) : isSelected ? (
            <CheckIcon data-slot="check-indicator" />
          ) : null

          const content = isStringChild ? (
            <CheckboxLabel>{children}</CheckboxLabel>
          ) : (
            children
          )

          return (
            <div
              className={twMerge(
                "flex items-center",
                "has-[*:nth-child(2)]:grid has-[*:nth-child(2)]:grid-cols-[1.125rem_1fr] has-[*:nth-child(2)]:gap-x-3 has-[*:nth-child(2)]:gap-y-1 has-[*:nth-child(2)]:sm:grid-cols-[1rem_1fr]",
                "*:data-[slot=indicator]:col-start-1 *:data-[slot=indicator]:row-start-1 *:data-[slot=indicator]:my-auto",
                "*:data-[slot=label]:col-start-2 *:data-[slot=label]:row-start-1",
                "*:[[slot=description]]:col-start-2 *:[[slot=description]]:row-start-2",
                "has-[[slot=description]]:**:data-[slot=label]:font-medium"
              )}
            >
              <span
                data-slot="indicator"
                className={twMerge([
                  "inset-ring-input text-background group-hover:inset-ring-muted-foreground/30 relative isolate flex shrink-0 items-center justify-center rounded inset-ring transition",
                  "sm:size-4 sm:*:data-[slot=check-indicator]:size-3.5",
                  "size-4.5 *:data-[slot=check-indicator]:size-4",
                  (isSelected || isIndeterminate) && [
                    "inset-ring-primary bg-primary text-primary-foreground",
                    "group-invalid:inset-ring-destructive/70 group-invalid:bg-destructive group-invalid:text-destructive-foreground dark:group-invalid:inset-ring-destructive/70",
                  ],
                  isFocusVisible && [
                    "inset-ring-primary ring-ring/20 ring-3",
                    "group-invalid:inset-ring-destructive/70 group-invalid:text-destructive-foreground group-invalid:ring-destructive/20",
                  ],
                  isInvalid &&
                    "inset-ring-destructive/70 text-destructive-foreground ring-destructive/20 group-hover:inset-ring-destructive/70",
                ])}
              >
                {indicator}
              </span>
              {content}
            </div>
          )
        }
      )}
    </CheckboxPrimitive>
  )
}

export function CheckboxLabel(props: React.ComponentProps<typeof Label>) {
  return <Label elementType="span" {...props} />
}

No selection tags yet — empty means no evidence, not lack of support.

DependenciesBundle weight · light

icons
lucide-react