Form fields
Labelled fields with descriptions, errors and widths: TextField, EmailField, PhoneField, PasswordField and seven more.
On this page
A field component renders a whole form row in one element: a FormGroup with a label, an optional description, the control and an error message, with the ARIA attributes between them already wired. There is one field per control type. Start with these; drop to the primitives only for layouts they don't cover.
import { EmailField, Form, PasswordField, TextField } from '@systhemaui/next'
export default function Demo() {
return (
<Form style={{ width: '100%', maxWidth: 480 }}>
<TextField name="fields-name" label="Full name" autoComplete="name" required />
<EmailField name="fields-email" label="Email" description="We never share it." required />
<PasswordField
name="fields-password"
label="Password"
description="At least 12 characters."
descriptionPosition="bottom"
autoComplete="new-password"
minLength={12}
required
/>
</Form>
)
}ImportLink to this section
import { EmailField, TextField } from '@systhemaui/next'In a React app without Next.js, import them from @systhemaui/react.
Shared propsLink to this section
Every field takes these props on top of its control's own attributes:
| Prop | What it does |
|---|---|
name | Required. Also the default id, which the label's htmlFor points at |
label | The label. Any React node on the single-control fields; a string on RadioField and CheckboxField |
description | Help text in a FormDescription |
descriptionPosition | 'top' (default) puts the description between label and control, 'bottom' below the control |
invalidMessage | The error text, in a FormInvalidMessage |
width | A CSS percentage that sizes the field inside a FormRow from md up (see Widths) |
slotProps | Props for the surrounding elements: group, label, description, invalidMessage (and inputGroup on the group fields) |
Everything else (required, placeholder, defaultValue, onChange, className, …) goes to the control itself.
ExamplesLink to this section
Description positionLink to this section
Shown on your profile.
Shown on your profile.
import { FormRow, TextField } from '@systhemaui/next'
export default function Demo() {
return (
<FormRow style={{ width: '100%', maxWidth: 640 }}>
<TextField name="position-top" label="Username" description="Shown on your profile." />
<TextField
name="position-bottom"
label="Display name"
description="Shown on your profile."
descriptionPosition="bottom"
/>
</FormRow>
)
}Showing an errorLink to this section
The error message stays hidden until the browser marks the control :user-invalid, which happens once the visitor has changed the value or tried to submit the form. So with native validation (required, pattern, minLength, the input type), pass invalidMessage up front and the message appears at the right moment. Type something that isn't an email address below and leave the field:
import { EmailField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<EmailField
name="error-native"
label="Email"
required
invalidMessage="Enter an email address like jane@example.com."
/>
</div>
)
}When you validate in JavaScript (or on the server), add the is-invalid class to the control: it forces the invalid style and shows the message immediately.
import { TextField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<TextField
name="error-server"
label="Voucher code"
defaultValue="SUMMER2019"
className="is-invalid"
invalidMessage="This code expired on 31 August 2019."
/>
</div>
)
}WidthsLink to this section
import { EmailField, FormRow, PhoneField, TextField } from '@systhemaui/next'
export default function Demo() {
return (
<FormRow style={{ width: '100%', maxWidth: 640 }}>
<TextField name="widths-first" label="First name" width="50%" />
<TextField name="widths-last" label="Last name" width="50%" />
<EmailField name="widths-email" label="Email" width="67%" />
<PhoneField name="widths-phone" label="Phone" width="33%" />
</FormRow>
)
}Slot propsLink to this section
slotProps reaches the elements around the control without dropping to primitives, for example to give the group an id or restyle the description. The control's own props stay at the top level:
;<TextField
name="company"
label="Company"
description="Optional"
autoComplete="organization"
slotProps={{
group: { id: 'company-group' },
description: { className: 'italic' },
}}
/>FieldsLink to this section
TextFieldLink to this section
A text input. type defaults to 'text'; override it for other text-like inputs such as url or search. For email, phone, number, date and password, use the dedicated field, which sets the right inputMode and autoComplete.
Include https://.
import { TextField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<TextField
name="field-website"
type="url"
label="Website"
placeholder="https://"
description="Include https://."
invalidMessage="Enter a full URL."
/>
</div>
)
}EmailFieldLink to this section
An type="email" input with inputMode="email", autoComplete="email" and a case-insensitive pattern that requires a domain with a top-level part (jane@example fails, Jane.Doe+news@Example.com passes). Each default can be overridden.
import { EmailField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<EmailField
name="field-email"
label="Work email"
defaultValue="jane@example"
className="is-invalid"
invalidMessage="Add the domain ending, for example .com."
/>
</div>
)
}PhoneFieldLink to this section
A type="tel" input with inputMode="tel" and autoComplete="tel". It adds no pattern, because phone formats vary; pass pattern if you need one.
Only used for delivery updates.
import { PhoneField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<PhoneField
name="field-phone"
label="Phone"
placeholder="+32 470 12 34 56"
description="Only used for delivery updates."
/>
</div>
)
}PasswordFieldLink to this section
A type="password" input. Set autoComplete to current-password on sign-in forms and new-password on sign-up and reset forms, so password managers act correctly.
At least 12 characters.
import { PasswordField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<PasswordField
name="field-password"
label="New password"
autoComplete="new-password"
minLength={12}
required
description="At least 12 characters."
descriptionPosition="bottom"
invalidMessage="Use at least 12 characters."
/>
</div>
)
}NumberFieldLink to this section
A type="number" input with inputMode="decimal". Use min, max and step for the range.
Up to 12 per booking.
import { NumberField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<NumberField
name="field-guests"
label="Guests"
min={1}
max={12}
defaultValue={2}
description="Up to 12 per booking."
invalidMessage="Choose between 1 and 12 guests."
/>
</div>
)
}DateFieldLink to this section
A date input. inputType picks 'date' (default) or 'datetime-local'; min and max take ISO strings.
import { DateField, FormRow } from '@systhemaui/next'
export default function Demo() {
return (
<FormRow style={{ width: '100%', maxWidth: 640 }}>
<DateField name="field-arrival" label="Arrival" min="2026-01-01" />
<DateField name="field-meeting" label="Call" inputType="datetime-local" />
</FormRow>
)
}TextareaFieldLink to this section
A multi-line text area. The default CSS gives it a fixed height of 328 px with resizing off; resize lets the visitor resize it vertically, and a style or class sets another height.
Tell us about your project.
import { TextareaField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<TextareaField
name="field-message"
label="Message"
description="Tell us about your project."
maxLength={2000}
resize
style={{ height: 160 }}
/>
</div>
)
}SelectFieldLink to this section
A FormSelect with a label and messages. It takes the same Option and SelectPlaceholder children and the same selectClassName, iconClassName, prepend, append and disableIcon props. className goes on the select wrapper.
Routes your message to the right team.
import { Option, SelectField, SelectPlaceholder } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<SelectField
name="field-topic"
label="Topic"
required
description="Routes your message to the right team."
invalidMessage="Choose a topic."
>
<SelectPlaceholder>Choose a topic</SelectPlaceholder>
<Option value="sales">Sales</Option>
<Option value="support">Support</Option>
<Option value="press">Press</Option>
</SelectField>
</div>
)
}FileFieldLink to this section
A FormFileInput with a label and messages; accept and multiple pass through.
Images or PDF, up to 10 MB each.
import { FileField } from '@systhemaui/next'
export default function Demo() {
return (
<div style={{ width: '100%', maxWidth: 480 }}>
<FileField
name="field-attachments"
label="Attachments"
accept="image/*,.pdf"
multiple
description="Images or PDF, up to 10 MB each."
/>
</div>
)
}RadioField and CheckboxFieldLink to this section
The group fields take their choices as items, each an object with the FormRadio or FormCheckbox props minus name. The group label is a <div> that names the group through aria-labelledby, and the description and error attach to the group. required is applied to every radio, so one choice is needed.
Pick-up is free.
import { RadioField } from '@systhemaui/next'
export default function Demo() {
return (
<RadioField
name="field-delivery"
label="Delivery"
description="Pick-up is free."
required
items={[
{
id: 'delivery-standard',
value: 'standard',
label: 'Standard (3 to 5 days)',
defaultChecked: true,
},
{ id: 'delivery-express', value: 'express', label: 'Express (next day)' },
{ id: 'delivery-pickup', value: 'pickup', label: 'Pick up in store' },
]}
/>
)
}Each item's id defaults to its value. Two groups on one page with the same values (yes / no) would then share ids, so give the items ids, as above.
For checkboxes, requiredMode decides what required means: 'each' (default) puts required on every box, for a list of things that must all be confirmed; 'any' asks for at least one box. In 'any' mode the field reads the items' checked props, so control the items with state:
Choose at least one.
'use client'
import { useState } from 'react'
import { CheckboxField } from '@systhemaui/next'
const topics = [
{ value: 'design', label: 'Design' },
{ value: 'development', label: 'Development' },
{ value: 'content', label: 'Content' },
]
export default function Demo() {
const [selected, setSelected] = useState<string[]>(['design'])
return (
<CheckboxField
name="field-topics"
label="Topics"
description="Choose at least one."
required
requiredMode="any"
invalidMessage="Choose at least one topic."
items={topics.map((topic) => ({
...topic,
id: `topics-${topic.value}`,
checked: selected.includes(topic.value),
onChange: () =>
setSelected((current) =>
current.includes(topic.value)
? current.filter((value) => value !== topic.value)
: [...current, topic.value],
),
}))}
/>
)
}PropsLink to this section
TextFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | |
name (required) | string | - | |
autoComplete | string | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
type | HTMLInputTypeAttribute | 'text' | Defaults to 'text'. Override for niche text-style inputs (e.g. 'search', 'url'). For email, tel, number, date, password, prefer the dedicated EmailField / PhoneField / NumberField / DateField / PasswordField — those carry sensible default inputMode, autoComplete, and (for email) pattern. |
width | string | - | |
slotProps | FieldSlotProps | - | |
…and all <input> attributes |
EmailFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | |
name (required) | string | - | |
autoComplete | string | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
PhoneFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | |
name (required) | string | - | |
autoComplete | string | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
PasswordFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | |
name (required) | string | - | |
autoComplete | string | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
NumberFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | |
name (required) | string | - | |
autoComplete | string | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
DateFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | |
name (required) | string | - | |
autoComplete | string | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
inputType | 'date' | 'datetime-local' | 'date' | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
TextareaFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
name (required) | string | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
id | string | - | |
resize | boolean | false | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
SelectFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
children (required) | ReactNode | - | |
name (required) | string | - | |
prepend | ReactNode | - | |
append | ReactNode | - | |
selectClassName | string | - | |
iconClassName | string | - | |
disableIcon | boolean | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
id | string | - | |
className | string | - | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
FileFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | - | |
name (required) | string | - | |
accept | string | - | |
multiple | boolean | - | |
label | ReactNode | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
width | string | - | |
slotProps | FieldSlotProps | - | |
| …and all inherited HTML attributes |
RadioFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
name (required) | string | - | |
items (required) | Omit<FormRadioProps, 'name'>[] | - | |
label | string | - | |
id | string | - | |
required | boolean | - | |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
width | string | - | |
slotProps | InlineFieldSlotProps | - |
CheckboxFieldLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
name (required) | string | - | |
items (required) | Omit<FormCheckboxProps, 'name'>[] | - | |
label | string | - | |
id | string | - | |
required | boolean | - | |
requiredMode | 'each' | 'any' | "each" | How required validation works for checkbox groups: - "each": every checkbox must be checked (native HTML required on each input) - "any": at least one checkbox must be checked (uses a hidden proxy input for native validation) |
description | ReactNode | - | |
descriptionPosition | 'top' | 'bottom' | - | |
invalidMessage | ReactNode | - | |
width | string | - | |
slotProps | InlineFieldSlotProps | - |
FieldSlotPropsLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
group | Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> | - | |
label | Omit<Omit<DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>, 'ref'>, 'htmlFor'> | - | |
description | Omit<DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>, 'ref'> | - | |
invalidMessage | Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> | - |
InlineFieldSlotPropsLink to this section
| Prop | Type | Default | Description |
|---|---|---|---|
group | Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> | - | |
label | Omit<Omit<DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTMLLabelElement>, 'ref'>, 'htmlFor'> | - | |
description | Omit<DetailedHTMLProps<HTMLAttributes<HTMLParagraphElement>, HTMLParagraphElement>, 'ref'> | - | |
invalidMessage | Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> | - | |
inputGroup | Omit<DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>, 'ref'> | - |
HTML and CSSLink to this section
A field renders this markup. Write it by hand in an HTML project; the ids and aria-* attributes are what the component adds for you.
<div class="form-group" style="--width: 50%">
<label class="form-label" for="email">Email</label>
<p class="form-description" id="email-description">We never share it.</p>
<input
class="form-input"
id="email"
name="email"
type="email"
inputmode="email"
autocomplete="email"
required
aria-describedby="email-description email-error"
/>
<div class="form-invalid-message" id="email-error" role="alert">Enter a valid email.</div>
</div>
<div class="form-group">
<div class="form-label" id="delivery-label">Delivery</div>
<div class="form-inline-input-group" role="radiogroup" aria-labelledby="delivery-label">
<div class="form-radio">
<input type="radio" id="standard" name="delivery" value="standard" required />
<label for="standard">Standard</label>
</div>
<div class="form-radio">
<input type="radio" id="express" name="delivery" value="express" required />
<label for="express">Express</label>
</div>
</div>
</div>| Class | Styles |
|---|---|
form | & > :not([hidden], input[type="hidden"]) ~ :not([hidden], input[type="hidden"]) { margin-top: var(--form-row-gap-y); } |
form-row | @media (width < 640px) { & { display: flex; flex-direction: column; row-gap: var(--form-row-gap-y); } }@media (width >= 640px) { & { display: flex; flex-wrap: wrap; gap: var(--form-row-gap-x); } }@media (width >= 640px) { & > .form-group { flex: 1 1 0%; min-width: 0; } }@media (width >= 640px) { & > .form-group[style*="--width"] { flex: 0 0 auto; width: calc(var(--width) - var(--form-row-gap-x) * (1 - var(--width) / 100%)); } } |
form-group | @media (width >= 640px) { .form-row > .form-group { flex: 1 1 0%; min-width: 0; } }@media (width >= 640px) { .form-row > .form-group[style*="--width"] { flex: 0 0 auto; width: calc(var(--width) - var(--form-row-gap-x) * (1 - var(--width) / 100%)); } }& > :not([hidden]) ~ :not([hidden]) { margin-top: var(--form-group-gap-y); }&:has(:required) .form-label::after { content: ' *'; color: var(--color-form-label-required); }+ 1 more rule |
form-label | & { display: block; color: var(--color-form-label); }& { font-family: var(--font-form-label-font-family); font-size: var(--typography-form-label-font-size); font-weight: var(--font-form-label-font-weight); letter-spacing: var(--typography-form-label-letter-spacing); line-height: var(--typography-form-label-line-height); text-transform: none; text-decoration: none; font-style: var(--font-form-label-font-style); }+ 1 more rule |
form-description | & { display: block; color: var(--color-form-description); }& { font-family: var(--font-form-description-font-family); font-size: var(--typography-form-description-font-size); font-weight: var(--font-form-description-font-weight); letter-spacing: var(--typography-form-description-letter-spacing); line-height: var(--typography-form-description-line-height); text-transform: none; text-decoration: none; font-style: var(--font-form-description-font-style); } |
form-input | & { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…+ 4 more rules |
form-select | & { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…+ 12 more rules |
form-textarea | & { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…+ 5 more rules |
search-field-input | & { --tw-shadow-color: var(--color-form-input-block-default-shadow); --tw-shadow: var(--tw-shadow-color); width: 100%; outline: none; border-style: solid; padding-inline: var(--form-input-block-padding-x); padding-block: var(--form-input-block-padding-y); border-radius: var(--form-input-block-border-radius); border-width: var(--form-input-block-border-width); background-color: var(--color-form-input-block-default-background); border-color: var(--color-form-input-block-defaul…+ 9 more rules |
is-invalid | :is(.form-select:user-invalid ~ .form-select-icon, .form-select.is-invalid ~ .form-select-icon) { color: var(--color-form-input-block-invalid-icon); background-color: var(--color-form-input-block-invalid-icon); }:is(.form-group:has(:user-invalid) .form-invalid-message, .form-group:has(.is-invalid) .form-invalid-message) { display: block; } |
form-select-wrapper | position: relative; |
form-select-icon | :is(.form-select ~ .form-select-icon) { pointer-events: none; position: absolute; top: 50%; transform: translateY(-50%); object-fit: contain; object-position: center; overflow: visible; line-height: 100%; right: var(--form-input-block-padding-x); width: var(--form-input-block-icon-size); height: var(--form-input-block-icon-size); font-size: var(--form-input-block-icon-size); color: var(--color-form-input-block-default-icon); }+ 3 more rules |
search-field | position: relative; |
search-field-icon | :is(.search-field-input ~ .search-field-icon) { pointer-events: none; position: absolute; top: 50%; transform: translateY(-50%); object-fit: contain; object-position: center; overflow: visible; line-height: 100%; right: var(--form-input-block-padding-x); width: var(--form-input-block-icon-size); height: var(--form-input-block-icon-size); font-size: var(--form-input-block-icon-size); color: var(--color-form-input-block-default-icon); }+ 2 more rules |
form-inline-input-group | & > :not([hidden]) ~ :not([hidden]) { margin-top: var(--form-input-inline-space-y); } |
form-radio | & { position: relative; user-select: none; }& input[type="radio"], .form-radio input[type="checkbox"], .form-radio:before { content: ""; position: absolute; left: 0; top: calc(var(--typography-form-inline-line-height) / 2); transform: translateY(-50%); width: var(--form-input-inline-input-size); height: var(--form-input-inline-input-size); }& input[type="radio"], .form-radio input[type="checkbox"] { opacity: 0; cursor: pointer; }&:has(:checked):after { opacity: 1; }+ 13 more rules |
form-checkbox | & { position: relative; user-select: none; }& input[type="radio"], .form-checkbox input[type="checkbox"], .form-checkbox:before { content: ""; position: absolute; left: 0; top: calc(var(--typography-form-inline-line-height) / 2); transform: translateY(-50%); width: var(--form-input-inline-input-size); height: var(--form-input-inline-input-size); }& input[type="radio"], .form-checkbox input[type="checkbox"] { opacity: 0; cursor: pointer; }+ 14 more rules |
form-invalid-message | & { display: none; color: var(--color-form-invalid-message); }:is(.form-group:has(:user-invalid) .form-invalid-message, .form-group:has(.is-invalid) .form-invalid-message) { display: block; } |
form-file-input | & { cursor: pointer; padding: 0; height: calc(var(--form-input-block-padding-y) * 2 + var(--typography-form-block-line-height)); display: flex; align-items: center; }+ 2 more rules |
Next.jsLink to this section
@systhemaui/next re-exports the field components from @systhemaui/react unchanged. They render on the server; only a module that passes event handlers (like the requiredMode="any" example) needs 'use client'.
AccessibilityLink to this section
Every field wires its parts through FormFieldShell, the shared wrapper behind all eleven fields:
- The label is a
<label for>pointing at the control'sid(iddefaults toname). - The description gets the id
<id>-descriptionand the error<id>-error. Both ids are added to the control'saria-describedby, after anyaria-describedbyyou pass yourself. - When
invalidMessageis set, the control getsaria-invalid="true", and the message is arole="alert"so it is announced when it appears. RadioFieldrenders its items in arole="radiogroup"andCheckboxFieldin arole="group", both named by the group label througharia-labelledby(<name>-label). For these,aria-describedbyandaria-invalidgo on the group element.- The required asterisk after the label is decorative CSS; assistive technology reads the
requiredattribute.
See Accessibility for what the components guarantee across the system.
RelatedLink to this section
- Form
- Form controls
- Select and Radios and checkboxes
- Form block and the Forms module