Form
Composable form primitives — Form, FormField, FormLabel, FormControl, FormDescription, FormMessage — that auto-wire labels, descriptions, validation, and aria-invalid for every field.
FormField generates a stable id, applies htmlFor on the label, and registers FormDescription / FormMessage ids on the control via aria-describedby; invalid state propagates aria-invalid.
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormLabel,
Input,
Stack,
} from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<Form
onSubmit={(event) => {
event.preventDefault();
}}
>
<Stack gap="4" style={{ width: 360 }}>
<FormField name="name" required>
<FormLabel>Your name</FormLabel>
<FormControl>
<Input />
</FormControl>
</FormField>
<FormField name="email" required>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" />
</FormControl>
<FormDescription>We will never share your address.</FormDescription>
</FormField>
<Button type="submit">Send</Button>
</Stack>
</Form>
);
}
import { Form, FormControl, FormDescription, FormField, FormLabel, FormMessage, Input,} from "@arshad-shah/cynosure-react";
<Form> <FormField name="email" required> <FormLabel>Email</FormLabel> <FormControl> <Input type="email" /> </FormControl> <FormDescription>We will never share your address.</FormDescription> <FormMessage /> </FormField></Form>Form is a thin <form> wrapper that sets noValidate to true by default so Cynosure’s FormMessage becomes the single source of validation UX.
Composition
Section titled “Composition”FormField is the one stateful primitive — it generates the id, owns the invalid / disabled / required flags, and provides a context the other parts read from.
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormLabel,
FormMessage,
Input,
Stack,
} from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<Form>
<Stack gap="4" style={{ width: 360 }}>
<FormField name="username" required>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="ada" />
</FormControl>
<FormDescription>3 to 20 lowercase letters or digits.</FormDescription>
<FormMessage />
</FormField>
<Button type="submit">Continue</Button>
</Stack>
</Form>
);
}
With validation
Section titled “With validation”Mark a field as invalid and render a FormMessage to show the error. FormMessage automatically registers its id on the control via aria-describedby and renders nothing when there’s no message.
import {
Button,
Form,
FormControl,
FormDescription,
FormField,
FormLabel,
FormMessage,
Input,
Stack,
} from '@arshad-shah/cynosure-react';
import { useState } from 'react';
export default function Example() {
const [email, setEmail] = useState('');
const invalid = email.length > 0 && !email.includes('@');
return (
<Form>
<Stack gap="4" style={{ width: 360 }}>
<FormField name="email" invalid={invalid} required>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" value={email} onChange={setEmail} />
</FormControl>
<FormDescription>We will never share your address.</FormDescription>
<FormMessage>{invalid ? 'Needs an @ symbol.' : undefined}</FormMessage>
</FormField>
<Button type="submit">Sign in</Button>
</Stack>
</Form>
);
}
Every field type
Section titled “Every field type”FormControl works with any Cynosure form control — Input, Textarea, Select, Checkbox, etc. — and forwards id, name, invalid, disabled, and required down to the child.
import {
Button,
Checkbox,
Form,
FormControl,
FormDescription,
FormField,
FormLabel,
Input,
Select,
Stack,
Textarea,
} from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<Form>
<Stack gap="4" style={{ width: 420 }}>
<FormField name="title" required>
<FormLabel>Title</FormLabel>
<FormControl>
<Input />
</FormControl>
</FormField>
<FormField name="description">
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea rows={4} />
</FormControl>
<FormDescription>Markdown supported.</FormDescription>
</FormField>
<FormField name="priority" required>
<FormLabel>Priority</FormLabel>
<FormControl>
<Select
items={[
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
]}
defaultValue="medium"
/>
</FormControl>
</FormField>
<FormField name="terms" required>
<Checkbox>I accept the terms</Checkbox>
</FormField>
<Button type="submit">Create</Button>
</Stack>
</Form>
);
}
Disabled field
Section titled “Disabled field”Setting disabled on the FormField cascades to the inner control without disabling the label.
import {
Form,
FormControl,
FormDescription,
FormField,
FormLabel,
Input,
Stack,
} from '@arshad-shah/cynosure-react';
export default function Example() {
return (
<Form>
<Stack gap="4" style={{ width: 360 }}>
<FormField name="email" disabled>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" defaultValue="you@example.com" />
</FormControl>
<FormDescription>Contact support to change your email.</FormDescription>
</FormField>
</Stack>
</Form>
);
}
FormField
Section titled “FormField”Accessibility
Section titled “Accessibility”FormFieldgenerates a stable id and applieshtmlForonFormLabelso the label and control are explicitly associated.FormDescriptionandFormMessageregister their ids on the control viaaria-describedby, so screen readers announce both hints and errors.invalidonFormFieldpropagatesaria-invalid="true"to the control and toggles adata-invalidattribute on the wrapper for CSS hooks.requiredpropagatesaria-required(and the nativerequiredattribute when supported).FormdefaultsnoValidatetotrue— setnoValidate={false}if you specifically want the browser’s native validation bubbles back.
Recipes
Section titled “Recipes”- Always wrap each control in a
FormFieldso id wiring andaria-describedbypropagation stays automatic. - Use
FormDescriptionfor persistent guidance andFormMessagefor transient validation — they share the wiring but role-distinct semantics. - Child props win over
FormFieldinheritance — passdisabled={false}on a specific input to keep it enabled inside a disabled field. - Pair with
react-hook-formor any state library —FormFieldprovides no state of its own.