Skip to content
shadcn.io

shadcn.io is not affiliated with official shadcn/ui

Modal Create Form Block for React Aria

A project manager's Projects page with its New project dialog shown open: name, description and category, and a discard guard that swaps the footer for Keep editing and Discard when a dirty form is dismissed.

Framework
UI library

Preview

Preview width

Installation

Part of Pro
Pro unlocks the code and one-line install of every Pro button and text effect, plus the MCP for your AI editor.
Have Pro already? Sign in to open it.Opens here as soon as you're Pro.

Overview

Create a record without losing a half-written one, with this modal create form for React with React Aria. It opens from the Projects page of Kiteboard, a project manager: the page's header holds an outline New project Button, and the page itself is a drawn table under a soft scrim inside a hairline frame with a plus mark on each corner. The dialog holds a name (Input, 3 to 60 characters, not one that exists), an optional description (Textarea, 140 characters or fewer) and a category, each in a Field with a reserved line for its hint or problem. Escape, the scrim, the close button or Cancel on a clean form close it at once. On a dirty form the footer swaps, in the same space, for "Discard this project?" with Keep editing, where focus lands, and Discard; a second Escape means keep editing. Create project, the only Shine Button, checks the fields, adds a row to the table behind, closes and says so. For preview the dialog is drawn open in place when the page loads; New project opens the real Dialog, with the same fields and state. For projects, workspaces, labels and any create form in a modal.

FAQ

Why is the dialog already open, and what do I keep?

So you can see it without a click. On load the dialog is a copy drawn in place over the page: a native dialog element that is not modal, so nothing is trapped, hidden or focused. The New project button opens the real Dialog, modal, with its focus trap, Escape and focus return, and the copy stays closed until a reload. Both show the same ProjectFields component and share the block's state, so what you typed in the copy is there in the real one. In your app keep the Dialog and drop the copy: the "in-place" state, the scrim and the dialog element inside the frame.

How does the discard guard work?

Every way out except Create goes through one function, ask. On a clean form it closes. On a dirty one (anything typed or chosen) it keeps the dialog open and swaps the footer for the question, which sits in the same grid cell as the footer so the dialog never changes height; focus goes to Keep editing and a polite message says what happened. Keep editing, or Escape again, goes back to the fields; Discard closes and clears the form. The dialog is controlled, so Escape and the scrim reach ask through the Dialog's own open handler.

What does Create project do, and how do I wire it up?

It checks the name (3 to 60 characters, and not the name of a project in the list), the description length and that a category is chosen. A problem shows under its field and focus goes to the first one; otherwise a row is added at the top of the drawn table, the dialog closes, focus returns to New project and a polite message says "Created Spring launch in Product. Nothing was saved in this demo." In your app post the values from create, wait for the server, add the row from the response and show a server refusal (a name that is taken) in the field's slot.

Is it accessible from the keyboard and to screen readers?

Yes. The real dialog is named by its title and described by its sentence, focus starts in the name field and stays inside until it closes, and every close returns focus to New project. The button and the dialog sit inside DialogTrigger, controlled with isOpen and onOpenChange, and the title is a level 2 heading. Because a modal hides the page behind it, a second polite status inside the dialog speaks while it is open.

How does it look on a phone?

The copy shown on load docks to the foot of the page as a bottom sheet: the fields scroll above, and the footer stays pinned with its two buttons side by side. The real dialog keeps the Dialog's own phone layout, stuck to the bottom of the screen. The discard question uses the same footer space, with the question above the two buttons.

How do I connect it to react-hook-form or TanStack Form?

Keep the three values in the form and read its dirty state for the guard. With react-hook-form, formState.isDirty replaces isDirty here, handleSubmit(create) replaces the form's onSubmit and reset() replaces reset. With TanStack Form, use form.state.isDirty, validate each field on blur and call form.reset() from Discard.