or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

core-system.mddata-display.mddate-time.mdfeedback.mdforms.mdindex.mdinputs.mdinteractions.mdlayout.mdnavigation.mdoverlays.mdutilities.md

forms.mddocs/

0

# Form Integration

1

2

NextUI provides comprehensive form handling capabilities with the Form component and seamless integration patterns for validation, data collection, and form state management.

3

4

## Capabilities

5

6

### Form Component

7

8

A form container component that provides validation context, error handling, and integration with form libraries for building robust form interfaces.

9

10

```typescript { .api }

11

interface FormProps {

12

/** Form content and input elements */

13

children?: React.ReactNode;

14

/** Server-side or external validation errors */

15

validationErrors?: ValidationErrors;

16

/** Validation behavior mode */

17

validationBehavior?: "aria" | "native";

18

/** Custom CSS class */

19

className?: string;

20

/** Form reset handler */

21

onReset?: () => void;

22

/** Form submission handler */

23

onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void;

24

/** Invalid submission handler */

25

onInvalidSubmit?: (errors: ValidationErrors) => void;

26

}

27

28

interface ValidationErrors {

29

[fieldName: string]: ValidationError;

30

}

31

32

type ValidationError = string | string[];

33

34

function Form(props: FormProps): JSX.Element;

35

```

36

37

**Basic Form Usage:**

38

39

```typescript

40

import {

41

Form, Input, Button, Checkbox, Select, SelectItem,

42

Card, CardHeader, CardBody, CardFooter

43

} from "@nextui-org/react";

44

45

function BasicFormExample() {

46

const [formData, setFormData] = useState({

47

name: "",

48

email: "",

49

password: "",

50

confirmPassword: "",

51

terms: false,

52

country: "",

53

});

54

55

const [errors, setErrors] = useState<ValidationErrors>({});

56

57

const validateForm = () => {

58

const newErrors: ValidationErrors = {};

59

60

if (!formData.name.trim()) {

61

newErrors.name = "Name is required";

62

}

63

64

if (!formData.email.trim()) {

65

newErrors.email = "Email is required";

66

} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) {

67

newErrors.email = "Please enter a valid email";

68

}

69

70

if (!formData.password) {

71

newErrors.password = "Password is required";

72

} else if (formData.password.length < 8) {

73

newErrors.password = "Password must be at least 8 characters";

74

}

75

76

if (formData.password !== formData.confirmPassword) {

77

newErrors.confirmPassword = "Passwords do not match";

78

}

79

80

if (!formData.terms) {

81

newErrors.terms = "You must accept the terms and conditions";

82

}

83

84

return newErrors;

85

};

86

87

const handleSubmit = (e: React.FormEvent) => {

88

e.preventDefault();

89

const validationErrors = validateForm();

90

setErrors(validationErrors);

91

92

if (Object.keys(validationErrors).length === 0) {

93

console.log("Form submitted successfully:", formData);

94

// Handle successful submission

95

}

96

};

97

98

const updateField = (field: string) => (value: string | boolean) => {

99

setFormData(prev => ({ ...prev, [field]: value }));

100

// Clear error when user starts typing

101

if (errors[field]) {

102

setErrors(prev => ({ ...prev, [field]: undefined }));

103

}

104

};

105

106

return (

107

<Card className="max-w-md mx-auto">

108

<CardHeader>

109

<h2 className="text-xl font-bold">Create Account</h2>

110

</CardHeader>

111

<Form onSubmit={handleSubmit} validationErrors={errors}>

112

<CardBody className="space-y-4">

113

<Input

114

label="Full Name"

115

placeholder="Enter your full name"

116

value={formData.name}

117

onValueChange={updateField("name")}

118

isRequired

119

isInvalid={!!errors.name}

120

errorMessage={errors.name}

121

/>

122

123

<Input

124

type="email"

125

label="Email"

126

placeholder="Enter your email"

127

value={formData.email}

128

onValueChange={updateField("email")}

129

isRequired

130

isInvalid={!!errors.email}

131

errorMessage={errors.email}

132

/>

133

134

<Input

135

type="password"

136

label="Password"

137

placeholder="Enter your password"

138

value={formData.password}

139

onValueChange={updateField("password")}

140

isRequired

141

isInvalid={!!errors.password}

142

errorMessage={errors.password}

143

/>

144

145

<Input

146

type="password"

147

label="Confirm Password"

148

placeholder="Confirm your password"

149

value={formData.confirmPassword}

150

onValueChange={updateField("confirmPassword")}

151

isRequired

152

isInvalid={!!errors.confirmPassword}

153

errorMessage={errors.confirmPassword}

154

/>

155

156

<Select

157

label="Country"

158

placeholder="Select your country"

159

selectedKeys={formData.country ? [formData.country] : []}

160

onSelectionChange={(keys) => {

161

const selected = Array.from(keys)[0] as string;

162

updateField("country")(selected);

163

}}

164

isRequired

165

>

166

<SelectItem key="us">United States</SelectItem>

167

<SelectItem key="ca">Canada</SelectItem>

168

<SelectItem key="uk">United Kingdom</SelectItem>

169

<SelectItem key="de">Germany</SelectItem>

170

<SelectItem key="fr">France</SelectItem>

171

</Select>

172

173

<Checkbox

174

isSelected={formData.terms}

175

onValueChange={updateField("terms")}

176

isInvalid={!!errors.terms}

177

color={errors.terms ? "danger" : "primary"}

178

>

179

I agree to the{" "}

180

<a href="#" className="text-primary hover:underline">

181

terms and conditions

182

</a>

183

</Checkbox>

184

185

{errors.terms && (

186

<p className="text-danger text-sm mt-1">{errors.terms}</p>

187

)}

188

</CardBody>

189

<CardFooter>

190

<Button

191

type="submit"

192

color="primary"

193

className="w-full"

194

size="lg"

195

>

196

Create Account

197

</Button>

198

</CardFooter>

199

</Form>

200

</Card>

201

);

202

}

203

```

204

205

### Form Context

206

207

Context system for sharing form state and validation across form components.

208

209

```typescript { .api }

210

interface FormContext {

211

/** Current validation errors */

212

validationErrors?: ValidationErrors;

213

/** Validation behavior mode */

214

validationBehavior?: "aria" | "native";

215

/** Form submission state */

216

isSubmitting?: boolean;

217

/** Form dirty state */

218

isDirty?: boolean;

219

/** Form valid state */

220

isValid?: boolean;

221

/** Reset form */

222

reset?: () => void;

223

/** Update field validation */

224

updateValidation?: (fieldName: string, error: ValidationError | null) => void;

225

}

226

227

/**

228

* Hook to access form context

229

*/

230

function useSlottedContext<T>(context: React.Context<T>): T | undefined;

231

```

232

233

### React Hook Form Integration

234

235

NextUI components integrate seamlessly with React Hook Form for advanced form handling.

236

237

```typescript

238

import { useForm, Controller, SubmitHandler } from "react-hook-form";

239

import { zodResolver } from "@hookform/resolvers/zod";

240

import { z } from "zod";

241

import {

242

Form, Input, Button, Select, SelectItem, Checkbox,

243

Card, CardHeader, CardBody, CardFooter

244

} from "@nextui-org/react";

245

246

// Validation schema

247

const schema = z.object({

248

firstName: z.string().min(1, "First name is required"),

249

lastName: z.string().min(1, "Last name is required"),

250

email: z.string().email("Please enter a valid email"),

251

age: z.number().min(18, "Must be at least 18 years old"),

252

country: z.string().min(1, "Please select a country"),

253

newsletter: z.boolean(),

254

bio: z.string().max(500, "Bio must be less than 500 characters").optional(),

255

});

256

257

type FormData = z.infer<typeof schema>;

258

259

function ReactHookFormExample() {

260

const {

261

control,

262

handleSubmit,

263

reset,

264

formState: { errors, isSubmitting, isDirty, isValid }

265

} = useForm<FormData>({

266

resolver: zodResolver(schema),

267

defaultValues: {

268

firstName: "",

269

lastName: "",

270

email: "",

271

age: 18,

272

country: "",

273

newsletter: false,

274

bio: "",

275

},

276

mode: "onChange", // Validate on change

277

});

278

279

const onSubmit: SubmitHandler<FormData> = async (data) => {

280

try {

281

// Simulate API call

282

await new Promise(resolve => setTimeout(resolve, 1000));

283

console.log("Form submitted:", data);

284

reset();

285

} catch (error) {

286

console.error("Submission error:", error);

287

}

288

};

289

290

return (

291

<Card className="max-w-2xl mx-auto">

292

<CardHeader>

293

<h2 className="text-xl font-bold">User Profile</h2>

294

</CardHeader>

295

<form onSubmit={handleSubmit(onSubmit)}>

296

<CardBody className="space-y-4">

297

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">

298

<Controller

299

name="firstName"

300

control={control}

301

render={({ field, fieldState }) => (

302

<Input

303

{...field}

304

label="First Name"

305

placeholder="Enter your first name"

306

isInvalid={fieldState.invalid}

307

errorMessage={fieldState.error?.message}

308

/>

309

)}

310

/>

311

312

<Controller

313

name="lastName"

314

control={control}

315

render={({ field, fieldState }) => (

316

<Input

317

{...field}

318

label="Last Name"

319

placeholder="Enter your last name"

320

isInvalid={fieldState.invalid}

321

errorMessage={fieldState.error?.message}

322

/>

323

)}

324

/>

325

</div>

326

327

<Controller

328

name="email"

329

control={control}

330

render={({ field, fieldState }) => (

331

<Input

332

{...field}

333

type="email"

334

label="Email"

335

placeholder="Enter your email"

336

isInvalid={fieldState.invalid}

337

errorMessage={fieldState.error?.message}

338

/>

339

)}

340

/>

341

342

<Controller

343

name="age"

344

control={control}

345

render={({ field, fieldState }) => (

346

<Input

347

{...field}

348

type="number"

349

label="Age"

350

placeholder="Enter your age"

351

value={field.value?.toString() || ""}

352

onChange={(e) => field.onChange(parseInt(e.target.value) || 0)}

353

isInvalid={fieldState.invalid}

354

errorMessage={fieldState.error?.message}

355

/>

356

)}

357

/>

358

359

<Controller

360

name="country"

361

control={control}

362

render={({ field, fieldState }) => (

363

<Select

364

{...field}

365

label="Country"

366

placeholder="Select your country"

367

selectedKeys={field.value ? [field.value] : []}

368

onSelectionChange={(keys) => {

369

const selected = Array.from(keys)[0] as string;

370

field.onChange(selected);

371

}}

372

isInvalid={fieldState.invalid}

373

errorMessage={fieldState.error?.message}

374

>

375

<SelectItem key="us">United States</SelectItem>

376

<SelectItem key="ca">Canada</SelectItem>

377

<SelectItem key="uk">United Kingdom</SelectItem>

378

<SelectItem key="de">Germany</SelectItem>

379

<SelectItem key="fr">France</SelectItem>

380

</Select>

381

)}

382

/>

383

384

<Controller

385

name="bio"

386

control={control}

387

render={({ field, fieldState }) => (

388

<Textarea

389

{...field}

390

label="Bio"

391

placeholder="Tell us about yourself (optional)"

392

maxRows={3}

393

isInvalid={fieldState.invalid}

394

errorMessage={fieldState.error?.message}

395

/>

396

)}

397

/>

398

399

<Controller

400

name="newsletter"

401

control={control}

402

render={({ field }) => (

403

<Checkbox

404

isSelected={field.value}

405

onValueChange={field.onChange}

406

color="primary"

407

>

408

Subscribe to newsletter

409

</Checkbox>

410

)}

411

/>

412

</CardBody>

413

<CardFooter className="flex gap-2">

414

<Button

415

type="button"

416

variant="flat"

417

onPress={() => reset()}

418

isDisabled={!isDirty}

419

>

420

Reset

421

</Button>

422

<Button

423

type="submit"

424

color="primary"

425

isLoading={isSubmitting}

426

isDisabled={!isValid}

427

className="flex-1"

428

>

429

{isSubmitting ? "Saving..." : "Save Profile"}

430

</Button>

431

</CardFooter>

432

</form>

433

</Card>

434

);

435

}

436

```

437

438

### Formik Integration

439

440

NextUI components also work seamlessly with Formik for form state management.

441

442

```typescript

443

import { Formik, Form as FormikForm, Field } from "formik";

444

import * as Yup from "yup";

445

import {

446

Input, Button, Select, SelectItem, Textarea,

447

Card, CardHeader, CardBody, CardFooter

448

} from "@nextui-org/react";

449

450

// Validation schema

451

const validationSchema = Yup.object({

452

name: Yup.string().required("Name is required"),

453

email: Yup.string().email("Invalid email").required("Email is required"),

454

message: Yup.string()

455

.min(10, "Message must be at least 10 characters")

456

.required("Message is required"),

457

priority: Yup.string().required("Please select a priority"),

458

});

459

460

interface ContactFormValues {

461

name: string;

462

email: string;

463

message: string;

464

priority: string;

465

}

466

467

function FormikExample() {

468

const initialValues: ContactFormValues = {

469

name: "",

470

email: "",

471

message: "",

472

priority: "",

473

};

474

475

const handleSubmit = async (

476

values: ContactFormValues,

477

{ setSubmitting, resetForm }: any

478

) => {

479

try {

480

// Simulate API call

481

await new Promise(resolve => setTimeout(resolve, 1000));

482

console.log("Contact form submitted:", values);

483

resetForm();

484

} catch (error) {

485

console.error("Submission error:", error);

486

} finally {

487

setSubmitting(false);

488

}

489

};

490

491

return (

492

<Card className="max-w-lg mx-auto">

493

<CardHeader>

494

<h2 className="text-xl font-bold">Contact Us</h2>

495

</CardHeader>

496

<Formik

497

initialValues={initialValues}

498

validationSchema={validationSchema}

499

onSubmit={handleSubmit}

500

>

501

{({ isSubmitting, errors, touched, setFieldValue, values }) => (

502

<FormikForm>

503

<CardBody className="space-y-4">

504

<Field name="name">

505

{({ field, meta }: any) => (

506

<Input

507

{...field}

508

label="Name"

509

placeholder="Enter your name"

510

isInvalid={!!(meta.touched && meta.error)}

511

errorMessage={meta.touched && meta.error}

512

/>

513

)}

514

</Field>

515

516

<Field name="email">

517

{({ field, meta }: any) => (

518

<Input

519

{...field}

520

type="email"

521

label="Email"

522

placeholder="Enter your email"

523

isInvalid={!!(meta.touched && meta.error)}

524

errorMessage={meta.touched && meta.error}

525

/>

526

)}

527

</Field>

528

529

<Field name="priority">

530

{({ meta }: any) => (

531

<Select

532

label="Priority"

533

placeholder="Select priority level"

534

selectedKeys={values.priority ? [values.priority] : []}

535

onSelectionChange={(keys) => {

536

const selected = Array.from(keys)[0] as string;

537

setFieldValue("priority", selected);

538

}}

539

isInvalid={!!(meta.touched && meta.error)}

540

errorMessage={meta.touched && meta.error}

541

>

542

<SelectItem key="low">Low</SelectItem>

543

<SelectItem key="medium">Medium</SelectItem>

544

<SelectItem key="high">High</SelectItem>

545

<SelectItem key="urgent">Urgent</SelectItem>

546

</Select>

547

)}

548

</Field>

549

550

<Field name="message">

551

{({ field, meta }: any) => (

552

<Textarea

553

{...field}

554

label="Message"

555

placeholder="Enter your message"

556

minRows={4}

557

isInvalid={!!(meta.touched && meta.error)}

558

errorMessage={meta.touched && meta.error}

559

/>

560

)}

561

</Field>

562

</CardBody>

563

<CardFooter>

564

<Button

565

type="submit"

566

color="primary"

567

isLoading={isSubmitting}

568

className="w-full"

569

size="lg"

570

>

571

{isSubmitting ? "Sending..." : "Send Message"}

572

</Button>

573

</CardFooter>

574

</FormikForm>

575

)}

576

</Formik>

577

</Card>

578

);

579

}

580

```

581

582

### Field Validation Patterns

583

584

Common validation patterns and utilities for form fields.

585

586

```typescript

587

import {

588

Input, DatePicker, Select, SelectItem, Checkbox,

589

Button, Card, CardBody

590

} from "@nextui-org/react";

591

import { CalendarDate, today, getLocalTimeZone } from "@internationalized/date";

592

593

// Validation utilities

594

const validators = {

595

required: (value: any) => {

596

if (!value || (typeof value === "string" && !value.trim())) {

597

return "This field is required";

598

}

599

return true;

600

},

601

602

email: (value: string) => {

603

if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) {

604

return "Please enter a valid email address";

605

}

606

return true;

607

},

608

609

minLength: (min: number) => (value: string) => {

610

if (value && value.length < min) {

611

return `Must be at least ${min} characters`;

612

}

613

return true;

614

},

615

616

maxLength: (max: number) => (value: string) => {

617

if (value && value.length > max) {

618

return `Must be no more than ${max} characters`;

619

}

620

return true;

621

},

622

623

phone: (value: string) => {

624

if (value && !/^\+?[\d\s\-\(\)]+$/.test(value)) {

625

return "Please enter a valid phone number";

626

}

627

return true;

628

},

629

630

url: (value: string) => {

631

if (value) {

632

try {

633

new URL(value);

634

} catch {

635

return "Please enter a valid URL";

636

}

637

}

638

return true;

639

},

640

641

futureDate: (value: CalendarDate | null) => {

642

if (value && value.compare(today(getLocalTimeZone())) <= 0) {

643

return "Date must be in the future";

644

}

645

return true;

646

},

647

648

passwordStrength: (value: string) => {

649

if (value) {

650

const hasLower = /[a-z]/.test(value);

651

const hasUpper = /[A-Z]/.test(value);

652

const hasNumber = /\d/.test(value);

653

const hasSymbol = /[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(value);

654

const isLongEnough = value.length >= 8;

655

656

if (!isLongEnough) return "Password must be at least 8 characters";

657

if (!hasLower) return "Password must contain lowercase letters";

658

if (!hasUpper) return "Password must contain uppercase letters";

659

if (!hasNumber) return "Password must contain numbers";

660

if (!hasSymbol) return "Password must contain symbols";

661

}

662

return true;

663

},

664

665

confirmPassword: (original: string) => (value: string) => {

666

if (value && value !== original) {

667

return "Passwords do not match";

668

}

669

return true;

670

},

671

};

672

673

// Validation runner utility

674

const runValidations = (value: any, validationFns: ((value: any) => boolean | string)[]) => {

675

for (const validate of validationFns) {

676

const result = validate(value);

677

if (result !== true) {

678

return result;

679

}

680

}

681

return true;

682

};

683

684

function ValidationPatternsExample() {

685

const [formData, setFormData] = useState({

686

email: "",

687

password: "",

688

confirmPassword: "",

689

website: "",

690

birthDate: null as CalendarDate | null,

691

phone: "",

692

terms: false,

693

});

694

695

const [errors, setErrors] = useState<Record<string, string>>({});

696

697

const validateField = (name: string, value: any) => {

698

let result: boolean | string = true;

699

700

switch (name) {

701

case "email":

702

result = runValidations(value, [

703

validators.required,

704

validators.email,

705

]);

706

break;

707

case "password":

708

result = runValidations(value, [

709

validators.required,

710

validators.passwordStrength,

711

]);

712

break;

713

case "confirmPassword":

714

result = runValidations(value, [

715

validators.required,

716

validators.confirmPassword(formData.password),

717

]);

718

break;

719

case "website":

720

result = runValidations(value, [validators.url]);

721

break;

722

case "birthDate":

723

result = runValidations(value, [validators.futureDate]);

724

break;

725

case "phone":

726

result = runValidations(value, [validators.phone]);

727

break;

728

case "terms":

729

result = value ? true : "You must accept the terms";

730

break;

731

}

732

733

setErrors(prev => ({

734

...prev,

735

[name]: result === true ? "" : result

736

}));

737

738

return result === true;

739

};

740

741

const updateField = (name: string) => (value: any) => {

742

setFormData(prev => ({ ...prev, [name]: value }));

743

validateField(name, value);

744

};

745

746

return (

747

<Card className="max-w-lg mx-auto">

748

<CardBody className="space-y-4">

749

<Input

750

type="email"

751

label="Email Address"

752

placeholder="Enter your email"

753

value={formData.email}

754

onValueChange={updateField("email")}

755

isInvalid={!!errors.email}

756

errorMessage={errors.email}

757

isRequired

758

/>

759

760

<Input

761

type="password"

762

label="Password"

763

placeholder="Create a strong password"

764

value={formData.password}

765

onValueChange={updateField("password")}

766

isInvalid={!!errors.password}

767

errorMessage={errors.password}

768

description="Must contain uppercase, lowercase, numbers, and symbols"

769

isRequired

770

/>

771

772

<Input

773

type="password"

774

label="Confirm Password"

775

placeholder="Confirm your password"

776

value={formData.confirmPassword}

777

onValueChange={updateField("confirmPassword")}

778

isInvalid={!!errors.confirmPassword}

779

errorMessage={errors.confirmPassword}

780

isRequired

781

/>

782

783

<Input

784

type="url"

785

label="Website (Optional)"

786

placeholder="https://example.com"

787

value={formData.website}

788

onValueChange={updateField("website")}

789

isInvalid={!!errors.website}

790

errorMessage={errors.website}

791

/>

792

793

<Input

794

type="tel"

795

label="Phone Number (Optional)"

796

placeholder="+1 (555) 123-4567"

797

value={formData.phone}

798

onValueChange={updateField("phone")}

799

isInvalid={!!errors.phone}

800

errorMessage={errors.phone}

801

/>

802

803

<DatePicker

804

label="Event Date"

805

value={formData.birthDate}

806

onChange={updateField("birthDate")}

807

minValue={today(getLocalTimeZone()).add({ days: 1 })}

808

isInvalid={!!errors.birthDate}

809

errorMessage={errors.birthDate}

810

description="Select a future date"

811

/>

812

813

<Checkbox

814

isSelected={formData.terms}

815

onValueChange={updateField("terms")}

816

isInvalid={!!errors.terms}

817

color={errors.terms ? "danger" : "primary"}

818

>

819

I agree to the terms and conditions

820

</Checkbox>

821

822

{errors.terms && (

823

<p className="text-danger text-sm">{errors.terms}</p>

824

)}

825

826

<Button

827

color="primary"

828

className="w-full"

829

isDisabled={Object.values(errors).some(error => !!error) || !formData.terms}

830

>

831

Submit Form

832

</Button>

833

</CardBody>

834

</Card>

835

);

836

}

837

```

838

839

### Multi-Step Form Pattern

840

841

Building complex multi-step forms with NextUI components.

842

843

```typescript

844

import {

845

Card, CardHeader, CardBody, CardFooter,

846

Button, Input, Select, SelectItem, DatePicker, Textarea,

847

Progress, Divider

848

} from "@nextui-org/react";

849

import { CalendarDate } from "@internationalized/date";

850

851

interface StepData {

852

// Step 1: Personal Info

853

firstName: string;

854

lastName: string;

855

email: string;

856

phone: string;

857

birthDate: CalendarDate | null;

858

859

// Step 2: Address

860

address: string;

861

city: string;

862

state: string;

863

zipCode: string;

864

country: string;

865

866

// Step 3: Preferences

867

interests: string[];

868

bio: string;

869

newsletter: boolean;

870

}

871

872

function MultiStepFormExample() {

873

const [currentStep, setCurrentStep] = useState(1);

874

const [formData, setFormData] = useState<StepData>({

875

firstName: "", lastName: "", email: "", phone: "", birthDate: null,

876

address: "", city: "", state: "", zipCode: "", country: "",

877

interests: [], bio: "", newsletter: false,

878

});

879

const [errors, setErrors] = useState<Record<string, string>>({});

880

881

const totalSteps = 3;

882

const progress = (currentStep / totalSteps) * 100;

883

884

const validateStep = (step: number): boolean => {

885

const newErrors: Record<string, string> = {};

886

887

switch (step) {

888

case 1:

889

if (!formData.firstName.trim()) newErrors.firstName = "First name is required";

890

if (!formData.lastName.trim()) newErrors.lastName = "Last name is required";

891

if (!formData.email.trim()) newErrors.email = "Email is required";

892

break;

893

case 2:

894

if (!formData.address.trim()) newErrors.address = "Address is required";

895

if (!formData.city.trim()) newErrors.city = "City is required";

896

if (!formData.country) newErrors.country = "Country is required";

897

break;

898

case 3:

899

// Optional validation for final step

900

break;

901

}

902

903

setErrors(newErrors);

904

return Object.keys(newErrors).length === 0;

905

};

906

907

const nextStep = () => {

908

if (validateStep(currentStep)) {

909

setCurrentStep(prev => Math.min(prev + 1, totalSteps));

910

}

911

};

912

913

const prevStep = () => {

914

setCurrentStep(prev => Math.max(prev - 1, 1));

915

};

916

917

const handleSubmit = () => {

918

if (validateStep(currentStep)) {

919

console.log("Form submitted:", formData);

920

// Handle final submission

921

}

922

};

923

924

const updateField = (field: keyof StepData) => (value: any) => {

925

setFormData(prev => ({ ...prev, [field]: value }));

926

if (errors[field]) {

927

setErrors(prev => ({ ...prev, [field]: "" }));

928

}

929

};

930

931

const renderStep = () => {

932

switch (currentStep) {

933

case 1:

934

return (

935

<div className="space-y-4">

936

<div className="grid grid-cols-2 gap-4">

937

<Input

938

label="First Name"

939

placeholder="Enter first name"

940

value={formData.firstName}

941

onValueChange={updateField("firstName")}

942

isInvalid={!!errors.firstName}

943

errorMessage={errors.firstName}

944

isRequired

945

/>

946

<Input

947

label="Last Name"

948

placeholder="Enter last name"

949

value={formData.lastName}

950

onValueChange={updateField("lastName")}

951

isInvalid={!!errors.lastName}

952

errorMessage={errors.lastName}

953

isRequired

954

/>

955

</div>

956

<Input

957

type="email"

958

label="Email"

959

placeholder="Enter your email"

960

value={formData.email}

961

onValueChange={updateField("email")}

962

isInvalid={!!errors.email}

963

errorMessage={errors.email}

964

isRequired

965

/>

966

<Input

967

type="tel"

968

label="Phone Number"

969

placeholder="Enter phone number"

970

value={formData.phone}

971

onValueChange={updateField("phone")}

972

/>

973

<DatePicker

974

label="Birth Date"

975

value={formData.birthDate}

976

onChange={updateField("birthDate")}

977

/>

978

</div>

979

);

980

981

case 2:

982

return (

983

<div className="space-y-4">

984

<Input

985

label="Street Address"

986

placeholder="Enter your address"

987

value={formData.address}

988

onValueChange={updateField("address")}

989

isInvalid={!!errors.address}

990

errorMessage={errors.address}

991

isRequired

992

/>

993

<div className="grid grid-cols-2 gap-4">

994

<Input

995

label="City"

996

placeholder="Enter city"

997

value={formData.city}

998

onValueChange={updateField("city")}

999

isInvalid={!!errors.city}

1000

errorMessage={errors.city}

1001

isRequired

1002

/>

1003

<Input

1004

label="State/Province"

1005

placeholder="Enter state"

1006

value={formData.state}

1007

onValueChange={updateField("state")}

1008

/>

1009

</div>

1010

<div className="grid grid-cols-2 gap-4">

1011

<Input

1012

label="ZIP Code"

1013

placeholder="Enter ZIP code"

1014

value={formData.zipCode}

1015

onValueChange={updateField("zipCode")}

1016

/>

1017

<Select

1018

label="Country"

1019

placeholder="Select country"

1020

selectedKeys={formData.country ? [formData.country] : []}

1021

onSelectionChange={(keys) => {

1022

const selected = Array.from(keys)[0] as string;

1023

updateField("country")(selected);

1024

}}

1025

isInvalid={!!errors.country}

1026

errorMessage={errors.country}

1027

isRequired

1028

>

1029

<SelectItem key="us">United States</SelectItem>

1030

<SelectItem key="ca">Canada</SelectItem>

1031

<SelectItem key="uk">United Kingdom</SelectItem>

1032

</Select>

1033

</div>

1034

</div>

1035

);

1036

1037

case 3:

1038

return (

1039

<div className="space-y-4">

1040

<Textarea

1041

label="Bio (Optional)"

1042

placeholder="Tell us about yourself"

1043

value={formData.bio}

1044

onValueChange={updateField("bio")}

1045

maxRows={4}

1046

/>

1047

<div className="text-center">

1048

<p>Review your information and submit when ready.</p>

1049

</div>

1050

</div>

1051

);

1052

1053

default:

1054

return null;

1055

}

1056

};

1057

1058

return (

1059

<Card className="max-w-2xl mx-auto">

1060

<CardHeader className="space-y-2">

1061

<div className="flex justify-between items-center w-full">

1062

<h2 className="text-xl font-bold">Registration Form</h2>

1063

<span className="text-sm text-default-500">

1064

Step {currentStep} of {totalSteps}

1065

</span>

1066

</div>

1067

<Progress

1068

value={progress}

1069

color="primary"

1070

className="w-full"

1071

showValueLabel={false}

1072

/>

1073

</CardHeader>

1074

1075

<CardBody>

1076

{renderStep()}

1077

</CardBody>

1078

1079

<Divider />

1080

1081

<CardFooter className="flex justify-between">

1082

<Button

1083

variant="flat"

1084

onPress={prevStep}

1085

isDisabled={currentStep === 1}

1086

>

1087

Previous

1088

</Button>

1089

1090

{currentStep < totalSteps ? (

1091

<Button color="primary" onPress={nextStep}>

1092

Next Step

1093

</Button>

1094

) : (

1095

<Button color="success" onPress={handleSubmit}>

1096

Submit Form

1097

</Button>

1098

)}

1099

</CardFooter>

1100

</Card>

1101

);

1102

}

1103

```

1104

1105

## Form Integration Types

1106

1107

```typescript { .api }

1108

// Form validation types

1109

type ValidationError = string | string[];

1110

1111

interface ValidationErrors {

1112

[fieldName: string]: ValidationError;

1113

}

1114

1115

interface ValidationResult {

1116

isInvalid: boolean;

1117

validationErrors: string[];

1118

validationDetails: ValidationDetails;

1119

}

1120

1121

interface ValidationDetails {

1122

[key: string]: any;

1123

}

1124

1125

// Form state types

1126

interface FormState {

1127

values: Record<string, any>;

1128

errors: ValidationErrors;

1129

touched: Record<string, boolean>;

1130

isSubmitting: boolean;

1131

isValidating: boolean;

1132

isDirty: boolean;

1133

isValid: boolean;

1134

submitCount: number;

1135

}

1136

1137

// Form field types

1138

interface FieldProps<T = any> {

1139

name: string;

1140

value: T;

1141

onChange: (value: T) => void;

1142

onBlur?: () => void;

1143

error?: string;

1144

isInvalid?: boolean;

1145

isDisabled?: boolean;

1146

isRequired?: boolean;

1147

}

1148

1149

// Validation function types

1150

type FieldValidator<T = any> = (value: T) => boolean | string | Promise<boolean | string>;

1151

type FormValidator<T = Record<string, any>> = (values: T) => ValidationErrors | Promise<ValidationErrors>;

1152

1153

// Form hook return types

1154

interface UseFormReturn<T = Record<string, any>> {

1155

values: T;

1156

errors: ValidationErrors;

1157

touched: Record<keyof T, boolean>;

1158

isSubmitting: boolean;

1159

isValid: boolean;

1160

isDirty: boolean;

1161

1162

setFieldValue: (field: keyof T, value: any) => void;

1163

setFieldError: (field: keyof T, error: string | null) => void;

1164

setFieldTouched: (field: keyof T, touched?: boolean) => void;

1165

1166

validateField: (field: keyof T) => Promise<boolean>;

1167

validateForm: () => Promise<boolean>;

1168

1169

handleSubmit: (onSubmit: (values: T) => void | Promise<void>) => (e?: React.FormEvent) => void;

1170

handleReset: () => void;

1171

1172

getFieldProps: (name: keyof T) => FieldProps;

1173

}

1174

1175

// Multi-step form types

1176

interface StepConfig {

1177

id: string;

1178

title: string;

1179

fields: string[];

1180

validation?: FormValidator;

1181

optional?: boolean;

1182

}

1183

1184

interface MultiStepFormState {

1185

currentStep: number;

1186

totalSteps: number;

1187

completedSteps: number[];

1188

canGoNext: boolean;

1189

canGoPrevious: boolean;

1190

isFirstStep: boolean;

1191

isLastStep: boolean;

1192

}

1193

```

1194

1195

## Integration Best Practices

1196

1197

### Form Accessibility

1198

1199

Ensuring forms are accessible and follow ARIA best practices.

1200

1201

```typescript

1202

// Accessibility patterns for forms

1203

const AccessibleFormExample = () => {

1204

return (

1205

<form role="form" aria-label="Contact form">

1206

<fieldset>

1207

<legend className="text-lg font-semibold mb-4">Personal Information</legend>

1208

1209

<Input

1210

label="Full Name"

1211

placeholder="Enter your full name"

1212

isRequired

1213

description="This will be used as your display name"

1214

// Automatically gets proper ARIA attributes

1215

/>

1216

1217

<Input

1218

type="email"

1219

label="Email Address"

1220

placeholder="Enter your email"

1221

isRequired

1222

validate={(value) => {

1223

if (!value) return "Email is required";

1224

if (!/\S+@\S+\.\S+/.test(value)) return "Invalid email format";

1225

return true;

1226

}}

1227

// Error messages are automatically announced by screen readers

1228

/>

1229

</fieldset>

1230

1231

<Button

1232

type="submit"

1233

aria-describedby="submit-help"

1234

>

1235

Submit Form

1236

</Button>

1237

1238

<p id="submit-help" className="text-sm text-default-500 mt-2">

1239

Your information will be processed securely

1240

</p>

1241

</form>

1242

);

1243

};

1244

```

1245

1246

### Performance Optimization

1247

1248

Optimizing form performance for large forms with many fields.

1249

1250

```typescript

1251

// Debounced validation for better performance

1252

import { useDeferredValue, useCallback } from "react";

1253

import { debounce } from "lodash";

1254

1255

const useOptimizedValidation = (validator: FieldValidator, delay = 300) => {

1256

const debouncedValidator = useCallback(

1257

debounce(validator, delay),

1258

[validator, delay]

1259

);

1260

1261

return debouncedValidator;

1262

};

1263

1264

// Memoized form fields to prevent unnecessary re-renders

1265

const MemoizedFormField = React.memo(({ field, value, onChange, error }: any) => {

1266

return (

1267

<Input

1268

label={field.label}

1269

value={value}

1270

onValueChange={onChange}

1271

isInvalid={!!error}

1272

errorMessage={error}

1273

{...field.props}

1274

/>

1275

);

1276

});

1277

```