How toOpen a Modal Dialog in React with the Invoker Commands API
React modal dialogs often start with useState, refs, click handlers, and a dialog component library. That makes sense when you need animations, controlled state, nested overlays, or custom focus behavior, but it is a lot of code for a simple "open this dialog" interaction.
The Invoker Commands API lets a button trigger built-in browser actions on another element with HTML attributes. In React, you can use those attributes directly on a button, keep the native dialog element, and avoid storing open state in React at all.
Type the Button Attributes
React passes unknown DOM attributes through to the browser, but your React types may not know about command and commandfor yet. Add a small type augmentation so TypeScript accepts the attributes in JSX.
The StandardCommand union includes the standard command values browsers understand today. The command attribute also accepts custom commands that start with --, so the type allows any `--${string}` value too:
app/types/react-command-attributes.d.ts import "react"; declare module "react" { type StandardCommand = | "show-modal" | "close" | "request-close" | "show-popover" | "hide-popover" | "toggle-popover"; interface ButtonHTMLAttributes<T> { command?: StandardCommand | `--${string}`; commandfor?: string; } }
This only updates the TypeScript type for React button props. It does not add runtime code, and you can remove it once your React types include these attributes.
Render the Dialog
Start with a React component that renders the trigger and the native dialog. The commandfor value must match the id of the dialog:
app/components/contact-dialog.tsx import { useId } from "react"; export function ContactDialog() { let id = useId(); return ( <> <button type="button" command="show-modal" commandfor={id}> Contact Us </button> <dialog id={id}> <form method="post"> <h2>Contact Us</h2> <label> Email <input type="email" name="email" required /> </label> <label> Message <textarea name="message" required /> </label> <button type="submit">Send Message</button> </form> </dialog> </> ); }
The command="show-modal" attribute tells the browser which action to run. The commandfor={id} attribute tells the browser which element receives that command.
There is no useState(false), no setOpen(true), and no ref calling dialog.showModal(). React renders the markup, then the browser handles the interaction when the user clicks the button.
Add a Close Button
A modal dialog should give the user an explicit way to close it. Inside a dialog, a form with method="dialog" closes the dialog when submitted:
app/components/contact-dialog.tsx import { useId } from "react"; export function ContactDialog() { let id = useId(); return ( <> <button type="button" command="show-modal" commandfor={id}> Contact Us </button> <dialog id={id}> <form method="dialog"> <button type="submit" aria-label="Close"> Close </button> </form> <form method="post"> <h2>Contact Us</h2> <label> Email <input type="email" name="email" required /> </label> <label> Message <textarea name="message" required /> </label> <button type="submit">Send Message</button> </form> </dialog> </> ); }
Keeping the close control inside its own method="dialog" form avoids mixing it with the contact form submission. The contact form can still use method="post", while the close form only dismisses the dialog.
You can also use the close command from another invoker button if you prefer to keep open and close behavior in the same declarative pattern:
app/components/contact-dialog.tsx <button type="button" command="close" commandfor={id}> Close </button>
Use the method="dialog" form when the button lives inside the dialog and should work without depending on the newer command attributes. Use the close command when the close control is part of the invoker pattern you are already using.
Keep Libraries for Harder Dialogs
This pattern works well for simple modal dialogs where the browser's dialog behavior is enough. You do not need a complex dialog library just to open a contact form, settings panel, or confirmation message.
A dialog library is still useful when you need app-level composition, custom animation states, nested overlay coordination, or consistent styling primitives across many dialogs. The trade-off is more component state and more library code for behavior the browser may already provide.
Final Thoughts
For simple modal dialogs, this removes the React state you would usually add only to call showModal() and close(). The button already knows the command, the dialog already knows how to open, and React only needs to render the markup.
Do you like my content?
Your sponsorship helps me create more tutorials, articles, and open-source tools.