component — conform
Conform Switch
The quebi Switch wired to Conform. Binds a boolean field's name, form, and default from field metadata without the defaultChecked/required trap of a blind spread.
- form
- conform
- switch
- toggle
- boolean
- validation
01 example
Bound to a Conform form
Submit with the switch off to see the validation error wired from field metadata.
source
conform-switch.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 type { SwitchProps } from "react-aria-components"
import { cn } from "@/lib/utils"
import { describedBy, Description, Field, FieldError } from "@/components/field"
import { Switch, SwitchLabel } from "@/components/switch"
export interface ConformSwitchProps
extends Omit<SwitchProps, "name" | "value" | "form" | "defaultSelected" | "isSelected"> {
/**
* The wrapper's class. A plain string, unlike the inherited
* `SwitchProps["className"]`: this lands on the `Field` div around the
* control, which has no render state to compose a function against. Keeping
* the wider type would let a caller pass a render prop that clsx drops in
* silence.
*/
className?: string
/** A switch bound to a boolean form value. */
field: FieldMetadata<boolean>
label?: string
description?: string
}
/**
* ConformSwitch — Switch wired to Conform.
*
* Binds a boolean Conform field to the quebi Switch: derives name, form,
* default, and the described-by wiring from the field metadata and renders
* inline errors.
*
* Two things about this control specifically:
*
* 1. `getInputProps(field, { type: "checkbox" })` must NOT be spread onto it.
* Conform returns the DOM names — `defaultChecked` and `required` — and
* react-aria's Switch takes neither, so `filterDOMProps` drops both without
* a TS error (JSX spread skips excess-property checking) and without a DOM
* attribute left behind to notice. The switch then renders off after a
* failed submit. Every metadata attribute is therefore read by name here.
* 2. react-aria's Switch has no `isRequired`/`isInvalid` — it omits them from
* its props on purpose, since a switch is never "unfilled". Required is a
* schema concern; invalidity is announced through `aria-describedby` and the
* message below.
*/
export function ConformSwitch({
field,
label,
description,
className,
...props
}: ConformSwitchProps) {
const hasErrors = !field.valid && !!field.errors
const isRequired = field.required ?? false
return (
<Field className={cn(className)}>
<Switch
{...props}
name={field.name}
form={field.formId}
value="on"
defaultSelected={field.defaultChecked}
aria-describedby={describedBy(
hasErrors && field.errorId,
description && field.descriptionId,
)}
>
{label && (
<SwitchLabel className={cn(hasErrors && "text-quebi-danger")}>
{label}
{isRequired && <span className="ml-1 text-quebi-fg">*</span>}
</SwitchLabel>
)}
</Switch>
{/* These ids are ours to set: the control above is not a react-aria
field, so nothing generates them and its aria-describedby is their
only reference. */}
{description && <Description id={field.descriptionId}>{description}</Description>}
{hasErrors && <FieldError id={field.errorId}>{field.errors?.join(", ")}</FieldError>}
</Field>
)
}