StartFoundationsComponentsContent

Dialog

A dialog is a modal window over the page: people finish or dismiss it before they go back. tk-dialog holds a short task; tk-confirm-dialog confirms one action.

Stable Figma
OverviewUsageAPIAccessibility
Rename project Cancel Rename Delete project
<tk-button onclick="document.getElementById('dialog').show()">Rename project</tk-button>
<tk-dialog id="dialog" heading="Rename project">
  <tk-field label="Project name"><tk-input value="Aurora"></tk-input></tk-field>
  <tk-button slot="footer" onclick="this.closest('tk-dialog').close()">Cancel</tk-button>
  <tk-button slot="footer" variant="primary" onclick="this.closest('tk-dialog').close()">Rename</tk-button>
</tk-dialog>
<tk-button tone="danger" onclick="document.getElementById('confirm').show()">Delete project</tk-button>
<tk-confirm-dialog id="confirm" heading="Delete project?" message="This permanently deletes 'Aurora' and all its data." confirm-label="Delete project" danger></tk-confirm-dialog>

Anatomy

Part tk-dialog tk-confirm-dialog
heading heading: the title and accessible name, required heading: a specific question, required
close button built in, top right none; Cancel is the way out
body the default slot; it scrolls message: one line about the consequence
footer the footer slot: your buttons, right-aligned Cancel and the confirm button, built in
backdrop dims the page and blocks it the same
dialog part the panel the panel

Variants

<tk-button tone="danger" onclick="document.getElementById('delete').show()">Delete project</tk-button>
<tk-confirm-dialog id="delete" heading="Delete project?" message="This permanently deletes Aurora and all its data." confirm-label="Delete" danger></tk-confirm-dialog>
  • tk-dialog: a task with content. You fill the body and the footer.
  • tk-confirm-dialog: confirms one action. Set it with attributes only: no slots, no close button, no scrolling, always 320px wide. confirm-label is required; without it, only Cancel renders.
  • danger: for an action that deletes something or can't be undone. The confirm button becomes a danger button, red-tinted with a red label, and focus starts on Cancel. Without danger, the confirm button is primary and takes focus.

For a large loss, add friction in a tk-dialog: keep the danger button disabled until the user checks a Checkbox or types the item's name into an Input. Keep this rare; friction everywhere teaches people to rush.

Sizes

size Width Use for
small 320px short decisions with some content
medium (default) 480px most forms and tasks
large 640px wide content, such as a two-column form or a table
  • The dialog stays inside the viewport with space-16 (16px) to spare on each side, down to a 320 × 256px viewport.
  • Long content scrolls in the body while the heading and footer stay put. The content fades at an edge while there's more beyond it. Nothing scrolls sideways; use a wider size.
  • tk-confirm-dialog has no size: it is 320px wide and doesn't scroll, so keep it short.

Opening and closing

<tk-button onclick="document.getElementById('rename').show()">Rename project</tk-button>
<tk-dialog id="rename" heading="Rename project">
  <form id="rename-form">
    <tk-field label="Project name"><tk-input name="name" value="Aurora" autofocus></tk-input></tk-field>
  </form>
  <tk-button slot="footer" onclick="this.closest('tk-dialog').close()">Cancel</tk-button>
  <tk-button slot="footer" variant="primary" type="submit" form="rename-form">Rename</tk-button>
</tk-dialog>
const dialog = document.getElementById('rename');
dialog.querySelector('form').addEventListener('submit', (event) => {
  event.preventDefault();
  // save the name, then:
  dialog.close('renamed');
});
  • show() opens the dialog, always as a modal. close(value) closes it, and returnValue holds the value in your close listener. Focus goes back to the element that opened it.
  • The footer sits outside your form, so give the submit button form with the form's id. <form method="dialog"> doesn't close the dialog; call close() on submit.
  • Put autofocus on the first field.
Close path tk-dialog tk-confirm-dialog
Esc closes closes, as Cancel
close button closes —
backdrop click closes, unless the dialog holds a <form> does nothing
Cancel closes; you wire it closes
main action you wire it closes with returnValue "confirm"
  • Closing never saves anything. Every way out except the main action leaves returnValue as "" in tk-dialog and "cancel" in tk-confirm-dialog.
  • persistent turns off Esc and the backdrop. The close button and the dialog's buttons still work. Keep it for rare cases, such as accepting legal terms.
  • Open at most one confirm over one dialog. Esc closes the top one.

Unsaved changes

Esc and a backdrop click fire a cancel event first, and preventDefault() keeps the dialog open. Use it to ask before typed work is lost. The close button and Cancel don't fire cancel.

<tk-confirm-dialog id="discard" heading="Discard changes?" message="Your edits will be lost." cancel-label="Continue editing" confirm-label="Discard changes" danger></tk-confirm-dialog>
<script>
  const dialog = document.getElementById('rename');
  const discard = document.getElementById('discard');
  dialog.addEventListener('cancel', (event) => {
    if (!hasUnsavedChanges()) return;
    event.preventDefault();
    discard.show();
  });
  discard.addEventListener('close', () => {
    if (discard.returnValue === 'confirm') dialog.close();
  });
</script>

Waiting for the server

preventDefault() on the confirm button's click keeps tk-confirm-dialog open while your request runs.

const deleteDialog = document.getElementById('delete');
deleteDialog.addEventListener('click', async (event) => {
  const button = event.composedPath().find((node) => node.dataset?.action === 'confirm');
  if (!button) return;
  event.preventDefault();
  button.loading = true;
  await deleteProject();
  button.loading = false;
  deleteDialog.close('confirm');
});