Overlays
EditSheet
Edits something in a form inside a dialog: the whole interaction, done right. It opens from trigger as a centered dialog, or a bottom sheet on phones, with your fields, Cancel and a save button. Saving runs the browser's checks, then onSubmit with the fields as FormData; a promise shows loading on the save button and keeps the sheet open (it cannot be dismissed meanwhile). Errors it returns land on the matching fields, with focus on the first; a failed save shows failureMessage at the top of the form; success closes the sheet and returns focus to the trigger. Dismissing it after changing a field (Escape, a tap outside, a swipe down, Cancel) asks inside the sheet before the edits are lost. With controlled fields, reset your state in onDiscard, or the discarded values come back on the next opening.
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/edit-sheet#v1.2.13Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/edit-sheetIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/edit-sheet#v1.2.13 ourostack/ouro-ui/edit-sheet-tests#v1.2.13import { EditSheet } from "@/components/ouro/form/edit-sheet";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<EditSheet
title="Edit recipe"
trigger={<Button variant="outline">Edit</Button>}
onSubmit={async (data) => (await saveRecipe(data)).errors}
>
<TextField label="Name" name="name" defaultValue={recipe.name} required />
<Select label="Course" name="course" defaultValue={recipe.course} options={courses} />
</EditSheet>Use instead when
- Use
Forminstead for a form on the page,Dialogfor a dialog that is not a form, andConfirmDialogto confirm one action.
Props
The props the modes share. Type: EditSheetProps.
cancelLabel- Type:
string - Default:
"Cancel" - The cancel button's label. Translate it with your app.
children(required)- Type:
ReactNode - The fields:
TextField,Selectand other controls with aname. className- Type:
string - Layout only, applied to the panel: margins (
mt-4). Your classes win over Ouro UI's. Appearance classes warn in development. closeLabel- Type:
string - Default:
"Close" - The label of the close button in the corner. Translate it with your app.
description- Type:
ReactNode - A short line under the title, read out when the sheet opens.
discardLabel- Type:
string - Default:
"Discard" - The button that discards the edits and closes the sheet.
discardMessage- Type:
string - Default:
"Discard your changes?" - The question the sheet asks, in place of its buttons, when the user dismisses it (Escape, a tap outside, a swipe down, Cancel or the close button) after changing a field. Translate it with your app.
failureMessage- Type:
string - Default:
"Couldn't save. Try again." - What the sheet shows, at the top of the form, when
onSubmitrejects or throws. Translate it with your app. hideTitle- Type:
boolean - Hides the title visually. It still names the sheet for screen readers.
keepEditingLabel- Type:
string - Default:
"Keep editing" - The button that goes back to the form with the edits kept; it takes focus.
onDiscard- Type:
(() => void) - Called when the user confirms Discard, before the sheet closes. With controlled fields (
valueandonValueChange), reset your state here: the sheet cannot reset values it does not own, so they would come back on the next opening:<TextField name="name" value={name} onValueChange={setName} />withonDiscard={() => setName(recipe.name)}. Not called for a save, or a dismissal with nothing changed. onSubmit(required)- Type:
(data: FormData) => EditSheetResult | Promise<EditSheetResult> - Saves the form. It gets the fields as
FormData, after the browser's own checks (required,type="email") pass. Return nothing to close the sheet and return focus to the trigger, or errors by field name ({ name: "That name is taken." }) to keep it open with each message on its field and focus on the first; aformkey, or a key no field is named, shows in an alert at the top of the form. Return a promise to showloadingon the save button until it settles; meanwhile Cancel is disabled and the sheet cannot be dismissed. If it rejects (or throws), the sheet stays open withfailureMessage. submitLabel- Type:
string - Default:
"Save" - The save button's label: the action, such as
"Save recipe". Translate it with your app. title(required)- Type:
string - Names the sheet for screen readers and shows at its top:
title="Edit recipe". Hide it withhideTitle.
Modes
EditSheetProps is a union: pass the props of exactly one mode. Mixing them is a type error.
Controlled: EditSheetControlledProps
A controlled EditSheet: you own open and update it in onOpenChange.
onOpenChange(required)- Type:
(open: boolean) => void - Called with
falsewhen the user cancels or dismisses the sheet (after confirming, with edits), and when a save succeeds. open(required)- Type:
boolean - Whether the sheet is open.
Not in this mode (typed never):
trigger: Not withopen. RemoveopenandonOpenChangeto let a trigger open the sheet.
With trigger: EditSheetWithTriggerProps
An uncontrolled EditSheet: trigger opens it.
trigger(required)- Type:
ReactElement - The element that opens the sheet, usually a Button:
trigger={<Button>Edit</Button>}.
Not in this mode (typed never):
onOpenChange: Not withtrigger. Removetriggerto control the sheet withopenandonOpenChange.open: Not withtrigger. Removetriggerto control the sheet withopenandonOpenChange.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
errors: Not a prop. Return errors fromonSubmit:onSubmit={async (data) => (await save(data)).errors}.isOpen: Not a prop. Ouro UI usesopenwithonOpenChange:<EditSheet open={open} onOpenChange={setOpen}>.onClose: Not a prop.onOpenChange(false)fires on close:<EditSheet open={open} onOpenChange={setOpen}>.onSave: Not a prop. Ouro UI usesonSubmit:<EditSheet onSubmit={(data) => save(data)}>.saveLabel: Not a prop. Ouro UI usessubmitLabel:<EditSheet submitLabel="Save">.submitText: Not a prop. Ouro UI usessubmitLabel:<EditSheet submitLabel="Save">.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2).
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
EditSheetResult
What onSubmit may return: nothing (saved), or errors by field name (not saved).
type EditSheetResult = void | undefined | ReadonlyFormErrors;ErrorMessage
One error message: text, or a React element for a message that carries a link or other markup.
type ErrorMessage = string | ReactElement;ReadonlyErrorMessages
A read-only message view accepted by fields and Form; mutable input arrays remain valid.
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];ReadonlyFormErrors
A read-only display projection for Form and EditSheet; mutable raw FormErrors remain valid inputs.
type ReadonlyFormErrors = Readonly<Record<string, ReadonlyErrorMessages | null | undefined>>;