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
<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>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
<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>Its backtests and alerts are deleted too. This can't be undone.
<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().
const confirm = document.querySelector('#delete-dialog')
if (await confirm.ask()) deleteStrategy(id) // true only if “Delete strategy” was pressedAPI
<tai-dialog>
| Attribute | Property | Type | Description |
|---|---|---|---|
open | open | boolean | Shows the dialog as a modal. Reflects; React can drive it directly. |
label | — | string | Accessible name when there's no heading slot. |
close-label | — | string | Name of the close button. Default “Close”. |
size | — | sm | md | lg | Maximum width: 24, 32 (default) or 44rem. |
light-dismiss | — | boolean | A click on the backdrop closes it. Off by default — forms lose work too easily. |
persistent | — | boolean | Escape, the close button and the backdrop can't close it. Use sparingly. |
| — | returnValue | string | Value passed to close(), also in tai-close. |
| Name | Kind | Description |
|---|---|---|
show() | method | Opens as a modal and remembers what had focus. |
close(value?) | method | Closes; focus returns to the opener. |
tai-open | event | After opening. |
tai-cancel | event, cancellable | Before a dismiss. detail.source: escape, close-button, backdrop. Call preventDefault() to keep it open (e.g. unsaved changes). |
tai-close | event | After closing. detail.returnValue. |
| Name | Kind | Description |
|---|---|---|
heading | slot | Title, rendered inside an h2; also the dialog's accessible name. |
| (default) | slot | Body content. |
footer | slot | Actions. Hidden when empty. |
dialog, panel, header, heading, close-button, body, footer | parts | Style with tai-dialog::part(panel) { … }. |
--tai-dialog-width | custom property | Maximum width. |
<tai-confirm-dialog>
Same open, persistent, show(), close(), tai-cancel and tai-close as above, plus:
| Name | Kind | Description |
|---|---|---|
tone | attribute | danger: red confirm button, warning icon, and Cancel gets initial focus. |
confirm-label, cancel-label | attributes | Button text. Name the action (“Delete strategy”), not “OK”. |
ask() | method | Opens; resolves true on confirm, false on cancel or Escape. |
tai-confirm | event | The user confirmed. Then tai-close with returnValue: 'confirm'. |
heading / default | slots | The question, and the summary the screen reader reads with it. |
icon, actions, cancel-button, confirm-button | parts | Plus dialog, panel, heading, body. |
Keyboard
| Key | Action |
|---|---|
| Tab / Shift Tab | Moves between controls inside the dialog. The page behind is inert, so focus never lands there. |
| Escape | Closes (fires tai-cancel first; cancel it to keep the dialog open). For a confirm dialog, Escape means Cancel. |
| Enter / Space | Activate the focused button. In a danger confirm, focus starts on Cancel, so a reflex Enter is safe. |
Accessibility notes
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.
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
alertdialogwitharia-describedbypointing 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 nativecloseevent, 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-dismissfor 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.
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>
</>
)
}