- ✅ Accessibility first — Focus trap[1] + keyboard navigation + aria-attributes
- ✅ Fully controlled component
- ✅ Pure vue, no wrapping.
- ✅ Simplicity + size
- 🕸 Nested dialogs (questionable pattern, not recommended, but possible because it happens) and it's actually in WAI-ARIA examples so...
- 🚧 renderless version
Detailed documentation and additional info is available at documentation site
npm i a11y-vue-dialog
# or
yarn add a11y-vue-dialog
// portal vue is binstalled as dependency, and it's required
import PortalVue from "portal-vue";
Vue.use(PortalVue);
// add the component
import A11yVueDialog from "a11y-vue-dialog";
// if you want to register globally
Vue.use(A11yVueDialog);
<a11y-vue-dialog :open="true">
<p>This slot content will be rendered wherever the <portal-target name="a11y-vue-dialogs">
is located. (we adivse at the bottom of your root component)</p>
</a11y-vue-dialog>
<portal-target name="a11y-vue-dialogs" multiple />
A renderless version provides all the functionality required to build a proper Dialog
, but gives zero f*cks about your markup and styles. The default scopedSlot
props help you bind the accessibility attributes and event listeners to your markup elements, but semantics and styling layer it's now the consumer full responsibility.
Each
ref
suffixed slotProp is an object that contains a "props" and "listeners" keys to be attached to elements viav-bind
andv-on
respectively
slotProp | type | desc |
---|---|---|
open | Boolean | prop forwarding for portal v-if |
close | Function | method forwarding for closing the dialog |
backdropRef | Object | for the backdrop element |
dialogRef | Object | for the main dialog element |
closeRef | Object | for attaching close buttons/actions |
titleRef | Object | For attaching dialog title, accessibility |
focusRef | Object | For cherry-picking the first focusable element on open |
<!-- compose into you own markup, MyDialog.vue -->
<template>
<portal to="a11y-vue-dialogs" v-if="open">
<a11y-vue-dialog-renderless
v-bind="$props"
v-bind="$listeners"
#default="{ open, closeFn, backdropRef, dialogRef, titleRef, closeRef, focusRef }"
>
<div class="youclasses"
v-bind="backdropRef.props"
v-on="backdropRef.listeners"
>
<div
class="youclasses__element"
v-bind="dialogRef.props"
v-on="dialogRef.listeners"
>
<h1 v-bind="titleRef.props">Title</h1>
<button
v-bind="closeRef.props"
v-on="closeRef.listeners"
>
x
</button>
<section>
<!-- autofocus would also work on this case, but not every focusable element supports it -->
<input
type="text"
placeholder="I will get focused first because i'm the focus ref"
v-bind="focusRef.props"
/>
<slot />
</section>
<footer>
<button @click="closeFn">Cancel</button>
<button @click="emit('confirm')">Confirm</button>
</footer>
</div>
</div>
</a11y-vue-dialog-renderless>
</portal>
</template>
<script>
import { A11yVueDialogRenderless } from "a11y-vue-dialog";
import { Portal } from "portal-vue";
export default {
name: 'MyDialog',
components: {
A11yVueDialogRenderless,
Portal
},
extends: { A11yVueDialogRenderless },
props: ['open', 'role'],
}
</script>
<!-- page.vue -->
<template>
<div id="page">
<button @click="openMyModal = true">
<my-dialog
open="openMyModal"
@close="openMyModal = false"
@confirm="handSubmit"
/>
My markup, my rules.
</my-dialog>
</div>
</template>
- Here's a codesandbox to play with
- Checkout this example for what's the minimum expected markup for an accessible dialog
When you use a as the root element of the portal and then remove the portal (i.e. with v-if) or set its disabled prop to true, no leave transition will happen. While this is to expected, as the same thing would happen if you removed a div that contains a , it often trips people up, which is why it's mentioned here. vue-simple-portal
if you really need to apply the v-if
to portal, check the example in the link above
But based on the info above, this also works fine:
<portal to="a11y-vue-dialogs">
<!--
[1] note the v-if is applied to transition not portal.
could also be applied to the component itself
-->
<transition name="fade" appear v-if="open">
<a11y-vue-dialog-renderless
v-bind="$props"
v-bind="$listeners"
#default="{ open, closeFn, backdropRef, dialogRef, titleRef, closeRef, focusRef }"
>
<!-- your implementation -->
</a11y-vue-dialog-renderless>
</transition>
</portal>
A playground is used to test the component locally. It uses vue/cli
instant prototyping feature, so the downside is that you have to install it globally.
- Then, just run
yarn play
Thanks to all this packages for inspiration and guidance.
- Since
v0.5.0
focus trap is powered by the awesomefocus-trap
— go and give them some ✨ - portal-vue from @LinusBorg wich makes escape overflow traps like breeze
- a11y-dialog (vanilla) from @HugoGiraudel to lead the path that ended here
- vue-a11y-dialog (wrapper around ^) from @morkro for the motivation to build a pure vue alternative to it.
- vue-js-dialog a fully fledge massive dialog
MIT © Renato de Leão