component — conform

Conform Field

A text field bound to a Conform field — derives name, default, required, and validation errors from the field metadata.

  • form
  • conform
  • input
  • text-field
  • validation

01 example

Bound to a Conform form

Submit with an invalid or empty email to see the error wired from field metadata.

We'll never share your email.

source

conform-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 { composeRenderProps, TextField, type TextFieldProps } from "react-aria-components"
import { cn } from "@/lib/utils"
import { Description, FieldError, fieldStyles, Label } from "@/components/field"
import { Input } from "@/components/input"

type InputProps = Omit<TextFieldProps, "type"> & { type?: "email" | "password" | "text" }

export interface ConformFieldProps
  extends Omit<InputProps, "name" | "form" | "value" | "defaultValue" | "isRequired" | "isInvalid"> {
  /** A text field bound to a string form value. */
  field: FieldMetadata<string>
  label?: string
  placeholder?: string
  description?: string
}

/**
 * ConformField — text field wired to Conform.
 *
 * Binds a string Conform field to the quebi TextField/Input stack: derives
 * name, id, form, required, default, and validity from the field metadata and
 * renders an inline label, optional description, and field errors.
 *
 * The metadata is read attribute by attribute rather than spread from
 * `getInputProps`. That helper returns the DOM names — `required`,
 * `defaultChecked`, `min`/`max` — and react-aria's controls take `isRequired`,
 * `defaultSelected`, `minValue`/`maxValue`; a JSX spread skips excess-property
 * checking, so the mismatched ones are dropped by `filterDOMProps` with no type
 * error and no attribute left in the DOM to notice. Named props are checked.
 */
export function ConformField({
  field,
  label,
  type = "text",
  placeholder,
  description,
  className,
  ...restProps
}: ConformFieldProps) {
  const hasErrors = !field.valid && !!field.errors
  const isRequired = field.required ?? false

  return (
    <TextField
      {...restProps}
      id={field.id}
      name={field.name}
      form={field.formId}
      type={type}
      defaultValue={(field.initialValue as string) ?? ""}
      minLength={field.minLength}
      maxLength={field.maxLength}
      pattern={field.pattern}
      isRequired={isRequired}
      isInvalid={hasErrors}
      className={composeRenderProps(className, (resolved) => cn(fieldStyles, resolved))}
    >
      {label && (
        <Label className={cn(hasErrors && "text-quebi-danger")}>
          {label}
          {isRequired && <span className="ml-1 text-quebi-fg">*</span>}
        </Label>
      )}
      <Input placeholder={placeholder} />
      {/* No ids and no aria-describedby here: this is a react-aria field, so it
          generates the description and error ids and already points the control
          at them. Setting id={field.errorId} would not break that — on mount
          react-aria re-points the control at whatever id the element actually
          carries — it would just duplicate wiring that is already correct.
          Outside a react-aria field the ids are yours: see ConformSwitch. */}
      {description && <Description>{description}</Description>}
      {hasErrors && <FieldError>{field.errors?.join(", ")}</FieldError>}
    </TextField>
  )
}