Form Field
Accessible form primitive for labels, descriptions, validation messages, required markers, disabled state, and aria-describedby wiring around a control.
Overview
Form Field groups a single form control with its visible label, helper text, required marker, and validation message. It keeps form layouts consistent and helps connect labels, descriptions, and validation states correctly across HTML, React, and Vue forms. Form Field is used across SaaS dashboards, admin panels, settings pages, billing forms, ecommerce product forms, ERP and POS customer forms, authentication forms, and Laravel, React, and Vue starter kits. It is available in @byteion/react and @byteion/vue.
Why this exists
Without a shared Form Field pattern, teams create inconsistent label spacing — sometimes mb-1, sometimes mb-4. Helper text is present in one form and missing in another. Error messages are disconnected from their fields — no aria-describedby, no aria-invalid. Placeholder text is used as the only label. Validation states are styled ad-hoc without theming support. Form Field unifies every form control into a single, accessible wrapper that generates correct id, htmlFor, aria-describedby, and aria-invalid automatically.
Mental model
- Label tells the user what the field is. It should be visible, concise, and associated with the control via
htmlFor/forandid. - Helper text explains how to complete it. Optional guidance that sits below the control. It is hidden when an error or success message is present.
- Error text explains what to fix. Shown after validation, connected with
aria-describedby. The control receivesaria-invalid="true". - Success text confirms a rare validation success. Used for inline availability checks (username, email) or transient confirmations.
- The control is the child element. Form Field wraps exactly one control —
<Input>,<Select>,<Textarea>, or any other field that acceptsid,aria-describedby,aria-invalid, andstate.
Form Field vs Input vs Label vs Fieldset
| Element | Purpose | Example |
|---|---|---|
| Form Field | Wrapper for one control with label, helper text, and validation | <FormField label="Email"><Input /></FormField> |
| Input / Select / Textarea | The actual form control | <Input type="email" placeholder="you@company.com" /> |
| Label | Names the form control | <label for="email">Email</label> |
| Fieldset / Legend | Groups related controls like radio or checkbox groups | <fieldset><legend>Plan</legend>...</fieldset> |
When to use
- Text inputs — name, email, username, URL, search queries
- Email and password fields — sign-in, sign-up, password reset forms
- Select fields — dropdown choices for country, role, category
- Textarea fields — bio, description, release notes, feedback
- Datepicker, search, and combobox wrappers
- Settings forms — account, profile, notification preferences
- Billing and account forms — payment details, address, plan selection
- Ecommerce admin forms — product details, pricing, inventory
- ERP and POS data-entry forms — customer info, orders, items
- Authentication forms — sign-in, sign-up, forgot password, OTP, reset password
When not to use
- Do not use Form Field for non-form content — it is a form-control wrapper, not a general spacing or layout component.
- Do not use it as a generic spacing wrapper — use Stack or Section for layout spacing.
- Do not wrap multiple unrelated controls in one Form Field — each control (first name, last name) needs its own Form Field.
- Do not use it for radio or checkbox groups — use
<fieldset>and<legend>or the matching group component (RadioGroup). - Do not use placeholder text as the only label — placeholders disappear on input and fail accessibility. Always use a visible label.
- Do not use success state for normal completion — success is for rare inline confirmations, not for every valid field.
Basic example
A minimal Form Field with a label and helper text. The label is visually associated with the input, and the helper text is connected via aria-describedby.
We'll never share your email with anyone else.
Labels and helper text
Every Form Field should have a visible label. Helper text provides optional context — requirements, format hints, or useful notes. Helper text is hidden when an error or success message replaces it.
Markdown is supported. Max 500 characters.
Validation states
Form Field supports three validation states: error, success, and default. Only one message is visible at a time — error and success replace helper text.
Please enter a valid email address.
Username is available.
Required fields
When required is set, Form Field appends a visual marker (*) after the label and validates that the field is filled. The visual marker alone is not enough — validation rules or the required attribute must also be applied where appropriate.
Anatomy
- Label — visible text above the control using
text-sm font-medium text-foreground - Required marker —
*intext-dangerappended after the label whenrequiredis true - Form control — the child element:
<Input>,<Select>,<Textarea>, etc. - Helper text (optional) —
text-xs text-muted-foregroundbelow the control - Error text (optional) —
text-xs text-danger, replaces helper text when present - Success text (optional) —
text-xs text-success, replaces helper text when present
Usage guidelines
- Use a visible label for every field. Placeholder is not a label — it disappears on input and fails accessibility requirements.
- Keep helper text short and useful. One or two sentences. If the guidance needs more space, link to documentation.
- Show one clear validation message at a time. Error replaces helper text. Do not show both simultaneously.
- Use error text to tell users how to fix the issue. Not just "Invalid input" but "Must be at least 8 characters."
- Use required only when the field is truly required. Over-marking required fields reduces their meaning.
- Keep label, control, and message close together. The group should be visually distinct from surrounding fields.
Do and Don't
✓ Do
- Use a visible label for every field, associated with
htmlFor/id. - Keep helper text short and useful — one or two sentences.
- Show one clear validation message at a time — error replaces helper text.
- Tell users how to fix the issue in error text.
- Use required only when the field is genuinely required.
- Keep label, control, and message close together as a visual group.
✗ Don't
- Use placeholder text as the only label — it disappears on input.
- Show helper and error messages together — they compete.
- Wrap unrelated fields in one Form Field.
- Rely only on red color for errors — include text and
aria-invalid. - Mark fields required visually without client or server validation.
- Put layout-only content inside Form Field.
Accessibility
- Associate label with control using
htmlFor(React) /for(HTML/Vue) and a matchingidon the control. Form Field generates these automatically when noidprop is provided. - Use
aria-describedbyto connect helper, error, or success text to the control. Screen readers announce the description when the field receives focus. - Use
aria-invalid="true"when the field has an error. This tells assistive technology that the field's value is not valid. - Error text should explain how to fix the problem. Not just "Error" but "Password must be at least 8 characters."
- Do not rely on color alone for validation state. Error state uses
text-dangercolor andaria-invalidand text content that describes the issue. - Required marker is not enough by itself. The
*symbol must be supplemented with therequiredattribute or client-side validation. - For grouped radio or checkbox controls, use
<fieldset>and<legend>, not Form Field wrapping each option. - Keep focus behavior on the child control, not the wrapper. Form Field does not trap or manage focus — the Input, Select, or Textarea handles keyboard interaction.
Customization
Form Field should be customized through tokens and child-control variants:
- Label typography —
text-sm font-medium text-foreground - Helper text color —
text-xs text-muted-foreground - Error text color —
text-xs text-danger - Success text color —
text-xs text-success - Vertical spacing —
mb-1.5(label to control),mt-1.5(control to message) - Control border and ring state —
border-border(default),border-danger(error),border-success(success) - Required marker color —
text-danger
HTML example
<!-- Default field with helper text -->
<div class="space-y-1.5">
<label for="email" class="text-sm font-medium text-foreground">
Email Address
</label>
<input id="email" type="email" placeholder="you@company.com"
class="h-10 w-full rounded-lg border border-border bg-background px-3.5 text-sm
text-foreground placeholder:text-muted-foreground
focus:outline-none focus:ring-2 focus:ring-primary/20 focus:border-primary"
aria-describedby="email-helper" />
<p id="email-helper" class="text-xs text-muted-foreground">
We'll never share your email.
</p>
</div>
<!-- Error state -->
<div class="space-y-1.5">
<label for="password" class="text-sm font-medium text-foreground">
Password <span class="text-danger">*</span>
</label>
<input id="password" type="password" required
class="h-10 w-full rounded-lg border border-danger bg-background px-3.5 text-sm
text-foreground placeholder:text-muted-foreground
focus:outline-none focus:ring-2 focus:ring-danger/20 focus:border-danger"
aria-invalid="true" aria-describedby="password-error" />
<p id="password-error" class="text-xs text-danger">
Must be at least 8 characters.
</p>
</div>
React example
import { FormField, Input } from '@byteion/react';
export default function SignupForm() {
return (
<div className="space-y-4 max-w-md">
<FormField
label="Email Address"
helperText="We'll never share your email."
>
<Input type="email" placeholder="you@company.com" />
</FormField>
<FormField
label="Password"
required
error="Must be at least 8 characters."
>
<Input type="password" />
</FormField>
<FormField
label="Username"
success="Username is available."
>
<Input type="text" />
</FormField>
</div>
);
}
Vue example
<script setup lang="ts">
import { FormField, Input } from '@byteion/vue';
</script>
<template>
<div class="space-y-4 max-w-md">
<FormField
label="Email Address"
helper-text="We'll never share your email."
>
<template #default="{ id, describedBy }">
<Input :id="id" :aria-describedby="describedBy"
type="email" placeholder="you@company.com" />
</template>
</FormField>
<FormField
label="Password"
required
error="Must be at least 8 characters."
>
<template #default="{ id, describedBy }">
<Input :id="id" :aria-describedby="describedBy"
type="password" />
</template>
</FormField>
<FormField
label="Username"
success="Username is available."
>
<template #default="{ id, describedBy }">
<Input :id="id" :aria-describedby="describedBy"
type="text" />
</template>
</FormField>
</div>
</template>
API Reference
React
| Prop | Type | Description |
|---|---|---|
label |
ReactNode |
Visible label. Associates with the child via htmlFor / id. |
helperText |
ReactNode |
Supporting text below the control. Hidden when error or success is present. |
error |
ReactNode |
Validation message. Sets aria-invalid and state="error" on the child if supported. |
success |
ReactNode |
Positive confirmation. Use sparingly — for username availability, email confirmations, etc. |
required |
boolean |
Appends * after the label. |
id |
string |
Override the auto-generated id for the control. |
children |
ReactElement |
A single form control. Receives generated id, aria-describedby, aria-invalid, and state where supported. |
Vue
| Prop | Type | Description |
|---|---|---|
label |
string |
Visible label text. |
helperText |
string |
Supporting text below the control. |
error |
string |
Validation error message. |
success |
string |
Positive confirmation message. |
required |
boolean |
Appends * after the label. |
id |
string |
Override the auto-generated id. Auto-generated if absent. |
Developer tips
- Prefer visible labels over placeholders. A label stays visible after the user types. A placeholder disappears, making it impossible to verify what the field expects.
- Use stable
idvalues for SSR or hydration-sensitive forms when the auto-generatedidcauses hydration mismatches between server and client. - Keep validation messages short. One line is ideal. If the rule needs more space, place it in a callout above the form.
- Use
errorfor blocking issues andhelperTextfor guidance. - Use
successsparingly. Inline success messages are useful for async validation (username check) but distracting when every valid field shows a checkmark. - Test generated
idvalues across React and Vue. Both frameworks auto-generate uniqueidattributes. Verify they do not conflict with otheridvalues on the page.
Common mistakes
- Placeholder-only labels — placeholders disappear on input. Use a visible
labelprop. - Missing
aria-describedby— helper and error text must be connected to the control for screen readers. - Missing
aria-invalidon errors — without it, assistive technology does not know the field has a validation error. - Duplicate
idvalues — using the sameidon multiple fields breaks label association. - Wrapping multiple controls in one Form Field — email and password are two fields, each needs its own Form Field.
- Showing helper and error text together — error replaces helper text. Both visible at once creates confusion.
- Required marker without actual validation — the
*alone does not enforce required behavior.
Production usage tips
- Test form validation with keyboard only. Tab through all fields, verify error messages are announced by screen readers, and confirm focus moves to the first invalid field on submit.
- Test screen reader announcements. Labels, helper text, and error messages should all be announced when the field receives focus.
- Test dark mode and branded themes. The
text-danger,text-success, andborder-dangertokens must remain distinguishable in all themes. - Test server-side validation messages. When the server returns errors, map them to the correct
errorprop per field. - Test translated labels and errors. Translated text can be significantly longer than the source. Ensure labels and messages do not break the layout.
- Keep React and Vue behavior aligned with the documented props. Both frameworks should support the same label, helperText, error, success, required, and id interface.
Premium examples
Advanced patterns available in Pro extend Form Field into richer form layouts with persistent validation and cross-field coordination.
- Billing form with validation states — a credit card form with Form Fields for card number, expiry, CVC, and billing address, each with its own error and success state.
- Profile settings form — a multi-section settings page with Form Fields for name, email, bio, and timezone, grouped by Section.
- Ecommerce product edit form — an admin product form with Form Fields for title, price, SKU, stock count, and description, using Select and Textarea as controls.
- ERP / POS customer form — a customer data-entry form with Form Fields for name, phone, email, address, and tax ID, with async validation on duplicate fields.
- Multi-step onboarding form — a stepped form where each step uses Form Fields for a logical group of inputs, coordinated with Stepper.
- Validation summary with linked fields — an Alert at the top of the form listing all errors. Each error links to its Form Field via
id, allowing users to jump to the failing field.
Related components
- Input — single-line text control used inside Form Field
- Select — dropdown select control used inside Form Field
- Textarea — multi-line text control used inside Form Field
- Checkbox — form checkbox control
- Radio — form radio control with RadioGroup for grouped options
- Switch — form toggle control
- Button — form submission and action triggers
- Alert — contextual messages for form-level validation summaries
API Reference
Related components
Runtime events
Byteion keeps framework APIs idiomatic: HTML uses DOM custom events, React uses callback props, and Vue uses emitted events / v-model updates.
HTML / JS events
No HTML runtime events are declared by this adapter.
React callbacks
No React callback events are declared by this adapter.
Vue emits
No Vue emitted events are declared by this adapter.
Remote / AJAX
This component does not implement Byteion JS remote loading. Use normal HTML form submission, framework state, or your application data layer around it.
Unlock Premium Patterns
Get access to copy-paste ready Pro blocks, SaaS templates, and advanced component patterns.
Explore Pro Components