HTML modal form

The dialog element plus a form with method dialog gives you a modal form with focus handling and Escape built in.

An HTML modal form is a <form method="dialog"> placed inside a <dialog> element and opened with showModal().

That combination gives you the backdrop, the focus trap, Escape to close and a return value, with no library and no click handler on a background overlay.

A page with a button labelled Add contact and no dialog visible.
A page with a button labelled Add contact and no dialog visible.

The whole HTML modal form

<button type="button" id="open">Add contact</button>

<dialog id="dlg">
  <form method="dialog">
    <h2>Add contact</h2>

    <label for="name">Name</label>
    <input id="name" name="name" required>

    <label for="email">Email</label>
    <input id="email" name="email" type="email" required>

    <menu>
      <button value="cancel" formnovalidate>Cancel</button>
      <button value="save">Save</button>
    </menu>
  </form>
</dialog>
var dlg = document.getElementById('dlg');
document.getElementById('open').addEventListener('click', function () { dlg.showModal(); });
dlg.addEventListener('close', function () {
  if (dlg.returnValue === 'save') {
    console.log(new FormData(dlg.querySelector('form')).get('name'));
  }
});

Three lines in there do the work that overlay libraries exist for.

method="dialog" makes the submit button close the dialog instead of navigating, and writes the button's value into dialog.returnValue.

formnovalidate on Cancel lets the reader leave a half filled form. Without it, Cancel is blocked by the required fields, which is infuriating and very common.

The close listener is where you read the values. Reading them in the button's click handler works too, but close also catches Escape and the browser's own close paths.

showModal versus show versus the open attribute

How it is opened Backdrop Rest of page inert Escape closes
showModal() Yes Yes Yes
show() No No No
open attribute in markup No No No

Only the first row is a modal. If you set the open attribute in the markup and style a backdrop yourself, you get something that looks modal and lets Tab walk straight out of it into the page behind.

Style the backdrop with the pseudo element:

dialog::backdrop { background: rgba(0,0,0,.6); }
dialog { border: 1px solid #2c2f36; background: #16181d; color: #e6e8eb;
         border-radius: 10px; padding: 1.25rem; max-width: 28rem; width: 92vw; }
The dialog open over a dimmed page, with two fields and two buttons.
The dialog open over a dimmed page, with two fields and two buttons.

Validation inside a dialog

Required fields, type="email", pattern and min all behave exactly as they do on a page. The browser's message appears anchored to the offending field inside the dialog.

One thing to watch. If a required field is inside a collapsed section of the dialog, the browser cannot anchor a message to an invisible field, and submission fails silently.

Keep every field in a modal visible. If it does not fit, the form is too long for a modal.

You can also validate before closing. Give the save button no value shortcut and check form.reportValidity() yourself if the submission needs an extra rule beyond the built in ones.

Not losing what was typed

Escape fires cancel before close. The default action is to close, and everything typed is discarded.

dlg.addEventListener('cancel', function (e) {
  var f = dlg.querySelector('form');
  if (f.name.value || f.email.value) {
    e.preventDefault();
    if (confirm('Discard this contact?')) dlg.close('cancel');
  }
});

Do not remove Escape entirely. It is the expected way out, and a modal with no keyboard exit is worse than one that occasionally asks a question.

To clear the form between openings, call form.reset() after close. Dialogs keep their contents, so yesterday's half finished entry is still there the next time it opens.

Focus

showModal() moves focus into the dialog and restores it to the trigger on close. It picks the first focusable element, which is usually right.

To control it, mark the field you want with autofocus. Do not put autofocus on the Cancel button, and do not put it on a destructive action.

On a phone, focusing a text input opens the keyboard immediately and covers most of the modal. For a short form that is fine.

For anything with more than three fields it is a reason to use a page instead. Controlling focus order covers the cases where the first focusable element is not the right one.

Sending what was typed somewhere

A dialog closing with returnValue set to save is not a submission. Nothing has left the page.

Three routes from there, in increasing order of work.

Route What it needs Good for
Write the values into the page Nothing Calculators, drafts, previews
Post to a form endpoint A service that accepts posts Collecting real answers
Build a mailto link from the values Nothing One-off requests

For the first, read the FormData in the close handler and write the result into the document. The reader sees their entry appear, which is the feedback that tells them it worked.

For the second, change method="dialog" to a real method and action, and close the dialog yourself after the request resolves. You lose the free close behaviour and gain an actual submission.

Turning a form into a link covers the endpoints in more detail.

When a modal is the wrong container

  • More than three or four fields. The reader cannot see the page they were reading, so any form that needs context is being made harder.
  • Anything that needs scrolling inside it. Two scroll areas on one screen is a phone problem with no good fix.
  • A confirmation with no input. That is a yes no question, not a form, and it can be a much smaller dialog.
  • Anything that should be linkable. A modal has no address of its own, so nobody can send someone straight to it.

That last point is the one people find out late. If the form needs to be sent to someone, it wants to be a page with an address, which is what turning a form into a link covers.

Checking it and sending it

The dialog with focus on the Save button after tabbing from the last field.
The dialog with focus on the Save button after tabbing from the last field.

Open the file in the HTML file opener. Open the dialog, press Tab repeatedly, and confirm focus never leaves the dialog. Press Escape. Reopen it and check the fields are in the state you expect.

Then paste the HTML into a NOS document. The dialog, the script and the validation all run at the document's own address, so the reader opens a link and gets a working form.

Editing the page later does not change the address, so a correction to a field label reaches everyone who already has the link. Deeper styling of the container itself is covered in CSS modals.

Questions people ask

How do I make a modal form in HTML?

Put a form inside a dialog element and open it with showModal. Give the form method dialog so the submit button closes the dialog and the returnValue tells your script which button was used.

Does form validation work inside a dialog?

Yes. Required fields and input types validate normally, and the browser shows its message anchored to the field. The one exception is a button with formnovalidate, which closes the dialog without checking anything.

Why does Escape close my modal form and lose everything typed?

Escape fires the cancel event on the dialog, which closes it by default. If losing input matters, listen for cancel, call preventDefault, and ask for confirmation before closing.

Should a long form be in a modal?

No. A modal hides the page behind it, cannot be scrolled alongside the source material, and is awkward on a phone. Use a modal for two or three fields and a page for anything longer.

Keep reading