tai-ui TradeAIAlgo UI library
Theme LightDarkSystem

Component

Dialog

A modal for a focused task, and a confirmation step before anything consequential. Built on the native <dialog> element, with the focus handling the platform still leaves to you.

  • 2.1.1 Keyboard
  • 2.1.2 No keyboard trap
  • 2.4.3 Focus order
  • 2.4.7 Focus visible
  • 1.4.11 Non-text contrast
  • 4.1.2 Name, role, value

Examples

Form in a dialog
Price alert for NIFTY 50
HTML

<button type="button" class="btn" id="open-alert">Set price alert</button>
<tai-dialog id="alert-dialog">
  <span slot="heading">Price alert for NIFTY 50</span>
  <form id="alert-form">
    <div class="field">
      <label for="alert-level">Alert me when the price crosses</label>
      <input id="alert-level" name="level" inputmode="decimal" value="24,250" autocomplete="off">
    </div>
    <div class="field">
      <label for="alert-channel">Send it by</label>
      <select id="alert-channel" name="channel"><option>Push notification</option><option>Email</option></select>
    </div>
  </form>
  <button slot="footer" type="button" class="btn" data-close>Cancel</button>
  <button slot="footer" type="submit" form="alert-form" class="btn btn--primary">Save alert</button>
</tai-dialog>
<div class="log" role="log" aria-label="Dialog events" id="log-alert"></div>
Payment confirmation (banking)
Send £2,500.00 to J. Okafor?

Payments to a new payee can't be recalled once sent.

To
J. Okafor · 20-45-77 · ••••4821
From
Current account ••••1093
Arrives
Within 2 hours
HTML

<button type="button" class="btn btn--primary" id="open-pay">Send £2,500.00</button>
<tai-confirm-dialog id="pay-dialog" confirm-label="Send payment" cancel-label="Go back">
  <span slot="heading">Send £2,500.00 to J. Okafor?</span>
  <p>Payments to a new payee can't be recalled once sent.</p>
  <dl class="summary">
    <dt>To</dt><dd>J. Okafor · 20-45-77 · ••••4821</dd>
    <dt>From</dt><dd>Current account ••••1093</dd>
    <dt>Arrives</dt><dd>Within 2 hours</dd>
  </dl>
</tai-confirm-dialog>
<div class="log" role="log" aria-label="Payment dialog events" id="log-pay"></div>
Destructive action — Cancel is focused first
Delete “Opening range breakout”?

Its backtests and alerts are deleted too. This can't be undone.

HTML

<button type="button" class="btn" id="open-delete">Delete strategy</button>
<tai-confirm-dialog id="delete-dialog" tone="danger" confirm-label="Delete strategy" cancel-label="Keep it">
  <span slot="heading">Delete “Opening range breakout”?</span>
  <p>Its backtests and alerts are deleted too. This can't be undone.</p>
</tai-confirm-dialog>
<div class="log" role="log" aria-label="Delete dialog events" id="log-delete"></div>

Usage

Open with the open attribute or property, show(), or — for a yes/no question — await confirm.ask().

JS

const confirm = document.querySelector('#delete-dialog')
if (await confirm.ask()) deleteStrategy(id)   // true only if “Delete strategy” was pressed

API

<tai-dialog>

AttributePropertyTypeDescription
openopenbooleanShows the dialog as a modal. Reflects; React can drive it directly.
label—stringAccessible name when there's no heading slot.
close-label—stringName of the close button. Default “Close”.
size—sm | md | lgMaximum width: 24, 32 (default) or 44rem.
light-dismiss—booleanA click on the backdrop closes it. Off by default — forms lose work too easily.
persistent—booleanEscape, the close button and the backdrop can't close it. Use sparingly.
—returnValuestringValue passed to close(), also in tai-close.
NameKindDescription
show()methodOpens as a modal and remembers what had focus.
close(value?)methodCloses; focus returns to the opener.
tai-openeventAfter opening.
tai-cancelevent, cancellableBefore a dismiss. detail.source: escape, close-button, backdrop. Call preventDefault() to keep it open (e.g. unsaved changes).
tai-closeeventAfter closing. detail.returnValue.
NameKindDescription
headingslotTitle, rendered inside an h2; also the dialog's accessible name.
(default)slotBody content.
footerslotActions. Hidden when empty.
dialog, panel, header, heading, close-button, body, footerpartsStyle with tai-dialog::part(panel) { … }.
--tai-dialog-widthcustom propertyMaximum width.

<tai-confirm-dialog>

Same open, persistent, show(), close(), tai-cancel and tai-close as above, plus:

NameKindDescription
toneattributedanger: red confirm button, warning icon, and Cancel gets initial focus.
confirm-label, cancel-labelattributesButton text. Name the action (“Delete strategy”), not “OK”.
ask()methodOpens; resolves true on confirm, false on cancel or Escape.
tai-confirmeventThe user confirmed. Then tai-close with returnValue: 'confirm'.
heading / defaultslotsThe question, and the summary the screen reader reads with it.
icon, actions, cancel-button, confirm-buttonpartsPlus dialog, panel, heading, body.

Keyboard

KeyAction
Tab / Shift TabMoves between controls inside the dialog. The page behind is inert, so focus never lands there.
EscapeCloses (fires tai-cancel first; cancel it to keep the dialog open). For a confirm dialog, Escape means Cancel.
Enter / SpaceActivate the focused button. In a danger confirm, focus starts on Cancel, so a reflex Enter is safe.

Accessibility notes

Why the native <dialog> and not a role="dialog" div

showModal() gives the top layer (no z-index fights), makes everything behind it inert (focus and screen reader virtual cursors can't wander out), and handles Escape. A div needs all of that rebuilt — the audit found 14 hand-built overlays in the app; only one trapped focus.

Where focus goes first is a design decision

Content dialogs focus their heading, so a screen reader starts at the top instead of jumping to the first button. Confirmations focus the button you'd want on a reflex Enter: Confirm for benign actions, Cancel for tone="danger". The app's “Enable live trading” prompt used window.confirm(), where OK is the default — Enter turned on live trading.

  • Focus returns to the opener on every close path (Escape, buttons, programmatic). Browsers are converging on doing this natively; the component does it explicitly so behaviour is identical everywhere, including openers inside another shadow root.
  • The summary is part of the announcement. The confirm dialog is an alertdialog with aria-describedby pointing at the slotted summary, so the heading and the payment details are read together on open.
  • Escape can be cancelled (unsaved changes) through tai-cancel. Browsers deliberately limit this: pressing Escape twice without other interaction may force-close. The component's state follows the native close event, so it never gets out of sync.
  • Tab may reach the browser's own UI after the last control — that's native modal behaviour and not a keyboard trap failure; the page content stays unreachable.
  • Motion: the open animation only runs under prefers-reduced-motion: no-preference. Forced colours: borders switch to system colours.

Screen reader behaviour

Automated (Playwright, Chrome accessibility tree): the dialog exposes role dialog or alertdialog, its name comes from the heading slot, and the confirm dialog's description contains the summary text. Manual NVDA (Windows) and VoiceOver (macOS, iOS) passes are the next step; results will be recorded here.

Usage guidance

Do

  • Name buttons after the action: “Send payment”, “Delete strategy”.
  • Summarise exactly what will happen (amount, payee, account) in the confirm body.
  • Use tone="danger" for anything that can't be undone.

Don't

  • Use a dialog for information that could sit on the page.
  • Stack dialogs on dialogs.
  • Turn on light-dismiss for forms — a stray click loses the user's work.

Using it in React

React 19 passes open as a property and wires ontai-* props as event listeners — no wrapper component needed.

JS

import '@tradeaialgo/tai-ui'

function DeleteStrategy({ onDelete }) {
  const [open, setOpen] = useState(false)
  return (
    <>
      <button onClick={() => setOpen(true)}>Delete strategy</button>
      <tai-confirm-dialog open={open} tone="danger" confirm-label="Delete strategy"
        ontai-confirm={onDelete} ontai-close={() => setOpen(false)}>
        <span slot="heading">Delete this strategy?</span>
        <p>Its backtests and alerts are deleted too.</p>
      </tai-confirm-dialog>
    </>
  )
}