Skip to content

Login Form

A compact email and password form using Form, FormField, FormControl, and Button with no state library required.

A compact email + password form using Form, FormField, FormControl, and Button. No state library required — new FormData(form) at submit is enough.

import {
Button,
Form,
FormControl,
FormField,
FormLabel,
FormMessage,
Heading,
Input,
Link,
Stack,
} from '@arshad-shah/cynosure-react';
export function LoginForm() {
return (
<Stack gap="5" maxWidth="sm" marginX="auto" padding="6">
<Heading level={1} size="2xl">
Sign in
</Heading>
<Form
onSubmit={async (e) => {
e.preventDefault();
const data = Object.fromEntries(
new FormData(e.currentTarget).entries(),
);
await fetch('/api/session', {
method: 'POST',
body: JSON.stringify(data),
});
}}
>
<Stack gap="4">
<FormField name="email" required>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" autoComplete="email" />
</FormControl>
<FormMessage />
</FormField>
<FormField name="password" required>
<FormLabel>Password</FormLabel>
<FormControl>
<Input type="password" autoComplete="current-password" />
</FormControl>
<FormMessage />
</FormField>
<Button type="submit" size="lg" fullWidth>
Sign in
</Button>
<Link href="/forgot-password" size="sm">
Forgot your password?
</Link>
</Stack>
</Form>
</Stack>
);
}

See the Form + RHF + Zod recipe that ships alongside the form composition primitives for a fully validated variant of this form.

  • FormField wires id / aria-describedby / aria-invalid onto the <input> automatically.
  • FormLabel becomes the control’s accessible name; no extra aria-label needed.
  • FormMessage announces only when the field is invalid.