Form
A form wrapper component that simplifies validation and submission. Built with Base UI and Tailwind CSS. Copy-paste ready.
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
export default function Particle() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
setLoading(true);
await new Promise((r) => setTimeout(r, 800));
setLoading(false);
alert(`Email: ${formData.get("email") || ""}`);
};
return (
<Form className="flex w-full max-w-64 flex-col gap-4" onSubmit={onSubmit}>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input placeholder="you@example.com" required type="email" />
<FieldError>Please enter a valid email.</FieldError>
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
);
}
Installation
pnpm dlx shadcn@latest add @cnippet/form
Usage
import {
Field,
FieldError,
FieldLabel,
} from "@/components/ui/field"
import { Form } from "@/components/ui/form"
import { Input } from "@/components/ui/input"<Form
onSubmit={(e) => {
/* handle submit */
}}
>
<Field>
<FieldLabel>Email</FieldLabel>
<Input name="email" type="email" required />
<FieldError>Please enter a valid email.</FieldError>
</Field>
</Form>Examples
Using with Zod
Integrates the Form component with a Zod schema for type-safe client-side validation, showing inline field errors on submit.
"use client";
import type { FormEvent } from "react";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const schema = z.object({
age: z.coerce
.number({ message: "Please enter a number." })
.positive({ message: "Number must be positive." }),
name: z.string().min(1, { message: "Please enter a name." }),
});
type Errors = Record<string, string | string[]>;
async function submitForm(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const result = schema.safeParse(Object.fromEntries(formData));
if (!result.success) {
const { fieldErrors } = z.flattenError(result.error);
return { errors: fieldErrors as Errors };
}
return {
errors: {} as Errors,
};
}
export default function Particle() {
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
setLoading(true);
const response = await submitForm(event);
await new Promise((r) => setTimeout(r, 800));
setErrors(response.errors);
setLoading(false);
if (Object.keys(response.errors).length === 0) {
alert(
`Name: ${String(formData.get("name") || "")}\nAge: ${String(
formData.get("age") || "",
)}`,
);
}
};
return (
<Form
className="flex w-full max-w-64 flex-col gap-4"
errors={errors}
onSubmit={onSubmit}
>
<Field name="name">
<FieldLabel>Name</FieldLabel>
<Input placeholder="Enter name" />
<FieldError />
</Field>
<Field name="age">
<FieldLabel>Age</FieldLabel>
<Input placeholder="Enter age" />
<FieldError />
</Field>
<Button loading={loading} type="submit">
Submit
</Button>
</Form>
);
}
Sign In
Email and password fields with a show/hide visibility toggle, a "Forgot password?" link, a "Remember me" checkbox, and a "Create account" fallback.
Welcome back
Sign in to your account to continue.
"use client";
import { EyeIcon, EyeOffIcon } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
export function Pattern() {
const [loading, setLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
};
return (
<div className="w-full max-w-sm space-y-6">
<div className="space-y-1">
<h2 className="font-semibold text-lg">Welcome back</h2>
<p className="text-muted-foreground text-sm">
Sign in to your account to continue.
</p>
</div>
<Form className="gap-4" onSubmit={onSubmit}>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input
autoComplete="email"
placeholder="you@example.com"
required
type="email"
/>
<FieldError />
</Field>
<Field name="password">
<div className="flex items-center justify-between">
<FieldLabel>Password</FieldLabel>
<button
className="text-muted-foreground text-xs underline-offset-2 transition-colors hover:text-foreground hover:underline"
type="button"
>
Forgot password?
</button>
</div>
<div className="relative w-full">
<Input
autoComplete="current-password"
className="pr-10"
placeholder="••••••••"
required
type={showPassword ? "text" : "password"}
/>
<button
aria-label={showPassword ? "Hide password" : "Show password"}
className="absolute top-1/2 right-3 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
onClick={() => setShowPassword((v) => !v)}
type="button"
>
{showPassword ? (
<EyeOffIcon className="size-4" />
) : (
<EyeIcon className="size-4" />
)}
</button>
</div>
<FieldError />
</Field>
<Label className="flex items-center gap-2 font-normal">
<Checkbox name="remember" />
Remember me for 30 days
</Label>
<Button className="w-full" loading={loading} type="submit">
Sign in
</Button>
</Form>
<div className="flex items-center gap-3">
<Separator className="flex-1" />
<span className="text-muted-foreground text-xs">
Don't have an account?
</span>
<Separator className="flex-1" />
</div>
<Button className="w-full" type="button" variant="outline">
Create account
</Button>
</div>
);
}
Profile Settings
A card-wrapped profile editor with first and last name, username (with field description), role select, bio textarea with character hint, and website URL. The save button shows "Saved!" for three seconds after submission.
"use client";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardPanel,
CardTitle,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Separator } from "@/components/ui/separator";
import { Textarea } from "@/components/ui/textarea";
export function Pattern() {
const [loading, setLoading] = useState(false);
const [saved, setSaved] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
setSaved(true);
setTimeout(() => setSaved(false), 3000);
};
return (
<div className="w-full max-w-lg">
<Card>
<CardHeader className="border-b">
<CardTitle>Profile</CardTitle>
<CardDescription>
Update your public profile information.
</CardDescription>
</CardHeader>
<Form onSubmit={onSubmit}>
<CardPanel className="flex flex-col gap-4">
<div className="grid grid-cols-2 gap-4">
<Field name="firstName">
<FieldLabel>First name</FieldLabel>
<Input defaultValue="Alex" placeholder="First name" />
<FieldError />
</Field>
<Field name="lastName">
<FieldLabel>Last name</FieldLabel>
<Input defaultValue="Rivera" placeholder="Last name" />
<FieldError />
</Field>
</div>
<Field name="username">
<FieldLabel>Username</FieldLabel>
<Input defaultValue="alexrivera" placeholder="username" />
<FieldDescription>
Your unique handle — visible on your public profile.
</FieldDescription>
<FieldError />
</Field>
<Field name="role">
<FieldLabel>Role</FieldLabel>
<Select defaultValue="engineer">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value="designer">Designer</SelectItem>
<SelectItem value="engineer">Engineer</SelectItem>
<SelectItem value="manager">Product Manager</SelectItem>
<SelectItem value="founder">Founder</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectPopup>
</Select>
<FieldError />
</Field>
<Field name="bio">
<FieldLabel>Bio</FieldLabel>
<Textarea
defaultValue="Building design systems and open-source tools."
placeholder="Tell us a little about yourself…"
rows={3}
/>
<FieldDescription>Max 160 characters.</FieldDescription>
<FieldError />
</Field>
<Field name="website">
<FieldLabel>Website</FieldLabel>
<Input
defaultValue="https://alexrivera.dev"
placeholder="https://yoursite.com"
type="url"
/>
<FieldError />
</Field>
</CardPanel>
<Separator />
<CardFooter className="justify-end gap-3">
<Button type="reset" variant="outline">
Reset
</Button>
<Button loading={loading} type="submit">
{saved ? "Saved!" : "Save changes"}
</Button>
</CardFooter>
</Form>
</Card>
</div>
);
}
Contact Form
Name grid, email, topic select, priority radio group, and message textarea. On submit, the form is replaced with a success confirmation state.
"use client";
import { CheckCircle2Icon } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
export function Pattern() {
const [loading, setLoading] = useState(false);
const [submitted, setSubmitted] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await new Promise((r) => setTimeout(r, 1000));
setLoading(false);
setSubmitted(true);
};
if (submitted) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-4 py-8 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-success/10">
<CheckCircle2Icon className="size-6 text-success" />
</div>
<div className="space-y-1">
<p className="font-semibold">Message sent!</p>
<p className="text-muted-foreground text-sm">
We'll get back to you within 24 hours.
</p>
</div>
<Button onClick={() => setSubmitted(false)} size="sm" variant="outline">
Send another
</Button>
</div>
);
}
return (
<Form className="w-full max-w-sm gap-4" onSubmit={onSubmit}>
<div className="grid grid-cols-2 gap-4">
<Field name="firstName">
<FieldLabel>First name</FieldLabel>
<Input placeholder="Alex" required />
<FieldError />
</Field>
<Field name="lastName">
<FieldLabel>Last name</FieldLabel>
<Input placeholder="Rivera" required />
<FieldError />
</Field>
</div>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input placeholder="you@example.com" required type="email" />
<FieldError />
</Field>
<Field name="topic">
<FieldLabel>Topic</FieldLabel>
<Select defaultValue="general">
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value="general">General enquiry</SelectItem>
<SelectItem value="billing">Billing & payments</SelectItem>
<SelectItem value="technical">Technical support</SelectItem>
<SelectItem value="sales">Sales</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectPopup>
</Select>
<FieldError />
</Field>
<Field name="priority">
<FieldLabel>Priority</FieldLabel>
<RadioGroup className="flex flex-row gap-4" defaultValue="normal">
{["low", "normal", "high"].map((p) => (
<label
className="flex cursor-pointer items-center gap-1.5 text-sm capitalize"
key={p}
>
<Radio value={p} />
{p}
</label>
))}
</RadioGroup>
<FieldError />
</Field>
<Field name="message">
<FieldLabel>Message</FieldLabel>
<Textarea
placeholder="Describe your issue or question…"
required
rows={4}
/>
<FieldError />
</Field>
<Button className="w-full" loading={loading} type="submit">
Send message
</Button>
</Form>
);
}
Change Password
A card-wrapped form with current password, then new and confirm password fields separated by a divider. Client-side validation checks minimum length and that both new fields match.
"use client";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardPanel,
CardTitle,
} from "@/components/ui/card";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
type Errors = Record<string, string>;
export function Pattern() {
const [loading, setLoading] = useState(false);
const [errors, setErrors] = useState<Errors>({});
const [done, setDone] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
const newPassword = data.get("newPassword") as string;
const confirm = data.get("confirm") as string;
const next: Errors = {};
if (newPassword.length < 8)
next.newPassword = "Password must be at least 8 characters.";
if (newPassword !== confirm) next.confirm = "Passwords do not match.";
setErrors(next);
if (Object.keys(next).length > 0) return;
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
setDone(true);
setTimeout(() => setDone(false), 3000);
};
return (
<div className="w-full max-w-sm">
<Card>
<CardHeader className="border-b">
<CardTitle>Change password</CardTitle>
<CardDescription>
Choose a new password for your account.
</CardDescription>
</CardHeader>
<Form errors={errors} onSubmit={onSubmit}>
<CardPanel className="flex flex-col gap-4">
<Field name="current">
<FieldLabel>Current password</FieldLabel>
<Input
autoComplete="current-password"
placeholder="••••••••"
required
type="password"
/>
<FieldError />
</Field>
<Separator />
<Field name="newPassword">
<FieldLabel>New password</FieldLabel>
<Input
autoComplete="new-password"
placeholder="••••••••"
required
type="password"
/>
<FieldDescription>At least 8 characters.</FieldDescription>
<FieldError />
</Field>
<Field name="confirm">
<FieldLabel>Confirm new password</FieldLabel>
<Input
autoComplete="new-password"
placeholder="••••••••"
required
type="password"
/>
<FieldError />
</Field>
</CardPanel>
<Separator />
<CardFooter className="justify-end">
<Button loading={loading} type="submit">
{done ? "Password updated!" : "Update password"}
</Button>
</CardFooter>
</Form>
</Card>
</div>
);
}
Newsletter Signup
A centered card with a mail icon, first/last name grid, email, a "weekly digest" checkbox pre-checked, and a subscribe button. Confirming shows a success state with the submitted email address echoed back.
Stay in the loop
Get product updates, tips, and early access to new features.
"use client";
import { CheckCircle2Icon, MailIcon } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
const [loading, setLoading] = useState(false);
const [joined, setJoined] = useState(false);
const [email, setEmail] = useState("");
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
setEmail(data.get("email") as string);
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
setJoined(true);
};
if (joined) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-4 rounded-xl border bg-card px-6 py-8 text-center shadow-xs/5">
<div className="flex size-11 items-center justify-center rounded-full bg-success/10">
<CheckCircle2Icon className="size-5 text-success" />
</div>
<div className="space-y-1">
<p className="font-semibold">You're on the list!</p>
<p className="text-muted-foreground text-sm">
We'll send updates to{" "}
<span className="font-medium text-foreground">{email}</span>.
</p>
</div>
</div>
);
}
return (
<div className="w-full max-w-sm rounded-xl border bg-card px-6 py-8 shadow-xs/5">
<div className="mb-6 flex flex-col items-center gap-3 text-center">
<div className="flex size-10 items-center justify-center rounded-full bg-primary/10 text-primary">
<MailIcon className="size-5" />
</div>
<div className="space-y-1">
<p className="font-semibold">Stay in the loop</p>
<p className="text-muted-foreground text-sm">
Get product updates, tips, and early access to new features.
</p>
</div>
</div>
<Form className="gap-4" onSubmit={onSubmit}>
<div className="grid grid-cols-2 gap-3">
<Field name="firstName">
<FieldLabel className="sr-only">First name</FieldLabel>
<Input placeholder="First name" required />
<FieldError />
</Field>
<Field name="lastName">
<FieldLabel className="sr-only">Last name</FieldLabel>
<Input placeholder="Last name" />
<FieldError />
</Field>
</div>
<Field name="email">
<FieldLabel className="sr-only">Email address</FieldLabel>
<Input placeholder="you@example.com" required type="email" />
<FieldError />
</Field>
<Label className="flex items-start gap-2 font-normal text-muted-foreground text-xs">
<Checkbox className="mt-0.5" defaultChecked name="digest" />
Send me a weekly digest instead of individual emails
</Label>
<Button className="w-full" loading={loading} type="submit">
Subscribe
</Button>
<p className="text-center text-muted-foreground text-xs">
No spam, ever. Unsubscribe at any time.
</p>
</Form>
</div>
);
}
Multi-step Onboarding
A three-step wizard with a numbered step indicator: step 1 collects name and email, step 2 presents a plan radio group and newsletter preference, step 3 shows a read-only summary before final account creation.
"use client";
import { CheckCircle2Icon } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Radio, RadioGroup } from "@/components/ui/radio-group";
const STEPS = ["Account", "Plan", "Confirm"] as const;
type Step = 0 | 1 | 2;
const plans = [
{
description: "Free forever, up to 3 projects",
id: "hobby",
label: "Hobby",
},
{ description: "$12/mo — unlimited projects", id: "pro", label: "Pro" },
{ description: "$49/mo — collaboration tools", id: "team", label: "Team" },
];
export function Pattern() {
const [step, setStep] = useState<Step>(0);
const [loading, setLoading] = useState(false);
const [done, setDone] = useState(false);
const [data, setData] = useState({
email: "",
name: "",
newsletter: true,
plan: "hobby",
});
const handleAccount = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
setData((d) => ({
...d,
email: fd.get("email") as string,
name: fd.get("name") as string,
}));
setStep(1);
};
const handlePlan = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const fd = new FormData(e.currentTarget);
setData((d) => ({
...d,
newsletter: fd.get("newsletter") === "on",
plan: fd.get("plan") as string,
}));
setStep(2);
};
const handleConfirm = async () => {
setLoading(true);
await new Promise((r) => setTimeout(r, 1000));
setLoading(false);
setDone(true);
};
if (done) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-4 py-10 text-center">
<div className="flex size-12 items-center justify-center rounded-full bg-success/10">
<CheckCircle2Icon className="size-6 text-success" />
</div>
<div className="space-y-1">
<p className="font-semibold">Account created!</p>
<p className="text-muted-foreground text-sm">
Welcome, {data.name}. Check your inbox to verify your email.
</p>
</div>
</div>
);
}
return (
<div className="w-full max-w-sm space-y-6">
<div className="flex items-center gap-2">
{STEPS.map((label, i) => (
<div className="flex items-center gap-2" key={label}>
<div className="flex items-center gap-1.5">
<div
className={`flex size-6 items-center justify-center rounded-full font-semibold text-xs transition-colors ${
i < step
? "bg-primary text-primary-foreground"
: i === step
? "border-2 border-primary text-primary"
: "border border-border text-muted-foreground"
}`}
>
{i < step ? <CheckCircle2Icon className="size-3.5" /> : i + 1}
</div>
<span
className={`font-medium text-xs ${i === step ? "text-foreground" : "text-muted-foreground"}`}
>
{label}
</span>
</div>
{i < STEPS.length - 1 && (
<div
className={`h-px min-w-6 flex-1 ${i < step ? "bg-primary" : "bg-border"}`}
/>
)}
</div>
))}
</div>
{step === 0 && (
<Form className="gap-4" onSubmit={handleAccount}>
<Field name="name">
<FieldLabel>Full name</FieldLabel>
<Input
defaultValue={data.name}
placeholder="Alex Rivera"
required
/>
<FieldError />
</Field>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input
defaultValue={data.email}
placeholder="you@example.com"
required
type="email"
/>
<FieldError />
</Field>
<Button className="w-full" type="submit">
Continue
</Button>
</Form>
)}
{step === 1 && (
<Form className="gap-4" onSubmit={handlePlan}>
<Field name="plan">
<FieldLabel>Choose a plan</FieldLabel>
<RadioGroup className="gap-3" defaultValue={data.plan} name="plan">
{plans.map((p) => (
<label
className="flex cursor-pointer items-start gap-3 rounded-lg border p-3 transition-colors hover:bg-accent/50 has-[input[data-checked]]:border-primary/40 has-[input[data-checked]]:bg-accent/50"
key={p.id}
>
<Radio className="mt-0.5" value={p.id} />
<div className="flex flex-col gap-0.5">
<span className="font-medium text-sm">{p.label}</span>
<span className="text-muted-foreground text-xs">
{p.description}
</span>
</div>
</label>
))}
</RadioGroup>
</Field>
<Label className="flex items-center gap-2 font-normal text-sm">
<Checkbox defaultChecked={data.newsletter} name="newsletter" />
Send me product updates and tips
</Label>
<div className="flex gap-3">
<Button
className="flex-1"
onClick={() => setStep(0)}
type="button"
variant="outline"
>
Back
</Button>
<Button className="flex-1" type="submit">
Continue
</Button>
</div>
</Form>
)}
{step === 2 && (
<div className="space-y-4">
<div className="divide-y rounded-lg border">
{[
{ label: "Name", value: data.name },
{ label: "Email", value: data.email },
{
label: "Plan",
value:
plans.find((p) => p.id === data.plan)?.label ?? data.plan,
},
{ label: "Updates", value: data.newsletter ? "Yes" : "No" },
].map(({ label, value }) => (
<div
className="flex items-center justify-between px-4 py-3 text-sm"
key={label}
>
<span className="text-muted-foreground">{label}</span>
<span className="font-medium">{value}</span>
</div>
))}
</div>
<div className="flex gap-3">
<Button
className="flex-1"
onClick={() => setStep(1)}
type="button"
variant="outline"
>
Back
</Button>
<Button
className="flex-1"
loading={loading}
onClick={handleConfirm}
>
Create account
</Button>
</div>
</div>
)}
</div>
);
}
Sign Up
Email, password, and confirm-password fields with a terms checkbox. A minimal register form for new accounts.
Create an account
Fill in the details below to get started.
"use client";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function Pattern() {
const [loading, setLoading] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
};
return (
<div className="w-full max-w-sm space-y-5">
<div className="space-y-1">
<h2 className="font-semibold text-lg">Create an account</h2>
<p className="text-muted-foreground text-sm">
Fill in the details below to get started.
</p>
</div>
<Form className="gap-4" onSubmit={onSubmit}>
<Field name="email">
<FieldLabel>Email</FieldLabel>
<Input
autoComplete="email"
placeholder="you@example.com"
required
type="email"
/>
<FieldError />
</Field>
<Field name="password">
<FieldLabel>Password</FieldLabel>
<Input
autoComplete="new-password"
minLength={8}
placeholder="At least 8 characters"
required
type="password"
/>
<FieldError />
</Field>
<Field name="confirm">
<FieldLabel>Confirm password</FieldLabel>
<Input
autoComplete="new-password"
placeholder="Re-enter your password"
required
type="password"
/>
<FieldError />
</Field>
<Label className="flex items-start gap-2 font-normal text-sm">
<Checkbox className="mt-0.5" name="terms" required />
<span className="text-muted-foreground">
I agree to the{" "}
<a
className="text-foreground underline underline-offset-2"
href="#"
>
Terms of Service
</a>{" "}
and{" "}
<a
className="text-foreground underline underline-offset-2"
href="#"
>
Privacy Policy
</a>
</span>
</Label>
<Button className="w-full" loading={loading} type="submit">
Create account
</Button>
</Form>
</div>
);
}
Feedback
A rating select paired with a free-text textarea. On submit, the form is replaced with a thank-you confirmation.
"use client";
import { CheckCircle2Icon } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Textarea } from "@/components/ui/textarea";
export function Pattern() {
const [loading, setLoading] = useState(false);
const [done, setDone] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
setDone(true);
};
if (done) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-3 py-8 text-center">
<div className="flex size-11 items-center justify-center rounded-full bg-success/10">
<CheckCircle2Icon className="size-5 text-success" />
</div>
<div className="space-y-1">
<p className="font-semibold">Thanks for your feedback!</p>
<p className="text-muted-foreground text-sm">
We review every submission and use it to improve.
</p>
</div>
</div>
);
}
return (
<Form className="w-full max-w-sm gap-4" onSubmit={onSubmit}>
<div className="space-y-1">
<h2 className="font-semibold">Share your feedback</h2>
<p className="text-muted-foreground text-sm">
How are we doing? We read every response.
</p>
</div>
<Field name="rating">
<FieldLabel>Overall rating</FieldLabel>
<Select
defaultValue="good"
items={[
{ label: "⭐ Poor", value: "poor" },
{ label: "⭐⭐ Fair", value: "fair" },
{ label: "⭐⭐⭐ Good", value: "good" },
{ label: "⭐⭐⭐⭐ Very Good", value: "very-good" },
{ label: "⭐⭐⭐⭐⭐ Excellent", value: "excellent" },
]}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value="poor">⭐ Poor</SelectItem>
<SelectItem value="fair">⭐⭐ Fair</SelectItem>
<SelectItem value="good">⭐⭐⭐ Good</SelectItem>
<SelectItem value="very-good">⭐⭐⭐⭐ Very Good</SelectItem>
<SelectItem value="excellent">⭐⭐⭐⭐⭐ Excellent</SelectItem>
</SelectPopup>
</Select>
<FieldError />
</Field>
<Field name="comment">
<FieldLabel>What could we improve?</FieldLabel>
<Textarea
placeholder="Tell us what you liked or what we could do better…"
required
rows={4}
/>
<FieldError />
</Field>
<Button className="w-full" loading={loading} type="submit">
Submit feedback
</Button>
</Form>
);
}
Forgot Password
Email input that triggers a reset link. On success, the form swaps to a confirmation card echoing the submitted address with a resend option.
Forgot your password?
Enter your email and we'll send you a reset link.
"use client";
import { MailIcon } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
export function Pattern() {
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const [email, setEmail] = useState("");
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
const data = new FormData(e.currentTarget);
setEmail(data.get("email") as string);
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
setSent(true);
};
if (sent) {
return (
<div className="flex w-full max-w-sm flex-col items-center gap-4 rounded-xl border bg-card px-6 py-8 text-center">
<div className="flex size-11 items-center justify-center rounded-full bg-primary/10 text-primary">
<MailIcon className="size-5" />
</div>
<div className="space-y-1">
<p className="font-semibold">Check your inbox</p>
<p className="text-muted-foreground text-sm">
We sent a reset link to{" "}
<span className="font-medium text-foreground">{email}</span>.
</p>
</div>
<Button
onClick={() => setSent(false)}
size="sm"
type="button"
variant="outline"
>
Resend email
</Button>
</div>
);
}
return (
<div className="w-full max-w-sm space-y-5">
<div className="space-y-1">
<h2 className="font-semibold text-lg">Forgot your password?</h2>
<p className="text-muted-foreground text-sm">
Enter your email and we'll send you a reset link.
</p>
</div>
<Form className="gap-4" onSubmit={onSubmit}>
<Field name="email">
<FieldLabel>Email address</FieldLabel>
<Input
autoComplete="email"
placeholder="you@example.com"
required
type="email"
/>
<FieldError />
</Field>
<Button className="w-full" loading={loading} type="submit">
Send reset link
</Button>
<p className="text-center text-muted-foreground text-sm">
Remember your password?{" "}
<a className="text-foreground underline underline-offset-2" href="#">
Sign in
</a>
</p>
</Form>
</div>
);
}
Delete Account
A danger zone confirmation form with a highlighted warning, requiring the user to type a specific phrase before the destructive submit button becomes active.
Delete your account
This action is permanent and cannot be undone. All your data, projects, and settings will be deleted.
"use client";
import { TriangleAlertIcon } from "lucide-react";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const CONFIRM_PHRASE = "delete my account";
export function Pattern() {
const [value, setValue] = useState("");
const [loading, setLoading] = useState(false);
const isConfirmed = value.toLowerCase() === CONFIRM_PHRASE;
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!isConfirmed) return;
setLoading(true);
await new Promise((r) => setTimeout(r, 1200));
setLoading(false);
alert("Account deleted.");
};
return (
<div className="w-full max-w-sm space-y-4 rounded-xl border border-destructive/30 bg-destructive/5 p-5">
<div className="flex items-start gap-3">
<TriangleAlertIcon className="mt-0.5 size-5 shrink-0 text-destructive" />
<div className="space-y-1">
<p className="font-semibold text-sm">Delete your account</p>
<p className="text-muted-foreground text-xs">
This action is permanent and cannot be undone. All your data,
projects, and settings will be deleted.
</p>
</div>
</div>
<Form className="gap-3" onSubmit={onSubmit}>
<Field name="confirm">
<FieldLabel className="text-sm">
Type{" "}
<code className="rounded bg-muted px-1 py-0.5 font-mono text-xs">
{CONFIRM_PHRASE}
</code>{" "}
to confirm
</FieldLabel>
<Input
onChange={(e) => setValue(e.target.value)}
placeholder={CONFIRM_PHRASE}
type="text"
value={value}
/>
<FieldDescription className="text-xs">
This cannot be undone. Please be certain.
</FieldDescription>
</Field>
<Button
className="w-full"
disabled={!isConfirmed}
loading={loading}
type="submit"
variant="destructive"
>
Permanently delete account
</Button>
</Form>
</div>
);
}
Invite Team Member
Email address and role select for adding a workspace collaborator, with a role description and cancel/invite action buttons.
"use client";
import { type FormEvent, useState } from "react";
import { Button } from "@/components/ui/button";
import {
Field,
FieldDescription,
FieldError,
FieldLabel,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import {
Select,
SelectItem,
SelectPopup,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
export function Pattern() {
const [loading, setLoading] = useState(false);
const [sent, setSent] = useState(false);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
await new Promise((r) => setTimeout(r, 900));
setLoading(false);
setSent(true);
setTimeout(() => setSent(false), 3000);
};
return (
<Form className="w-full max-w-sm gap-4" onSubmit={onSubmit}>
<div className="space-y-1">
<h2 className="font-semibold">Invite team member</h2>
<p className="text-muted-foreground text-sm">
Send an invitation to join your workspace.
</p>
</div>
<Field name="email">
<FieldLabel>
Email address <span className="text-destructive-foreground">*</span>
</FieldLabel>
<Input
autoComplete="email"
placeholder="colleague@company.com"
required
type="email"
/>
<FieldError />
</Field>
<Field name="role">
<FieldLabel>Role</FieldLabel>
<Select
defaultValue="member"
items={[
{ label: "Admin", value: "admin" },
{ label: "Member", value: "member" },
{ label: "Viewer", value: "viewer" },
]}
>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectPopup>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="member">Member</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectPopup>
</Select>
<FieldDescription>
Admins can manage members and billing.
</FieldDescription>
</Field>
<div className="flex gap-3">
<Button className="flex-1" type="reset" variant="outline">
Cancel
</Button>
<Button className="flex-1" loading={loading} type="submit">
{sent ? "Invitation sent!" : "Send invitation"}
</Button>
</div>
</Form>
);
}

