buzzform
uicheckboxsource: ok · hit
CheckboxField
Checkbox, checkbox-group, and tristate checkbox field for BuzzForm v2.
npx shadcn add "https://form.buildnbuzz.com/r/checkbox.json"free
Where it goes
stats
Interaction
animated-counter
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · lightui-primitives@base-ui/react
Registry dependencies
@buzzform/initcheckboxfield
Files4 · source cached 10/9/2026, 12:21:46 AM
registry/shadcn/fields/checkbox.tsx
"use client";
import type { CheckboxField as CheckboxFieldDef } from "@buildnbuzz/form-react";
import { useDataField } from "@buildnbuzz/form-react";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
interface CheckboxUi {
autoFocus?: boolean;
className?: string;
width?: string | number;
asterisk?: boolean;
}
export function CheckboxField() {
const {
fieldApi,
field,
isDisabled,
isReadOnly,
isRequired,
label,
description,
errors,
isInvalid,
descriptionId,
errorId,
ariaDescribedBy,
handleChange,
handleBlur,
} = useDataField<CheckboxFieldDef>();
// Delegate to group renderer if hasMany
if ("hasMany" in field && field.hasMany) {
return <CheckboxGroupField />;
}
// Delegate to tristate renderer if tristate
if ("tristate" in field && field.tristate) {
return <TristateCheckboxField />;
}
const value = (fieldApi.state.value as boolean) ?? false;
const ui = field.ui as CheckboxUi | undefined;
const width = ui?.width;
const widthStyle = width
? {
width: typeof width === "number" ? `${width}px` : width,
flex: "0 1 auto",
}
: undefined;
return (
<FieldGroup
data-field={fieldApi.name}
className={ui?.className}
style={widthStyle}
>
<Field
orientation="horizontal"
data-invalid={isInvalid}
data-disabled={isDisabled}
>
<Checkbox
id={fieldApi.name}
checked={value}
onCheckedChange={(checked) => {
if (isReadOnly) return;
handleChange(!!checked);
handleBlur();
}}
disabled={isDisabled}
aria-invalid={isInvalid}
aria-describedby={ariaDescribedBy}
aria-readonly={isReadOnly}
autoFocus={ui?.autoFocus}
/>
<FieldContent>
{label && (
<FieldLabel
htmlFor={fieldApi.name}
className="cursor-pointer m-0 font-normal inline"
>
{label}
{isRequired && ui?.asterisk !== false && (
<span className="text-destructive"> *</span>
)}
</FieldLabel>
)}
{description && !isInvalid && (
<FieldDescription id={descriptionId}>
{description}
</FieldDescription>
)}
{isInvalid && <FieldError id={errorId} errors={errors} />}
</FieldContent>
</Field>
</FieldGroup>
);
}
import { CheckboxGroupField } from "./checkbox-group";
import { TristateCheckboxField } from "./tristate-checkbox";
registry/shadcn/fields/checkbox-group.tsx
"use client";
import type { CheckboxGroupField as CheckboxGroupFieldDef } from "@buildnbuzz/form-react";
import { useDataField, useFieldOptions } from "@buildnbuzz/form-react";
import { Checkbox } from "@/components/ui/checkbox";
import {
Field,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
import { cn } from "@/lib/utils";
type OptionGroupVariant = "default" | "card";
type OptionGroupDirection = "vertical" | "horizontal";
type OptionGroupColumns = 1 | 2 | 3 | 4 | string | number | undefined;
interface OptionUi {
description?: string;
}
interface CheckboxGroupUi {
variant?: OptionGroupVariant;
direction?: OptionGroupDirection;
columns?: OptionGroupColumns;
card?: {
size?: "sm" | "md" | "lg";
bordered?: boolean;
};
autoFocus?: boolean;
className?: string;
width?: string | number;
asterisk?: boolean;
}
const cardSizeClasses = {
sm: "p-2.5 sm:p-3",
md: "p-3 sm:p-4",
lg: "p-4 sm:p-5",
} as const;
function getGridColumnsClass(columns: OptionGroupColumns | undefined) {
if (columns === 2) return "sm:grid-cols-2";
if (columns === 3) return "sm:grid-cols-2 md:grid-cols-3";
if (columns === 4) return "sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4";
return undefined;
}
function getLayoutClass({
variant = "default",
direction = "vertical",
columns,
}: {
variant?: OptionGroupVariant;
direction?: OptionGroupDirection;
columns?: OptionGroupColumns;
}) {
const isCard = variant === "card";
const isHorizontal = direction === "horizontal";
if (isHorizontal && !columns)
return "!flex flex-row flex-wrap gap-x-4 gap-y-2";
const effectiveColumns =
isCard || isHorizontal ? (columns ?? (isCard ? 2 : undefined)) : undefined;
if (!effectiveColumns || effectiveColumns === 1) return "flex flex-col gap-2";
return cn("grid gap-2", getGridColumnsClass(effectiveColumns));
}
export function CheckboxGroupField() {
const {
fieldApi,
field,
isDisabled,
isReadOnly,
isRequired,
label,
description,
errors,
isInvalid,
descriptionId,
errorId,
ariaDescribedBy,
handleBlur,
} = useDataField<CheckboxGroupFieldDef>();
const { options, isLoading } = useFieldOptions(field.options);
const ui = field.ui as CheckboxGroupUi | undefined;
const selected = Array.isArray(fieldApi.state.value)
? (fieldApi.state.value as string[])
: [];
const variant = ui?.variant ?? "default";
const direction = ui?.direction ?? "vertical";
const columns = ui?.columns;
const cardSize = ui?.card?.size ?? "md";
const cardBordered = ui?.card?.bordered ?? true;
const isCardVariant = variant === "card";
const isHorizontal = direction === "horizontal";
const layoutClass = getLayoutClass({ variant, direction, columns });
const width = ui?.width;
const widthStyle = width
? {
width: typeof width === "number" ? `${width}px` : width,
flex: "0 1 auto",
}
: undefined;
const handleToggle = (val: string, checked: boolean) => {
if (isReadOnly) return;
const maxSelected = field.maxSelected;
const hasMax = typeof maxSelected === "number";
if (checked && hasMax && selected.length >= maxSelected) return;
const next = checked
? [...selected, val]
: selected.filter((v) => v !== val);
fieldApi.handleChange(next);
handleBlur();
};
return (
<FieldGroup
data-field={fieldApi.name}
className={ui?.className}
style={widthStyle}
>
<div
className="flex flex-col gap-2"
data-invalid={isInvalid}
data-disabled={isDisabled}
>
{label && (
<FieldLabel className="gap-1 items-baseline">
{isRequired && ui?.asterisk !== false && (
<span className="text-destructive">*</span>
)}
{label}
</FieldLabel>
)}
{description && !isInvalid && (
<FieldDescription id={descriptionId} className="-mt-1 mb-1">
{description}
</FieldDescription>
)}
<div
className={layoutClass}
role="group"
aria-describedby={ariaDescribedBy}
>
{isLoading ? (
<div className="py-2 text-sm text-muted-foreground animate-pulse">
Loading options...
</div>
) : (
options.map((opt, i) => {
const {
value: val,
label: optLabel,
disabled: optDisabled,
ui: optUi,
} = opt;
const optDesc = (optUi as OptionUi | undefined)?.description;
const checked = selected.includes(val);
const hasMax = typeof field.maxSelected === "number";
const atMax = hasMax && selected.length >= field.maxSelected!;
const isOptDisabled =
optDisabled || isDisabled || (atMax && !checked);
const id = `${fieldApi.name}-${i}`;
if (isCardVariant) {
const showDescription = optDesc && cardSize !== "sm";
return (
<label
key={`${val}-${i}`}
htmlFor={id}
className={cn(
"cursor-pointer transition-all duration-150 rounded-lg flex items-start gap-3",
cardSizeClasses[cardSize],
cardBordered && "border",
cardBordered &&
checked &&
"border-primary ring-1 ring-primary/20 bg-primary/5 dark:bg-primary/10",
cardBordered &&
!checked &&
"border-border hover:border-foreground/20 hover:bg-accent/30",
!cardBordered && "hover:bg-accent/50",
!cardBordered && checked && "bg-accent",
isOptDisabled &&
"opacity-50 cursor-not-allowed pointer-events-none",
)}
data-checked={checked}
data-disabled={isOptDisabled}
>
<Checkbox
id={id}
checked={checked}
onCheckedChange={(next) => handleToggle(val, next === true)}
disabled={isOptDisabled}
className="shrink-0 mt-0.5"
autoFocus={ui?.autoFocus && i === 0}
/>
<div className="flex flex-col gap-0.5 min-w-0 flex-1">
<div className="flex items-center gap-2">
<span
className={cn(
"font-medium",
cardSize === "lg" ? "text-base" : "text-sm",
)}
>
{optLabel}
</span>
</div>
{showDescription && (
<span
className={cn(
"text-muted-foreground line-clamp-2",
cardSize === "lg" ? "text-sm" : "text-xs",
)}
>
{optDesc}
</span>
)}
</div>
</label>
);
}
return (
<Field
key={`${val}-${i}`}
orientation="horizontal"
className={cn(
"items-center gap-2.5 space-y-0",
isHorizontal && !columns && "w-auto",
isOptDisabled && "opacity-50 cursor-not-allowed",
)}
>
<Checkbox
id={id}
checked={checked}
onCheckedChange={(next) => handleToggle(val, next === true)}
disabled={isOptDisabled}
autoFocus={ui?.autoFocus && i === 0}
/>
<FieldLabel
htmlFor={id}
className={cn(
"font-normal cursor-pointer m-0 flex items-center gap-2 text-sm",
isOptDisabled && "cursor-not-allowed",
)}
>
{optLabel}
</FieldLabel>
</Field>
);
})
)}
{options.length === 0 && !isLoading && (
<div className="py-2 text-sm text-muted-foreground italic">
No options available
</div>
)}
</div>
{isInvalid && <FieldError id={errorId} errors={errors} />}
</div>
</FieldGroup>
);
}
registry/shadcn/fields/tristate-checkbox.tsx
"use client";
import type { TristateCheckboxField as TristateCheckboxFieldDef } from "@buildnbuzz/form-react";
import { useDataField } from "@buildnbuzz/form-react";
import { Checkbox } from "../components/checkbox";
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
} from "@/components/ui/field";
interface TristateCheckboxUi {
autoFocus?: boolean;
className?: string;
width?: string | number;
asterisk?: boolean;
}
/** Cycles null → true → false → null on each click. */
function nextTristateValue(current: boolean | null): boolean | null {
if (current === null) return true;
if (current === true) return false;
return null;
}
/**
* A tristate checkbox field component.
*
* Uses a registry Checkbox wrapper to support indeterminate state
* with icon placeholders.
*/
export function TristateCheckboxField() {
const {
fieldApi,
field,
isDisabled,
isReadOnly,
isRequired,
label,
description,
errors,
isInvalid,
descriptionId,
errorId,
ariaDescribedBy,
handleChange,
handleBlur,
} = useDataField<TristateCheckboxFieldDef>();
const value = (fieldApi.state.value as boolean | null) ?? null;
const ui = field.ui as TristateCheckboxUi | undefined;
const width = ui?.width;
const widthStyle = width
? {
width: typeof width === "number" ? `${width}px` : width,
flex: "0 1 auto",
}
: undefined;
// Base UI uses `checked` (boolean) + `indeterminate` (boolean) separately
const checked = value === true;
const indeterminate = value === null;
return (
<FieldGroup
data-field={fieldApi.name}
className={ui?.className}
style={widthStyle}
>
<Field
orientation="horizontal"
data-invalid={isInvalid}
data-disabled={isDisabled}
>
<Checkbox
id={fieldApi.name}
checked={checked}
indeterminate={indeterminate}
onCheckedChange={() => {
if (isReadOnly) return;
handleChange(nextTristateValue(value));
handleBlur();
}}
disabled={isDisabled}
aria-invalid={isInvalid}
aria-describedby={ariaDescribedBy}
aria-readonly={isReadOnly}
autoFocus={ui?.autoFocus}
data-slot="checkbox"
/>
<FieldContent>
{label && (
<FieldLabel
htmlFor={fieldApi.name}
className="cursor-pointer m-0 font-normal inline"
>
{label}
{isRequired && ui?.asterisk !== false && (
<span className="text-destructive"> *</span>
)}
</FieldLabel>
)}
{description && !isInvalid && (
<FieldDescription id={descriptionId}>
{description}
</FieldDescription>
)}
{isInvalid && <FieldError id={errorId} errors={errors} />}
</FieldContent>
</Field>
</FieldGroup>
);
}
registry/shadcn/components/checkbox.tsx
"use client";
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox";
import { IconPlaceholder } from "@/components/icon-placeholder";
import { cn } from "@/lib/utils";
export type CheckboxProps = Omit<CheckboxPrimitive.Root.Props, "checked"> & {
checked?: boolean | "indeterminate";
indeterminate?: boolean;
};
export function Checkbox({
className,
checked,
indeterminate,
...props
}: CheckboxProps) {
const isIndeterminate = indeterminate ?? checked === "indeterminate";
const normalizedChecked = checked === "indeterminate" ? false : checked;
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
checked={normalizedChecked}
indeterminate={isIndeterminate}
data-indeterminate={isIndeterminate ? "" : undefined}
className={cn(
"border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary data-indeterminate:bg-primary data-indeterminate:text-primary-foreground dark:data-indeterminate:bg-primary data-indeterminate:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-[4px] border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-[3px] aria-invalid:ring-[3px] peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50",
className,
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="[&>svg]:size-3.5 grid place-content-center text-current transition-none"
>
{isIndeterminate ? (
<IconPlaceholder
hugeicons="MinusSignIcon"
lucide="Minus"
tabler="IconMinus"
phosphor="Minus"
remixicon="RiSubtractLine"
/>
) : (
<IconPlaceholder
hugeicons="Tick02Icon"
lucide="Check"
tabler="IconCheck"
phosphor="Check"
remixicon="RiCheckLine"
/>
)}
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
);
}