component — conform

Conform Multiple Select

Multiple Select wired to Conform: derives name, id, default selection, and validity from field metadata, mirrors the selection into hidden inputs for submission, and renders inline errors. Pass the option list as children.

  • conform
  • select
  • multi-select
  • form
  • validation
  • tags

01 example

Bound to a Conform form

Type to filter, pick several frameworks, and remove a chip with its ✕ or with Backspace. Submit without selecting anything to see the validation error wired from field metadata.

source

conform-multiple-select.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 { PropsWithChildren } from "react"
import { describedBy, Description, Field, FieldError, Label } from "@/components/field"
import { MultipleSelect } from "@/components/multiple-select"
import { cn } from "@/lib/utils"

export interface ConformMultipleSelectProps {
  /** A multi-value select bound to a string-array form value. */
  field: FieldMetadata<string[]>
  label?: string
  description?: string
  placeholder?: string
  isDisabled?: boolean
  className?: string
  /**
   * Only for a field with no visible `label` — an `aria-label` wins over the
   * `<Label htmlFor>` below and would hide it from assistive technology.
   */
  "aria-label"?: string
}

/**
 * ConformMultipleSelect — Multiple Select wired to Conform.
 *
 * Binds a multi-value Conform field to the quebi Multiple Select: derives name,
 * id, form, required, default selection, and validity from the field metadata,
 * mirrors the selection into hidden inputs for submission, and renders inline
 * errors. Pass the option list (MultipleSelectContent + MultipleSelectItem) as
 * children.
 *
 * This is the same wiring as ConformAsyncMultipleSelect, for the same reason:
 * since task #157 Multiple Select is a hand-built combobox rather than a
 * react-aria Select, so nothing generates the description/error ids and nothing
 * points the control at them. They are set here, and `id` lands on the combobox
 * input so the label points at the control a user actually focuses.
 */
export function ConformMultipleSelect({
  field,
  label,
  description,
  placeholder,
  isDisabled,
  className,
  children,
  "aria-label": ariaLabel,
}: PropsWithChildren<ConformMultipleSelectProps>) {
  const hasErrors = !field.valid && !!field.errors
  const isRequired = field.required ?? false

  return (
    <Field className={cn(className)}>
      {label && (
        <Label htmlFor={field.id} className={cn(hasErrors && "text-quebi-danger")}>
          {label}
          {isRequired && <span className="ml-1 text-quebi-fg">*</span>}
        </Label>
      )}
      <MultipleSelect
        id={field.id}
        name={field.name}
        form={field.formId}
        aria-label={ariaLabel}
        placeholder={placeholder}
        defaultValue={(field.initialValue as string[]) ?? []}
        isRequired={isRequired}
        isInvalid={hasErrors}
        isDisabled={isDisabled}
        aria-describedby={describedBy(
          hasErrors && field.errorId,
          description && field.descriptionId,
        )}
      >
        {children}
      </MultipleSelect>
      {description && <Description id={field.descriptionId}>{description}</Description>}
      {hasErrors && <FieldError id={field.errorId}>{field.errors?.join(", ")}</FieldError>}
    </Field>
  )
}