component — conform

Conform Tag Field

The quebi TagField wired to Conform. Binds a tag list's name, form, default tags, and validity from field metadata and renders inline errors.

  • form
  • conform
  • tags
  • chips
  • validation

01 example

Bound to a Conform form

Remove a tag and submit to see the validation error wired from field metadata.

Enter, comma, or semicolon commits a tag.
react
forms

source

conform-tag-field.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 { cn } from "@/lib/utils"
import { describedBy, Field, FieldError } from "@/components/field"
import { TagField, type TagFieldProps } from "@/components/tag-field"

export interface ConformTagFieldProps
  extends Omit<
    TagFieldProps,
    "name" | "form" | "id" | "value" | "defaultValue" | "isRequired" | "isInvalid"
  > {
  /**
   * A tag list bound to a form value. TagField submits the comma-joined tags
   * through one hidden input, so the wire value is a string; a schema that
   * splits it surfaces as `string[]`.
   */
  field: FieldMetadata<string | string[]>
}

/** Read the field's initial value as a tag list, accepting either wire shape. */
function toDefaultTags(initialValue: unknown): string[] {
  if (Array.isArray(initialValue)) return initialValue.map(String)
  if (typeof initialValue === "string" && initialValue !== "") return initialValue.split(",")
  return []
}

/**
 * ConformTagField — TagField wired to Conform.
 *
 * Binds a Conform field to the quebi TagField: derives name, id, form, default
 * tags, and validity from the field metadata and renders inline errors.
 *
 * `isRequired` is deliberately not forwarded. TagField's own required handling
 * installs a `submit` listener that calls `reportValidity()`, which fights the
 * `noValidate` Conform puts on the form — the schema owns "at least one tag"
 * here, and its message is the one rendered below.
 */
export function ConformTagField({ field, className, ...props }: ConformTagFieldProps) {
  const hasErrors = !field.valid && !!field.errors

  return (
    <Field className={cn(className)}>
      <TagField
        {...props}
        id={field.id}
        name={field.name}
        form={field.formId}
        defaultValue={toDefaultTags(field.initialValue)}
        isInvalid={hasErrors}
        // Only the error id: TagField renders its own description and owns that
        // element's id, so pointing at field.descriptionId would point at nothing.
        aria-describedby={describedBy(hasErrors && field.errorId)}
      />
      {hasErrors && <FieldError id={field.errorId}>{field.errors?.join(", ")}</FieldError>}
    </Field>
  )
}