component — conform

Conform Checkbox

The quebi Checkbox wired to Conform. Binds a boolean field's name, required, default, and validity from field metadata and renders inline errors.

  • form
  • conform
  • checkbox
  • boolean
  • validation

01 example

Bound to a Conform form

Submit without checking to see the validation error wired from field metadata.

source

conform-checkbox.tsx

Copy this into your project. Resolve its dependencies from the registryDependencies in the component's API entry.

"use client"

import type { FieldMetadata } from "@conform-to/react"
import type { CheckboxProps } from "react-aria-components"
import { cn } from "@/lib/utils"
import { Checkbox } from "@/components/checkbox"
import { describedBy, Description, Field, FieldError } from "@/components/field"

export interface ConformCheckboxProps
  extends Omit<
    CheckboxProps,
    "name" | "form" | "value" | "defaultSelected" | "isSelected" | "isRequired" | "isInvalid"
  > {
  /**
   * The wrapper's class. A plain string, unlike the inherited
   * `CheckboxProps["className"]`: this lands on the `Field` div around the
   * control, which has no render state to compose a function against. Keeping
   * the wider type would let a caller pass a render prop that clsx drops in
   * silence.
   */
  className?: string
  /** A checkbox bound to a boolean form value. */
  field: FieldMetadata<boolean>
  label?: string
  description?: string
}

/**
 * ConformCheckbox — Checkbox wired to Conform.
 *
 * Binds a boolean Conform field to the quebi Checkbox: derives name, form,
 * required, default, and validity from the field metadata and renders inline
 * errors.
 *
 * `getInputProps(field, { type: "checkbox" })` must not be spread onto this
 * control. Conform returns the DOM names, `defaultChecked` and `required`, and
 * react-aria's Checkbox takes `defaultSelected` and `isRequired`: the spread
 * type-checks (JSX skips excess-property checking) and then `filterDOMProps`
 * drops both, so the box renders unchecked after a failed submit with nothing
 * in the DOM to show for it.
 */
export function ConformCheckbox({
  field,
  label,
  description,
  className,
  ...props
}: ConformCheckboxProps) {
  const hasErrors = !field.valid && !!field.errors
  const isRequired = field.required ?? false

  return (
    <Field className={cn(className)}>
      <Checkbox
        {...props}
        name={field.name}
        form={field.formId}
        value="on"
        defaultSelected={field.defaultChecked}
        isRequired={isRequired}
        isInvalid={hasErrors}
        aria-describedby={describedBy(
          hasErrors && field.errorId,
          description && field.descriptionId,
        )}
      >
        {label}
        {isRequired && <span className="ml-1 text-quebi-fg">*</span>}
      </Checkbox>
      {/* These ids are ours to set: a bare Checkbox is not a react-aria field
          (unlike CheckboxGroup, which supplies a FieldErrorContext), so nothing
          generates them and the aria-describedby above is their only reference. */}
      {description && <Description id={field.descriptionId}>{description}</Description>}
      {hasErrors && <FieldError id={field.errorId}>{field.errors?.join(", ")}</FieldError>}
    </Field>
  )
}