Patterns
Field
A form field with a label above the control and a hint or an error below it.
Patterns
A form field with a label above the control and a hint or an error below it.
import { Button, Field, Input, Textarea } from 'ferry-ui'
export default function FieldHero() {
return (
<form className="mx-auto flex w-full max-w-sm flex-col gap-5" onSubmit={(event) => event.preventDefault()}>
<Field label="Project name">
<Input placeholder="Billing portal" />
</Field>
<Field label="Billing email" hint="The app sends the invoices to this address.">
<Input type="email" placeholder="maya@example.com" />
</Field>
<Field label="Description" optional>
<Textarea placeholder="What is this project about?" />
</Field>
<Button type="submit" variant="primary" size="md" className="self-end">
Create project
</Button>
</form>
)
}Field in a dialog, a popover, a sign-in form or a form that stands alone.FormRow of Form Card.hint.Import the component. A field has one part. Its child is the control.
import { Field, Input } from 'ferry-ui'
<Field label="" hint="" error="">
<Input />
</Field>Field sets id, aria-invalid and aria-describedby on the control for you.
Pass the message to error. The message shows below the control and the control gets its error border. The error takes the place of the hint.
Enter a valid email address.
import * as React from 'react'
import { Field, Input } from 'ferry-ui'
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
export default function FieldError() {
const [email, setEmail] = React.useState('maya@example')
const error = email !== '' && !EMAIL.test(email) ? 'Enter a valid email address.' : undefined
return (
<Field label="Work email" hint="The app sends the sign-in link here." error={error} className="mx-auto w-full max-w-sm">
<Input type="email" value={email} onChange={(event) => setEmail(event.target.value)} />
</Field>
)
}If the hint helps the user to correct the value, set errorReplacesHint to false. The field then shows the hint and the error.
Between 1 and 50. Each seat is on the invoice.
Enter a whole number between 1 and 50.
import * as React from 'react'
import { Field, Input } from 'ferry-ui'
export default function FieldErrorAndHint() {
const [seats, setSeats] = React.useState('80')
const count = Number(seats)
const valid = Number.isInteger(count) && count >= 1 && count <= 50
return (
<Field
label="Seats"
hint="Between 1 and 50. Each seat is on the invoice."
error={valid ? undefined : 'Enter a whole number between 1 and 50.'}
errorReplacesHint={false}
className="mx-auto w-full max-w-sm"
>
<Input type="number" value={seats} onChange={(event) => setSeats(event.target.value)} className="w-24 tabular" />
</Field>
)
}The optional prop adds "(optional)" to the label. Pass a text to replace these words. Use the mark when most fields of the form are required.
The name shows on your invoices.
import { Field, Input } from 'ferry-ui'
export default function FieldOptional() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-5">
<Field label="Company" optional hint="The name shows on your invoices.">
<Input placeholder="Acme" />
</Field>
<Field label="Tax number" optional="(if you have one)">
<Input mono />
</Field>
</div>
)
}The size prop sets the text size of the label and of the hint.
For a form in a page.
For a form in a dialog or a popover.
import { Field, Input } from 'ferry-ui'
export default function FieldSizes() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
<Field size="md" label="Medium" hint="For a form in a page.">
<Input placeholder="Billing portal" />
</Field>
<Field size="sm" label="Small" hint="For a form in a dialog or a popover.">
<Input placeholder="Billing portal" />
</Field>
</div>
)
}| Size | Use |
|---|---|
md (default) | A form in a page, a sign-in form. |
sm | A form in a dialog or a popover. |
The labelVariant prop sets the look of the label.
The default label.
The subtle label.
The mono label, above a value to copy.
import { CopyField, Field, Input } from 'ferry-ui'
export default function FieldLabelVariants() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-6">
<Field label="Project name" hint="The default label.">
<Input placeholder="Billing portal" />
</Field>
<Field labelVariant="subtle" label="Email" hint="The subtle label.">
<Input type="email" placeholder="maya@example.com" />
</Field>
<Field labelVariant="mono" size="sm" label="API base URL" hint="The mono label, above a value to copy.">
<CopyField size="sm" value="https://api.example.com/v1" what="API base URL" />
</Field>
</div>
)
}| Variant | Use |
|---|---|
default | The standard label, with a medium weight. |
subtle | A sign-in form, a secondary field. |
mono | A read-only value in a popover or a panel. Use it with size="sm". |
A Select has many parts. Pass a function as the child. Spread its control argument on the part that takes the focus.
The app uses it for new invoices.
import { Field, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from 'ferry-ui'
export default function FieldRenderFunction() {
return (
<Field label="Currency" hint="The app uses it for new invoices." className="mx-auto w-full max-w-sm">
{(control) => (
<Select defaultValue="eur">
{/* The trigger takes the focus: it gets the id and the ARIA attributes. */}
<SelectTrigger {...control} className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="eur">Euro (EUR)</SelectItem>
<SelectItem value="usd">US dollar (USD)</SelectItem>
<SelectItem value="gbp">Pound sterling (GBP)</SelectItem>
</SelectContent>
</Select>
)}
</Field>
)
}Use the same function to put a button next to an Input. Only the input gets the control props.
The member gets an email with a link.
import { Button, Field, Input } from 'ferry-ui'
import { Plus } from 'lucide-react'
export default function FieldInputWithButton() {
return (
<Field label="Invite by email" hint="The member gets an email with a link." className="mx-auto w-full max-w-sm">
{(control) => (
<div className="flex gap-2">
<Input {...control} type="email" placeholder="maya@example.com" />
<Button icon={<Plus />} size="md" className="shrink-0">
Add
</Button>
</div>
)}
</Field>
)
}A Radio Group has no single control for the label. Set labelAs to span. The group then gets aria-labelledby.
You can change the period later.
import { Field, Label, RadioGroup, RadioGroupItem } from 'ferry-ui'
export default function FieldGroup() {
return (
<Field label="Billing period" labelAs="span" hint="You can change the period later." className="mx-auto w-full max-w-sm">
<RadioGroup defaultValue="monthly">
<Label className="font-normal">
<RadioGroupItem value="monthly" /> Monthly
</Label>
<Label className="font-normal">
<RadioGroupItem value="yearly" /> Yearly, two months free
</Label>
</RadioGroup>
</Field>
)
}role="alert". Screen readers read the message when it shows.aria-describedby of the control points to the hint and to the error.Field also accepts each attribute of the <div> element. They go to the root element.
| Prop | Type | Default |
|---|---|---|
labelRequired | ReactNode | - |
Field name shown above the control. Keep it short (1–4 words); put guidance in | ||
childrenRequired | ReactElement | ((control: FieldControlProps, meta: FieldRenderMeta) => ReactNode) | - |
The control. Either a single element (Input, Textarea, RadioGroup…): Field injects | ||
id | string | - |
Id of the control. Defaults to the child control's own | ||
hint | ReactNode | - |
Help text under the control (format rules, what the value is used for). | ||
error | ReactNode | - |
Validation message under the control, rendered as | ||
optional | ReactNode | false |
Appends a muted marker to the label: | ||
errorReplacesHint | boolean | true |
| ||
size | "sm" | "md" | md |
Type scale. | ||
labelVariant | "default" | "mono" | "subtle" | default |
Label style. | ||
labelAs | "label" | "span" | label |
Label element. | ||
The function gets two arguments: control, then an object with labelId.
Key of control | Type | Role |
|---|---|---|
id | string | The id of the control. |
aria-invalid | true | Set while the field shows an error. |
aria-describedby | string | The ids of the hint and of the error. |
aria-labelledby | string | The id of the label. Set only with labelAs="span". |