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.
source
conform-field.tsxCopy 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>
)
}