diff --git a/apps/v4/components/component-preview-tabs.tsx b/apps/v4/components/component-preview-tabs.tsx index e38e7a0f4b..04fdcda983 100644 --- a/apps/v4/components/component-preview-tabs.tsx +++ b/apps/v4/components/component-preview-tabs.tsx @@ -9,12 +9,14 @@ export function ComponentPreviewTabs({ className, align = "center", hideCode = false, + chromeLessOnMobile = false, component, source, ...props }: React.ComponentProps<"div"> & { align?: "center" | "start" | "end" hideCode?: boolean + chromeLessOnMobile?: boolean component: React.ReactNode source: React.ReactNode }) { @@ -51,7 +53,8 @@ export function ComponentPreviewTabs({
{component} diff --git a/apps/v4/components/component-preview.tsx b/apps/v4/components/component-preview.tsx index 1318bcd1be..f242cde03b 100644 --- a/apps/v4/components/component-preview.tsx +++ b/apps/v4/components/component-preview.tsx @@ -10,6 +10,7 @@ export function ComponentPreview({ className, align = "center", hideCode = false, + chromeLessOnMobile = false, ...props }: React.ComponentProps<"div"> & { name: string @@ -17,12 +18,13 @@ export function ComponentPreview({ description?: string hideCode?: boolean type?: "block" | "component" | "example" + chromeLessOnMobile?: boolean }) { const Component = Index[name]?.component if (!Component) { return ( -

+

Component{" "} {name} @@ -63,6 +65,7 @@ export function ComponentPreview({ hideCode={hideCode} component={} source={} + chromeLessOnMobile={chromeLessOnMobile} {...props} /> ) diff --git a/apps/v4/components/component-source.tsx b/apps/v4/components/component-source.tsx index 3aefe35f48..19bd09775c 100644 --- a/apps/v4/components/component-source.tsx +++ b/apps/v4/components/component-source.tsx @@ -43,6 +43,14 @@ export async function ComponentSource({ return null } + // Fix imports. + // Replace @/registry/new-york-v4/ with @/components/. + code = code.replaceAll("@/registry/new-york-v4/", "@/components/") + + // Replace export default with export. + code = code.replaceAll("export default", "export") + code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "") + const lang = language ?? title?.split(".").pop() ?? "tsx" const highlightedCode = await highlightCode(code, lang) diff --git a/apps/v4/components/docs-sidebar.tsx b/apps/v4/components/docs-sidebar.tsx index 313a225d67..08166a2e2f 100644 --- a/apps/v4/components/docs-sidebar.tsx +++ b/apps/v4/components/docs-sidebar.tsx @@ -31,6 +31,10 @@ const TOP_LEVEL_SECTIONS = [ name: "MCP Server", href: "/docs/mcp", }, + { + name: "Forms", + href: "/docs/forms", + }, { name: "Changelog", href: "/docs/changelog", diff --git a/apps/v4/components/mobile-nav.tsx b/apps/v4/components/mobile-nav.tsx index c400a5c89e..5722d1bf82 100644 --- a/apps/v4/components/mobile-nav.tsx +++ b/apps/v4/components/mobile-nav.tsx @@ -29,6 +29,10 @@ const TOP_LEVEL_SECTIONS = [ name: "MCP Server", href: "/docs/mcp", }, + { + name: "Forms", + href: "/docs/forms", + }, { name: "Changelog", href: "/docs/changelog", diff --git a/apps/v4/content/docs/components/checkbox.mdx b/apps/v4/content/docs/components/checkbox.mdx index 3b2ef2e9c4..0f2c24063d 100644 --- a/apps/v4/content/docs/components/checkbox.mdx +++ b/apps/v4/content/docs/components/checkbox.mdx @@ -56,9 +56,3 @@ import { Checkbox } from "@/components/ui/checkbox" ```tsx ``` - -## Examples - -### Form - - diff --git a/apps/v4/content/docs/components/combobox.mdx b/apps/v4/content/docs/components/combobox.mdx index 6c76d585f3..a8788f7f24 100644 --- a/apps/v4/content/docs/components/combobox.mdx +++ b/apps/v4/content/docs/components/combobox.mdx @@ -143,7 +143,3 @@ export function ExampleCombobox() { You can create a responsive combobox by using the `` on desktop and the `` components on mobile. - -### Form - - diff --git a/apps/v4/content/docs/components/date-picker.mdx b/apps/v4/content/docs/components/date-picker.mdx index f94ca0a014..24ca640ef0 100644 --- a/apps/v4/content/docs/components/date-picker.mdx +++ b/apps/v4/content/docs/components/date-picker.mdx @@ -94,7 +94,3 @@ This component uses the `chrono-node` library to parse natural language dates. title="Natural Language Picker" description="A calendar with natural language picker." /> - -### Form - - diff --git a/apps/v4/content/docs/components/field.mdx b/apps/v4/content/docs/components/field.mdx index ef65561110..72ea66badc 100644 --- a/apps/v4/content/docs/components/field.mdx +++ b/apps/v4/content/docs/components/field.mdx @@ -98,6 +98,10 @@ The `Field` family is designed for composing accessible forms. A typical field i - `FieldContent` is a flex column that groups label and description. Not required if you have no description. - Wrap related fields with `FieldGroup`, and use `FieldSet` with `FieldLegend` for semantic grouping. +## Form + +See the [Form](/docs/forms) documentation for building forms with the `Field` component and [React Hook Form](/docs/forms/react-hook-form) or [Tanstack Form](/docs/forms/tanstack-form). + ## Examples ### Input diff --git a/apps/v4/content/docs/components/form.mdx b/apps/v4/content/docs/components/form.mdx index 99a39e3b28..5992318212 100644 --- a/apps/v4/content/docs/components/form.mdx +++ b/apps/v4/content/docs/components/form.mdx @@ -1,10 +1,18 @@ --- -title: React Hook Form +title: Form description: Building forms with React Hook Form and Zod. links: doc: https://react-hook-form.com --- +import { InfoIcon } from "lucide-react" + +} title="We are not actively developing this component anymore."> + +The Form component is an abstraction over the `react-hook-form` library. Going forward, we recommend using the [``](/docs/components/field) component to build forms. See the [Form](/docs/form) documentation for more information. + + + Forms are tricky. They are one of the most common things you'll build in a web application, but also one of the most complex. Well-designed HTML forms are: @@ -119,8 +127,6 @@ npm install @radix-ui/react-label @radix-ui/react-slot react-hook-form @hookform ## Usage - - ### Create a form schema Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev). @@ -233,23 +239,3 @@ export function ProfileForm() { ### Done That's it. You now have a fully accessible form that is type-safe with client-side validation. - - - - - -## Examples - -See the following links for more examples on how to use the `

` component with other components: - -- [Checkbox](/docs/components/checkbox#form) -- [Date Picker](/docs/components/date-picker#form) -- [Input](/docs/components/input#form) -- [Radio Group](/docs/components/radio-group#form) -- [Select](/docs/components/select#form) -- [Switch](/docs/components/switch#form) -- [Textarea](/docs/components/textarea#form) -- [Combobox](/docs/components/combobox#form) diff --git a/apps/v4/content/docs/components/input.mdx b/apps/v4/content/docs/components/input.mdx index e9db832740..c4638ca6b1 100644 --- a/apps/v4/content/docs/components/input.mdx +++ b/apps/v4/content/docs/components/input.mdx @@ -94,10 +94,6 @@ import { Input } from "@/components/ui/input" description="An input component with a button." /> -### Form - - - ## Changelog ### 2025-09-18 Remove `flex` class diff --git a/apps/v4/content/docs/components/radio-group.mdx b/apps/v4/content/docs/components/radio-group.mdx index 934619fcaf..0b571c8354 100644 --- a/apps/v4/content/docs/components/radio-group.mdx +++ b/apps/v4/content/docs/components/radio-group.mdx @@ -69,9 +69,3 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
``` - -## Examples - -### Form - - diff --git a/apps/v4/content/docs/components/select.mdx b/apps/v4/content/docs/components/select.mdx index 0c037852da..96e6f3bc7e 100644 --- a/apps/v4/content/docs/components/select.mdx +++ b/apps/v4/content/docs/components/select.mdx @@ -84,7 +84,3 @@ import { name="select-scrollable" description="A select component with a scrollable list of options." /> - -### Form - - diff --git a/apps/v4/content/docs/components/switch.mdx b/apps/v4/content/docs/components/switch.mdx index 317fc81768..2c7bae254c 100644 --- a/apps/v4/content/docs/components/switch.mdx +++ b/apps/v4/content/docs/components/switch.mdx @@ -56,9 +56,3 @@ import { Switch } from "@/components/ui/switch" ```tsx ``` - -## Examples - -### Form - - diff --git a/apps/v4/content/docs/components/textarea.mdx b/apps/v4/content/docs/components/textarea.mdx index ca869b2734..c8175f73e9 100644 --- a/apps/v4/content/docs/components/textarea.mdx +++ b/apps/v4/content/docs/components/textarea.mdx @@ -79,7 +79,3 @@ import { Textarea } from "@/components/ui/textarea" name="textarea-with-button" description="A textarea with a button" /> - -### Form - - diff --git a/apps/v4/content/docs/forms/index.mdx b/apps/v4/content/docs/forms/index.mdx new file mode 100644 index 0000000000..abf56ae300 --- /dev/null +++ b/apps/v4/content/docs/forms/index.mdx @@ -0,0 +1,45 @@ +--- +title: Forms +description: Build forms with React and shadcn/ui. +--- + +import { ClipboardListIcon, InfoIcon } from "lucide-react" + +## Pick Your Framework + +Start by selecting your framework. Then follow the instructions to learn how to build forms with shadcn/ui and the form library of your choice. + +
+ + +

React Hook Form

+
+ + + + +

TanStack Form

+
+ + + React + + +

useActionState

+

(Coming Soon)

+ +
+
diff --git a/apps/v4/content/docs/forms/meta.json b/apps/v4/content/docs/forms/meta.json new file mode 100644 index 0000000000..a38239c8c5 --- /dev/null +++ b/apps/v4/content/docs/forms/meta.json @@ -0,0 +1,3 @@ +{ + "pages": ["react-hook-form", "tanstack-form"] +} diff --git a/apps/v4/content/docs/forms/next.mdx b/apps/v4/content/docs/forms/next.mdx new file mode 100644 index 0000000000..554114e505 --- /dev/null +++ b/apps/v4/content/docs/forms/next.mdx @@ -0,0 +1,397 @@ +--- +title: Next.js +description: Build forms in React using useActionState and Server Actions. +--- + +import { InfoIcon } from "lucide-react" + +In this guide, we will take a look at building forms with Next.js using `useActionState` and Server Actions. We'll cover building forms, validation, pending states, accessibility, and more. + +## Demo + +We are going to build the following form with a simple text input and a textarea. On submit, we'll use a server action to validate the form data and update the form state. + + + +}> + **Note:** The examples on this page intentionally disable browser validation + to show how schema validation and form errors work in server actions. + + +## Approach + +This form leverages Next.js and React's built-in capabilities for form handling. We'll build our form using the `` component, which gives you **complete flexibility over the markup and styling**. + +- Uses Next.js `` component for navigation and progressive enhancement. +- `` components for building accessible forms. +- `useActionState` for managing form state and errors. +- Handles loading states with pending prop. +- Server Actions for handling form submissions. +- Server-side validation using Zod. + +## Anatomy + +Here's a basic example of a form using the `` component. + +```tsx showLineNumbers + + + + Bug Title + + + Provide a concise title for your bug report. + + {formState.errors?.title && ( + {formState.errors.title[0]} + )} + + + + +``` + +## Usage + +### Create a form schema + +We'll start by defining the shape of our form using a Zod schema in a `schema.ts` file. + +}> + **Note:** This example uses `zod v3` for schema validation, but you can + replace it with any other schema validation library. Make sure your schema + library conforms to the Standard Schema specification. + + +```tsx showLineNumbers title="schema.ts" +import { z } from "zod" + +export const formSchema = z.object({ + title: z + .string() + .min(5, "Bug title must be at least 5 characters.") + .max(32, "Bug title must be at most 32 characters."), + description: z + .string() + .min(20, "Description must be at least 20 characters.") + .max(100, "Description must be at most 100 characters."), +}) +``` + +### Define the form state type + +Next, we'll create a type for our form state that includes values, errors, and success status. This will be used to type the form state on the client and server. + +```tsx showLineNumbers title="schema.ts" +import { z } from "zod" + +export type FormState = { + values?: z.infer + errors: null | Partial, string[]>> + success: boolean +} +``` + +**Important:** We define the schema and the `FormState` type in a separate file so we can import them into both the client and server components. + +### Create the Server Action + +A server action is a function that runs on the server and can be called from the client. We'll use it to validate the form data and update the form state. + + + +**Note:** We're returning `values` for error cases. This is because we want to keep the user submitted values in the form state. For success cases, we're returning empty values to reset the form. + +### Build the form + +We can now build the form using the `` component. We'll use the `useActionState` hook to manage the form state, server action, and pending state. + + + +### Done + +That's it. You now have a fully accessible form with client and server-side validation. + +When you submit the form, the `formAction` function will be called on the server. The server action will validate the form data and update the form state. + +If the form data is invalid, the server action will return the errors to the client. If the form data is valid, the server action will return the success status and update the form state. + +## Pending States + +Use the `pending` prop from `useActionState` to show loading indicators and disable form inputs. + +```tsx showLineNumbers {11,26-34} +"use client" + +import * as React from "react" +import Form from "next/form" + +import { Spinner } from "@/components/ui/spinner" + +import { bugReportFormAction } from "./actions" + +export function BugReportForm() { + const [formState, formAction, pending] = React.useActionState( + bugReportFormAction, + { + errors: null, + success: false, + } + ) + + return ( +
+ + + Name + + + + + + +
+ ) +} +``` + +## Disabled States + +### Submit Button + +To disable the submit button, use the `pending` prop on the button's `disabled` prop. + +```tsx showLineNumbers + +``` + +### Field + +To apply a disabled state and styling to a `` component, use the `data-disabled` prop on the `` component. + +```tsx showLineNumbers + + Name + + +``` + +## Validation + +### Server-side Validation + +Use `safeParse()` on your schema in your server action to validate the form data. + +```tsx showLineNumbers title="actions.ts" {12-20} +"use server" + +export async function bugReportFormAction( + _prevState: FormState, + formData: FormData +) { + const values = { + title: formData.get("title") as string, + description: formData.get("description") as string, + } + + const result = formSchema.safeParse(values) + + if (!result.success) { + return { + values, + success: false, + errors: result.error.flatten().fieldErrors, + } + } + + return { + errors: null, + success: true, + } +} +``` + +### Business Logic Validation + +You can add additional custom validation logic in your server action. + +Make sure to return the values on validation errors. This is to ensure that the form state maintains the user's input. + +```tsx showLineNumbers title="actions.ts" {22-35} +"use server" + +export async function bugReportFormAction( + _prevState: FormState, + formData: FormData +) { + const values = { + title: formData.get("title") as string, + description: formData.get("description") as string, + } + + const result = formSchema.safeParse(values) + + if (!result.success) { + return { + values, + success: false, + errors: result.error.flatten().fieldErrors, + } + } + + // Check if email already exists in database. + const existingUser = await db.user.findUnique({ + where: { email: result.data.email }, + }) + + if (existingUser) { + return { + values, + success: false, + errors: { + email: ["This email is already registered"], + }, + } + } + + return { + errors: null, + success: true, + } +} +``` + +## Displaying Errors + +Display errors next to the field using ``. Make sure to add the `data-invalid` prop to the `` component and `aria-invalid` prop to the input. + +```tsx showLineNumbers + + Email + + {formState.errors?.email && ( + {formState.errors.email[0]} + )} + +``` + +## Resetting the Form + +When you submit a form with a server action, React will automatically reset the form state to the initial values. + +### Reset on Success + +To reset the form on success, you can omit the `values` from the server action and React will automatically reset the form state to the initial values. This is standard React behavior. + +```tsx showLineNumbers title="actions.ts" {22-26} +export async function demoFormAction( + _prevState: FormState, + formData: FormData +) { + const values = { + title: formData.get("title") as string, + description: formData.get("description") as string, + } + + // Validation. + if (!result.success) { + return { + values, + success: false, + errors: result.error.flatten().fieldErrors, + } + } + + // Business logic. + callYourDatabaseOrAPI(values) + + // Omit the values on success to reset the form state. + return { + errors: null, + success: true, + } +} +``` + +### Preserve on Validation Errors + +To prevent the form from being reset on failure, you can return the values in the server action. This is to ensure that the form state maintains the user's input. + +```tsx showLineNumbers title="actions.ts" {12-17} +export async function demoFormAction( + _prevState: FormState, + formData: FormData +) { + const values = { + title: formData.get("title") as string, + description: formData.get("description") as string, + } + + // Validation. + if (!result.success) { + return { + // Return the values on validation errors. + values, + success: false, + errors: result.error.flatten().fieldErrors, + } + } +} +``` + +## Complex Forms + +Here is an example of a more complex form with multiple fields and validation. + + + +### Schema + + + +### Form + + + +### Server Action + + diff --git a/apps/v4/content/docs/forms/react-hook-form.mdx b/apps/v4/content/docs/forms/react-hook-form.mdx new file mode 100644 index 0000000000..76d07ab3ee --- /dev/null +++ b/apps/v4/content/docs/forms/react-hook-form.mdx @@ -0,0 +1,629 @@ +--- +title: React Hook Form +description: Build forms in React using React Hook Form and Zod. +links: + doc: https://react-hook-form.com +--- + +import { InfoIcon } from "lucide-react" + +In this guide, we will take a look at building forms with React Hook Form. We'll cover building forms with the `` component, adding schema validation using Zod, error handling, accessibility, and more. + +## Demo + +We are going to build the following form. It has a simple text input and a textarea. On submit, we'll validate the form data and display any errors. + +}> + **Note:** For the purpose of this demo, we have intentionally disabled browser + validation to show how schema validation and form errors work in React Hook + Form. It is recommended to add basic browser validation in your production + code. + + + + +## Approach + +This form leverages React Hook Form for performant, flexible form handling. We'll build our form using the `` component, which gives you **complete flexibility over the markup and styling**. + +- Uses React Hook Form's `useForm` hook for form state management. +- `` component for controlled inputs. +- `` components for building accessible forms. +- Client-side validation using Zod with `zodResolver`. + +## Anatomy + +Here's a basic example of a form using the `` component from React Hook Form and the `` component. + +```tsx showLineNumbers {5-18} + ( + + Bug Title + + + Provide a concise title for your bug report. + + {fieldState.invalid && } + + )} +/> +``` + +## Form + +### Create a form schema + +We'll start by defining the shape of our form using a Zod schema + +}> + **Note:** This example uses `zod v3` for schema validation, but you can + replace it with any other Standard Schema validation library supported by + React Hook Form. + + +```tsx showLineNumbers title="form.tsx" +import * as z from "zod" + +const formSchema = z.object({ + title: z + .string() + .min(5, "Bug title must be at least 5 characters.") + .max(32, "Bug title must be at most 32 characters."), + description: z + .string() + .min(20, "Description must be at least 20 characters.") + .max(100, "Description must be at most 100 characters."), +}) +``` + +### Setup the form + +Next, we'll use the `useForm` hook from React Hook Form to create our form instance. We'll also add the Zod resolver to validate the form data. + +```tsx showLineNumbers title="form.tsx" {17-23} +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" + +const formSchema = z.object({ + title: z + .string() + .min(5, "Bug title must be at least 5 characters.") + .max(32, "Bug title must be at most 32 characters."), + description: z + .string() + .min(20, "Description must be at least 20 characters.") + .max(100, "Description must be at most 100 characters."), +}) + +export function BugReportForm() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + title: "", + description: "", + }, + }) + + function onSubmit(data: z.infer) { + // Do something with the form values. + console.log(data) + } + + return ( +
+ {/* ... */} + {/* Build the form here */} + {/* ... */} +
+ ) +} +``` + +### Build the form + +We can now build the form using the `` component from React Hook Form and the `` component. + + + +### Done + +That's it. You now have a fully accessible form with client-side validation. + +When you submit the form, the `onSubmit` function will be called with the validated form data. If the form data is invalid, React Hook Form will display the errors next to each field. + +## Validation + +### Client-side Validation + +React Hook Form validates your form data using the Zod schema. Define a schema and pass it to the `resolver` option of the `useForm` hook. + +```tsx showLineNumbers title="example-form.tsx" {5-8,12} +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" + +const formSchema = z.object({ + title: z.string(), + description: z.string().optional(), +}) + +export function ExampleForm() { + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + title: "", + description: "", + }, + }) +} +``` + +### Validation Modes + +React Hook Form supports different validation modes. + +```tsx showLineNumbers title="form.tsx" {3} +const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "onChange", +}) +``` + +| Mode | Description | +| ------------- | -------------------------------------------------------- | +| `"onChange"` | Validation triggers on every change. | +| `"onBlur"` | Validation triggers on blur. | +| `"onSubmit"` | Validation triggers on submit (default). | +| `"onTouched"` | Validation triggers on first blur, then on every change. | +| `"all"` | Validation triggers on blur and change. | + +## Displaying Errors + +Display errors next to the field using ``. For styling and accessibility: + +- Add the `data-invalid` prop to the `` component. +- Add the `aria-invalid` prop to the form control such as ``, ``, ``, etc. + +```tsx showLineNumbers title="form.tsx" {5,11,13} + ( + + Email + + {fieldState.invalid && } + + )} +/> +``` + +## Working with Different Field Types + +### Input + +- For input fields, spread the `field` object onto the `` component. +- To show errors, add the `aria-invalid` prop to the `` component and the `data-invalid` prop to the `` component. + + + +For simple text inputs, spread the `field` object onto the input. + +```tsx showLineNumbers title="form.tsx" {5,7,8} + ( + + Name + + {fieldState.invalid && } + + )} +/> +``` + +### Textarea + +- For textarea fields, spread the `field` object onto the `