An accessible dialog component for Svelte apps. Demo.
npm install svelte-accessible-dialog
<script>
import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';
let isOpen;
const open = () => {
isOpen = true;
};
const close = () => {
isOpen = false;
};
</script>
<button on:click={open}>Open Dialog</button>
<DialogOverlay {isOpen} onDismiss={close}>
<DialogContent aria-label="Announcement">
<button on:click={close}>Close</button>
<p>I am a dialog</p>
</DialogContent>
</DialogOverlay>
By default, the first focusable element will receive focus when the dialog opens, but you can provide an element to focus instead.
<script>
import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';
let isOpen;
let initialFocusElement;
const open = () => {
isOpen = true;
};
const close = () => {
isOpen = false;
};
</script>
<button on:click={open}>Open Dialog</button>
<DialogOverlay {isOpen} {initialFocusElement} onDismiss={close}>
<DialogContent aria-label="Announcement">
<button on:click={close}>Close</button>
<label>
Name: <input type="text" bind:this={initialFocusElement} />
</label>
<p>I am a dialog</p>
</DialogContent>
</DialogOverlay>
By default, the element that invoked the dialog will receive focus when the dialog closes, but you can provide an element to focus instead.
See the WAI-ARIA authoring practices for more detail about when you might want to do this.
<script>
import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';
let isOpen;
let returnFocusElement;
const open = () => {
isOpen = true;
};
const close = () => {
isOpen = false;
};
</script>
<button on:click={open}>Open Dialog</button>
<button bind:this={returnFocusElement}>I focus on close</button>
<DialogOverlay {isOpen} {returnFocusElement} onDismiss={close}>
<DialogContent aria-label="Announcement">
<button on:click={close}>Close</button>
<p>I am a dialog</p>
</DialogContent>
</DialogOverlay>
DialogContent
has the aria-modal
attribute. This indicates to screen readers that only content contained within the dialog should be accessible to the user. Modern screen readers respect this attribute, but you can enable a legacy workaround if you require deeper support.
See the WAI-ARIA authoring practices for more detail.
<script>
import { DialogOverlay, DialogContent } from 'svelte-accessible-dialog';
let isOpen;
const open = () => {
isOpen = true;
};
const close = () => {
isOpen = false;
};
</script>
<button on:click={open}>Open Dialog</button>
<DialogOverlay {isOpen} ariaModalLegacy={true} onDismiss={close}>
<DialogContent aria-label="Announcement">
<button on:click={close}>Close</button>
<p>I am a dialog</p>
</DialogContent>
</DialogOverlay>
<style>
:global([data-svelte-dialog-overlay].overlay) {
z-index: 10;
}
:global([data-svelte-dialog-content].content) {
border: 2px solid #000;
}
</style>
<DialogOverlay class="overlay">
<DialogContent class="content">
<p>I am a dialog</p>
</DialogContent>
</DialogOverlay>
<DialogOverlay style="z-index: 10">
<DialogContent style="border: 2px solid #000">
<p>I am a dialog</p>
</DialogContent>
</DialogOverlay>
Must render DialogContent
. Any props not listed below will be spread onto the underlying div
.
Prop | Type | Required | Description |
---|---|---|---|
isOpen |
Boolean | Yes | Controls whether the dialog is open or not. |
onDismiss |
() => void | Yes | This function is called whenever the user hits "Escape" or clicks outside the dialog. The dialog must be closed on onDismiss . |
initialFocusElement |
HTMLElement | No | The element that will receive focus when the dialog is open. Defaults to the first focusable element. |
returnFocusElement |
HTMLElement | No | The element that will receive focus when the dialog closes. Defaults to the element that invoked the dialog. |
ariaModalLegacy |
Boolean | No | Enables a fallback for the aria-modal attribute. When true , all content outside of the active dialog will have the aria-hidden and inert attributes set to "true" . |
Must be a child of DialogOverlay
. Element props will be spread onto the underlying div
.
WAI-ARIA: https://www.w3.org/TR/wai-aria-practices-1.2/#dialog_modal
key | action |
---|---|
Escape |
Dismisses the dialog |
It's recommended to have at least one tabbable element in the DialogContent
. Ideally, the first element in the dialog should be a close button. If no tabbable elements are found, the dialog content element itself will receive focus.
Until fairly recently, keeping a screen reader within an active dialog was difficult. A focus trap prevents focus from leaving a dialog, but does nothing to stop a wandering virtual cursor. A common solution to this problem was to set the aria-hidden
and inert
attributes on all elements outside of the active dialog.
ARIA 1.1 introduced the aria-modal
attribute. aria-modal
indicates to screen readers that only content contained within a dialog with aria-modal="true"
should be accessible to the user. Modern screen readers respect this attribute, so svelte-accessible-dialog
does not implement the legacy workaround by default.
If support for aria-modal
is inadequate for your app, you can pass ariaModalLegacy={true}
to DialogOverlay
to enable the legacy workaround.
A dialog needs to be properly labelled to provide context for users that rely on assistive technology. If a dialog is announced to the user without a label, it can be confusing and difficult to navigate.
There are two general approaches to labelling: aria-label
and aria-labelledby
. If the text is visible on the screen, you should provide the label's HTML element with a unique id
attribute. That id
is then given to an aria-labelledby
attribute on the DialogContent
. With this context, the screen reader will announce whatever text is nested inside that ID'd markup as the title for the Dialog.
If a design doesn't include a visible label on the screen, you need to provide an aria-label
prop on the DialogContent
instead.
<DialogContent aria-label="Cookie notice">
<p>We use cookies to improve your website experience</p>
<button>Not interested</button>
<button>Ok, thanks</button>
</DialogContent>
<DialogContent aria-labelledby="cookie-dialog-title">
<h2 id="cookie-dialog-title">Cookie Notice</h2>
<p>We use cookies to improve your website experience</p>
<button>Not interested</button>
<button>Ok, thanks</button>
</DialogContent>
DialogOverlay
does not set a z-index
. It depends on DOM order to be on top of the page content (it's inserted at the end of the document when it's opened). If you're fighting z-index
wars, make sure to add a z-index
to DialogOverlay
.
If you're using webpack with svelte-loader, make sure to add "svelte"
to resolve.mainFields
in your webpack config. This ensures that webpack imports the uncompiled components (src/index.js
) rather than the compiled version (dist/index.mjs
), which is more efficient.
If you're using Rollup with rollup-plugin-svelte, this will happen automatically.
Tests use Jest and svelte-testing-library.
git clone [email protected]:reecelucas/svelte-accessible-dialog.git
cd svelte-accessible-dialog
yarn
yarn test