Form
Handles data entry with validation and submission using react-hook-form.
Usage
import {
Form,
FormField,
FormItem,
FormLabel,
FormControl,
FormMessage,
} from '@acronis-platform/shadcn-uikit';Basic
A simple form with a single field and validation.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
const formSchema = z.object({
username: z.string().min(2, {
message: 'Username must be at least 2 characters.',
}),
});
export function FormBasic() {
const form = useForm<z.infer<typeof formSchema>>({
// Cast is required because apps/demos uses zod v3 schemas while @hookform/resolvers is typed against zod v4.
resolver: zodResolver(formSchema as never),
defaultValues: {
username: '',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-md">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="Enter username" {...field} />
</FormControl>
<FormDescription>
This is your public display name.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Submit</Button>
</form>
</Form>
</div>
);
}
Login
A login form with email and password fields.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import { Checkbox } from '@acronis-platform/shadcn-uikit/react';
const formSchema = z.object({
email: z.string().email({
message: 'Please enter a valid email address.',
}),
password: z.string().min(8, {
message: 'Password must be at least 8 characters.',
}),
remember: z.boolean(),
});
export function FormLogin() {
const form = useForm<z.infer<typeof formSchema>>({
// TO-DO.md #3: apps/demos zod 3 schemas vs zod 4 types in @hookform/resolvers — cast bypasses the false-positive
resolver: zodResolver(formSchema as never),
defaultValues: {
email: '',
password: '',
remember: false,
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-md rounded-lg border p-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="email@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">Remember me</FormLabel>
</div>
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Sign in
</Button>
</form>
</Form>
</div>
);
}
Profile
A profile editing form with multiple field types.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
const formSchema = z.object({
username: z
.string()
.min(2, {
message: 'Username must be at least 2 characters.',
})
.max(30, {
message: 'Username must not be longer than 30 characters.',
}),
email: z.string().email({
message: 'Please enter a valid email address.',
}),
bio: z
.string()
.max(160, {
message: 'Bio must not be longer than 160 characters.',
})
.optional(),
});
export function FormProfile() {
const form = useForm<z.infer<typeof formSchema>>({
// TO-DO.md #3: apps/demos zod 3 schemas vs zod 4 types in @hookform/resolvers — cast bypasses the false-positive
resolver: zodResolver(formSchema as never),
defaultValues: {
username: '',
email: '',
bio: '',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-md">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>
This is your public display name. It can be your real name or
a pseudonym.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="john@example.com"
{...field}
/>
</FormControl>
<FormDescription>
We'll never share your email with anyone else.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="Tell us a little bit about yourself"
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>
You can write up to 160 characters.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update profile</Button>
</form>
</Form>
</div>
);
}
Registration
A user registration form with validation rules.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import { Checkbox } from '@acronis-platform/shadcn-uikit/react';
const formSchema = z
.object({
firstName: z.string().min(2, {
message: 'First name must be at least 2 characters.',
}),
lastName: z.string().min(2, {
message: 'Last name must be at least 2 characters.',
}),
email: z.string().email({
message: 'Please enter a valid email address.',
}),
password: z.string().min(8, {
message: 'Password must be at least 8 characters.',
}),
confirmPassword: z.string(),
terms: z.boolean().refine((val) => val === true, {
message: 'You must accept the terms and conditions.',
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
export function FormRegistration() {
const form = useForm<z.infer<typeof formSchema>>({
// TO-DO.md #3: apps/demos zod 3 schemas vs zod 4 types in @hookform/resolvers — cast bypasses the false-positive
resolver: zodResolver(formSchema as never),
defaultValues: {
firstName: '',
lastName: '',
email: '',
password: '',
confirmPassword: '',
terms: false,
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-md rounded-lg border p-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First Name</FormLabel>
<FormControl>
<Input placeholder="John" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last Name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="john@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Enter password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Confirm Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="Confirm password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="terms"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
I accept the terms and conditions
</FormLabel>
<FormMessage />
</div>
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Create account
</Button>
</form>
</Form>
</div>
);
}
Contact
A contact form with message textarea.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@acronis-platform/shadcn-uikit/react';
const formSchema = z.object({
name: z.string().min(2, {
message: 'Name must be at least 2 characters.',
}),
email: z.string().email({
message: 'Please enter a valid email address.',
}),
subject: z.string().min(5, {
message: 'Subject must be at least 5 characters.',
}),
message: z.string().min(10, {
message: 'Message must be at least 10 characters.',
}),
priority: z.enum(['low', 'medium', 'high'], {
message: 'Please select a priority level.',
}),
});
export function FormContact() {
const form = useForm<z.infer<typeof formSchema>>({
// TO-DO.md #3: apps/demos zod 3 schemas vs zod 4 types in @hookform/resolvers — cast bypasses the false-positive
resolver: zodResolver(formSchema as never),
defaultValues: {
name: '',
email: '',
subject: '',
message: '',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-md">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="your@email.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Priority</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select priority level" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subject"
render={({ field }) => (
<FormItem>
<FormLabel>Subject</FormLabel>
<FormControl>
<Input placeholder="What is this about?" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Message</FormLabel>
<FormControl>
<Textarea
placeholder="Your message here..."
className="min-h-[120px] resize-none"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Send message</Button>
</form>
</Form>
</div>
);
}
Settings
A settings form with switches and toggles.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import { Checkbox } from '@acronis-platform/shadcn-uikit/react';
import {
RadioGroup,
RadioGroupItem,
} from '@acronis-platform/shadcn-uikit/react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@acronis-platform/shadcn-uikit/react';
const formSchema = z.object({
notifications: z.boolean(),
marketing: z.boolean(),
security: z.boolean(),
language: z.string({
message: 'Please select a language.',
}),
theme: z.enum(['light', 'dark', 'system'], {
message: 'Please select a theme.',
}),
});
export function FormSettings() {
const form = useForm<z.infer<typeof formSchema>>({
// TO-DO.md #3: apps/demos zod 3 schemas vs zod 4 types in @hookform/resolvers — cast bypasses the false-positive
resolver: zodResolver(formSchema as never),
defaultValues: {
notifications: true,
marketing: false,
security: true,
theme: 'system',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
console.log(values);
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-md rounded-lg border p-6">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<div className="space-y-4">
<div>
<h4 className="text-sm font-medium mb-3">Email Notifications</h4>
<FormField
control={form.control}
name="notifications"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
Receive email notifications
</FormLabel>
<FormDescription>
Get notified about important updates
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="marketing"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
Marketing emails
</FormLabel>
<FormDescription>
Receive emails about new products and features
</FormDescription>
</div>
</FormItem>
)}
/>
<FormField
control={form.control}
name="security"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel className="font-normal">
Security alerts
</FormLabel>
<FormDescription>
Get notified about security updates
</FormDescription>
</div>
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="language"
render={({ field }) => (
<FormItem>
<FormLabel>Language</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a language" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="en">English</SelectItem>
<SelectItem value="es">Spanish</SelectItem>
<SelectItem value="fr">French</SelectItem>
<SelectItem value="de">German</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="theme"
render={({ field }) => (
<FormItem className="space-y-3">
<FormLabel>Theme</FormLabel>
<FormControl>
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
className="flex flex-col space-y-1"
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="light" />
</FormControl>
<FormLabel className="font-normal">Light</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="dark" />
</FormControl>
<FormLabel className="font-normal">Dark</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="system" />
</FormControl>
<FormLabel className="font-normal">System</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Save settings
</Button>
</form>
</Form>
</div>
);
}
Layout: Two-column grid
Side-by-side pairs for related fields.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@acronis-platform/shadcn-uikit/react';
const formSchema = z.object({
firstName: z.string().min(1, 'First name is required.'),
lastName: z.string().min(1, 'Last name is required.'),
email: z.string().email('Please enter a valid email address.'),
phone: z.string().optional(),
country: z.string().min(1, 'Please select a country.'),
city: z.string().optional(),
});
export function FormLayoutTwoColumn() {
const form = useForm<z.infer<typeof formSchema>>({
// TO-DO.md #3: apps/demos zod 3 schemas vs zod 4 types in @hookform/resolvers — cast bypasses the false-positive
resolver: zodResolver(formSchema as never),
defaultValues: {
firstName: '',
lastName: '',
email: '',
phone: '',
country: '',
city: '',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-2xl">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
{/* Two-column row */}
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>First name</FormLabel>
<FormControl>
<Input placeholder="Jane" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>Last name</FormLabel>
<FormControl>
<Input placeholder="Doe" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Two-column row */}
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="jane@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>
Phone{' '}
<span className="text-muted-foreground font-normal">
(optional)
</span>
</FormLabel>
<FormControl>
<Input
type="tel"
placeholder="+1 555 000 0000"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{/* Two-column row */}
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="country"
render={({ field }) => (
<FormItem>
<FormLabel>Country</FormLabel>
<Select
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select country" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="us">United States</SelectItem>
<SelectItem value="gb">United Kingdom</SelectItem>
<SelectItem value="de">Germany</SelectItem>
<SelectItem value="fr">France</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>
City{' '}
<span className="text-muted-foreground font-normal">
(optional)
</span>
</FormLabel>
<FormControl>
<Input placeholder="San Francisco" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit">Save</Button>
</form>
</Form>
</div>
);
}
Layout: Sectioned form
Long forms split into labelled sections with dividers.
View sourceHide source
import * as React from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import * as z from 'zod';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
import { Switch } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import { Separator } from '@acronis-platform/shadcn-uikit/react';
const formSchema = z.object({
displayName: z.string().min(2, 'Display name must be at least 2 characters.'),
email: z.string().email('Please enter a valid email address.'),
bio: z.string().max(160, 'Bio must not exceed 160 characters.').optional(),
emailNotifications: z.boolean(),
marketingEmails: z.boolean(),
currentPassword: z.string().optional(),
newPassword: z
.string()
.min(8, 'Password must be at least 8 characters.')
.optional(),
});
export function FormLayoutSections() {
const form = useForm<z.infer<typeof formSchema>>({
// TO-DO.md #3: apps/demos zod 3 schemas vs zod 4 types in @hookform/resolvers — cast bypasses the false-positive
resolver: zodResolver(formSchema as never),
defaultValues: {
displayName: '',
email: '',
bio: '',
emailNotifications: true,
marketingEmails: false,
currentPassword: '',
newPassword: '',
},
});
function onSubmit(values: z.infer<typeof formSchema>) {
alert(JSON.stringify(values, null, 2));
}
return (
<div className="w-full max-w-lg">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
{/* Section: Profile */}
<div className="space-y-4">
<div>
<h3 className="text-base font-semibold">Profile</h3>
<p className="text-sm text-muted-foreground">
Update your public profile information.
</p>
</div>
<FormField
control={form.control}
name="displayName"
render={({ field }) => (
<FormItem>
<FormLabel>Display name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input
type="email"
placeholder="you@example.com"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="bio"
render={({ field }) => (
<FormItem>
<FormLabel>Bio</FormLabel>
<FormControl>
<Textarea
placeholder="A short bio about yourself"
className="resize-none"
{...field}
/>
</FormControl>
<FormDescription>Up to 160 characters.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<Separator />
{/* Section: Notifications */}
<div className="space-y-4">
<div>
<h3 className="text-base font-semibold">Notifications</h3>
<p className="text-sm text-muted-foreground">
Choose how you want to be notified.
</p>
</div>
<FormField
control={form.control}
name="emailNotifications"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-sm font-medium">
Email notifications
</FormLabel>
<FormDescription>
Receive notifications about account activity.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormField
control={form.control}
name="marketingEmails"
render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-4">
<div className="space-y-0.5">
<FormLabel className="text-sm font-medium">
Marketing emails
</FormLabel>
<FormDescription>
Get updates on new features and promotions.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<Separator />
{/* Section: Security */}
<div className="space-y-4">
<div>
<h3 className="text-base font-semibold">Security</h3>
<p className="text-sm text-muted-foreground">
Change your password. Leave blank to keep the current one.
</p>
</div>
<FormField
control={form.control}
name="currentPassword"
render={({ field }) => (
<FormItem>
<FormLabel>Current password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>New password</FormLabel>
<FormControl>
<Input type="password" placeholder="••••••••" {...field} />
</FormControl>
<FormDescription>
Must be at least 8 characters.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
<Button type="submit">Save changes</Button>
</form>
</Form>
</div>
);
}
TanStack Form — Basic
The lower-level FormItem primitives work with any form library. This example uses @tanstack/react-form with Zod validation.
View sourceHide source
import * as React from 'react';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';
import {
Field,
FieldLabel,
FieldDescription,
FieldError,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
const usernameSchema = z
.string()
.min(2, 'Username must be at least 2 characters.');
export function FormTanstackBasic() {
const form = useForm({
defaultValues: { username: '' },
onSubmit: async ({ value }) => {
alert(JSON.stringify(value, null, 2));
},
});
return (
<div className="w-full max-w-md">
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<form.Field
name="username"
validators={{
onChange: ({ value }) => {
const result = usernameSchema.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field
data-invalid={
field.state.meta.isTouched && field.state.meta.errors.length > 0
? 'true'
: undefined
}
>
<FieldLabel htmlFor={field.name}>Username</FieldLabel>
<Input
id={field.name}
placeholder="Enter username"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
aria-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
}
/>
<FieldDescription>
This is your public display name.
</FieldDescription>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Subscribe
selector={(state) => [state.canSubmit, state.isSubmitting]}
>
{([canSubmit, isSubmitting]) => (
<Button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? 'Submitting…' : 'Submit'}
</Button>
)}
</form.Subscribe>
</form>
</div>
);
}
TanStack Form — Profile
Multiple field types (input, select, textarea) wired with TanStack Form.
View sourceHide source
import * as React from 'react';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';
import {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@acronis-platform/shadcn-uikit/react';
const profileSchema = z.object({
username: z
.string()
.min(2, 'At least 2 characters.')
.max(30, 'Max 30 characters.'),
email: z.string().email('Enter a valid email address.'),
role: z.string().min(1, 'Please select a role.'),
bio: z.string().max(160, 'Max 160 characters.').optional(),
});
type ProfileValues = z.infer<typeof profileSchema>;
export function FormTanstackProfile() {
const [submitted, setSubmitted] = React.useState<ProfileValues | null>(null);
const form = useForm({
defaultValues: { username: '', email: '', role: '', bio: '' },
onSubmit: async ({ value }) => setSubmitted(value as ProfileValues),
});
if (submitted) {
return (
<div className="w-full max-w-md rounded-lg border p-6 space-y-3">
<p className="text-sm font-medium text-green-600">Profile saved!</p>
<pre className="text-xs bg-muted rounded p-3 overflow-auto">
{JSON.stringify(submitted, null, 2)}
</pre>
<Button variant="outline" size="sm" onClick={() => setSubmitted(null)}>
Edit again
</Button>
</div>
);
}
return (
<div className="w-full max-w-md">
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
>
<FieldGroup>
<form.Field
name="username"
validators={{
onChange: ({ value }) => {
const result = profileSchema.shape.username.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field
data-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
? 'true'
: undefined
}
>
<FieldLabel htmlFor={field.name}>Username</FieldLabel>
<Input
id={field.name}
placeholder="johndoe"
value={field.state.value as string}
onChange={(e) => field.handleChange(e.target.value as never)}
onBlur={field.handleBlur}
aria-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
}
/>
<FieldDescription>Your public display name.</FieldDescription>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Field
name="email"
validators={{
onChange: ({ value }) => {
const result = profileSchema.shape.email.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field
data-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
? 'true'
: undefined
}
>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input
id={field.name}
type="email"
placeholder="jane@example.com"
value={field.state.value as never}
onChange={(e) => field.handleChange(e.target.value as never)}
onBlur={field.handleBlur}
aria-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
}
/>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Field
name="role"
validators={{
onChange: ({ value }) => {
const result = profileSchema.shape.role.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field
data-invalid={
field.state.meta.isTouched &&
field.state.meta.errors.length > 0
? 'true'
: undefined
}
>
<FieldLabel htmlFor={field.name}>Role</FieldLabel>
<Select
value={field.state.value}
onValueChange={(val) =>
field.handleChange((val ?? '') as never)
}
>
<SelectTrigger id={field.name} onBlur={field.handleBlur}>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="admin">Admin</SelectItem>
<SelectItem value="editor">Editor</SelectItem>
<SelectItem value="viewer">Viewer</SelectItem>
</SelectContent>
</Select>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Field
name="bio"
validators={{
onChange: ({ value }) => {
const result = profileSchema.shape.bio.safeParse(value);
return result.success
? undefined
: result.error.issues[0]?.message;
},
}}
>
{(field) => (
<Field>
<FieldLabel htmlFor={field.name}>Bio</FieldLabel>
<Textarea
id={field.name}
placeholder="Tell us a little about yourself"
className="resize-none"
value={field.state.value as never}
onChange={(e) => field.handleChange(e.target.value as never)}
onBlur={field.handleBlur}
/>
<FieldDescription>Up to 160 characters.</FieldDescription>
{field.state.meta.isTouched && (
<FieldError
errors={field.state.meta.errors.map((e) => ({
message: e?.toString(),
}))}
/>
)}
</Field>
)}
</form.Field>
<form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
{([canSubmit, isSubmitting]) => (
<Button type="submit" disabled={!canSubmit || isSubmitting}>
{isSubmitting ? 'Saving…' : 'Save profile'}
</Button>
)}
</form.Subscribe>
</FieldGroup>
</form>
</div>
);
}
For a full guide covering layout patterns, component reference, validation strategies, and accessibility, see the Forms Guide.
API Reference
The Form component is a re-export of react-hook-form's FormProvider. FormField accepts ControllerProps from react-hook-form. Sub-components FormItem, FormLabel, FormControl, FormDescription, and FormMessage extend their native HTML element props and connect to form context via the useFormField hook.