component — conform

Conform Color Field

The quebi ColorField wired to Conform. Binds a hex-string field's name, required, default, and validity from field metadata and renders inline errors.

  • form
  • conform
  • color
  • hex
  • validation

01 example

Bound to a Conform form

Type #FFFFFF and submit to see the validation error wired from field metadata.

A hex value; the visible input is the form control.

source

conform-color-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 } from "react-aria-components"
import { cn } from "@/lib/utils"
import {
  ColorField,
  ColorFieldGroup,
  type ColorFieldProps,
  ColorInput,
} from "@/components/color-field"
import { Description, FieldError, fieldStyles, Label } from "@/components/field"

export interface ConformColorFieldProps
  extends Omit<
    ColorFieldProps,
    "name" | "form" | "value" | "defaultValue" | "isRequired" | "isInvalid" | "children"
  > {
  /**
   * A hex colour bound to a string form value. Without a `channel` the field is
   * a HexColorField, so the wire value is `"#RRGGBB"` — a plain string, not a
   * serialised Color.
   */
  field: FieldMetadata<string>
  label?: string
  placeholder?: string
  description?: string
}

/**
 * ConformColorField — ColorField wired to Conform.
 *
 * Binds a hex-string Conform field to the quebi ColorField: derives name, id,
 * form, required, default, and validity from the field metadata and renders
 * inline errors. The visible input is the real form control, so there is no
 * hidden mirror here — unlike `conform-color-picker`, which drives a popover.
 */
export function ConformColorField({
  field,
  label,
  placeholder,
  description,
  className,
  ...props
}: ConformColorFieldProps) {
  const hasErrors = !field.valid && !!field.errors
  const isRequired = field.required ?? false
  const initialValue = (field.initialValue as string) ?? ""

  return (
    <ColorField
      {...props}
      id={field.id}
      name={field.name}
      form={field.formId}
      defaultValue={initialValue === "" ? null : initialValue}
      isRequired={isRequired}
      isInvalid={hasErrors}
      // A `label` is rendered as a real `<Label>` below, and react-aria folds
      // an `aria-label` into the input's `aria-labelledby` chain *alongside*
      // it — so `?? label` announced "Brand color Brand color" (task #147).
      // The literal is only for the unlabelled case, where nothing else names
      // the control.
      aria-label={props["aria-label"] ?? (label ? undefined : "Color")}
      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>
      )}
      <ColorFieldGroup>
        <ColorInput placeholder={placeholder} />
      </ColorFieldGroup>
      {/* 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>}
    </ColorField>
  )
}