Forms

Conform Field

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

formconforminputtext-fieldvalidation

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

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

"use client"

import { type FieldMetadata, getInputProps } from "@conform-to/react"
import { TextField, type TextFieldProps } from "react-aria-components"
import { cn } from "@/lib/utils"
import { Description, FieldError, Label } from "@/components/field"
import { Input } from "@/components/input"

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

export interface ConformFieldProps extends InputProps {
  // A text field from any form schema. The value type param is loose because
  // text fields can surface differently per zod schema; only
  // name/id/form/default/required/errors are used here.
  // biome-ignore lint/suspicious/noExplicitAny: form-schema type params vary per call site
  field: FieldMetadata<any, any, 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, required, default, and validity from the field metadata and renders
 * an inline label, optional description, and field errors.
 */
export function ConformField({
  field,
  label,
  type = "text",
  placeholder,
  description,
  ...restProps
}: ConformFieldProps) {
  const hasErrors = !field.valid && !!field.errors
  const isRequired = field.required ?? false

  // Strip defaultValue and required from conform props.
  // Pass defaultValue explicitly to TextField (React Aria consumes it for initial state).
  // The remaining props (name, id, form, type, aria-*) go to TextField which distributes
  // them to Input via InputContext.
  const { required: _required, defaultValue, ...conformProps } = getInputProps(field, { type })

  return (
    <TextField
      {...restProps}
      {...conformProps}
      defaultValue={(defaultValue as string) ?? ""}
      isRequired={isRequired}
      isInvalid={hasErrors}
      className="flex flex-col gap-1.5"
    >
      {label && (
        <Label htmlFor={field.id} className={cn(hasErrors && "text-red-500")}>
          {label}
          {isRequired && <span className="ml-1 text-red-500">*</span>}
        </Label>
      )}
      <Input placeholder={placeholder} />
      {description && <Description>{description}</Description>}
      <FieldError>{field.errors?.join(", ")}</FieldError>
    </TextField>
  )
}