Core Dialog
@nrk/core-dialog
is an elevated element with which the user interacts with to perform some task or decision.
It supports nestability, keyboard navigation containment and restoring focus when dialog is closed.
Examples (plain JS)
Nested dialog
<button data-for="my-dialog" type="button">Open dialog</button>
<core-dialog id="my-dialog" class="my-dialog" aria-label="first dialog title" hidden>
<h1>Dialog title</h1>
<p>Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.</p>
<label>
<small>Label for autofocused input</small>
<input type="text" autofocus placeholder="Input with autofocus">
</label>
<br>
<br>
<button data-for="close" type="button">Close</button>
<button data-for="my-dialog-nested" type="button">Open an additional dialog</button>
<core-dialog id="my-dialog-nested" class="my-dialog" aria-label="other dialog title" hidden>
<h1>Another dialog, triggered inside the first dialog</h1>
<p>Nunc mi felis, condimentum quis hendrerit sed, porta eget libero.</p>
<button data-for="close" type="button">Close</button>
</core-dialog>
</core-dialog>
Strict dialog
<button data-for="strict-dialog">Open strict dialog</button>
<core-dialog id="strict-dialog" class="my-dialog" aria-label="strict dialog title" hidden strict>
<h1>Strict dialog title</h1>
<p>Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.</p>
<button type="button">This button does nothing</button>
<button data-for="close" type="button">Close</button>
</core-dialog>
Modal dialog
<button data-for="modal-dialog">Open dialog without backdrop</button>
<core-dialog id="modal-dialog" class="my-dialog" aria-label="modal dialog title" hidden backdrop="off">
<h1>Dialog without backdrop</h1>
<p>Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.</p>
<button data-for="close">Close</button>
</core-dialog>
Custom backdrop
<button data-for="modal-custom">Open dialog with custom backdrop</button>
<core-dialog id="modal-custom" class="my-dialog" aria-label="modal dialog title" hidden backdrop="back-custom">
<h1>Dialog title</h1>
<p>Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.</p>
<button data-for="close">Close</button>
</core-dialog>
<div id="back-custom" class="my-backdrop" style="background:rgba(0,0,50,.8)" hidden></div>
Preventing overscroll
Prevent scrolling the background when scrolling inside a scrollable element inside the dialog by setting overscroll-behavior: contain
on the dialog-element.
Note that overscroll-behavior
is only supported through enabling experimental features in Safari and Safari on iOS (caniuse).
Read this if you want a primer on how overscroll-behavior
works.
<button data-for="overscroll-dialog">Open scrollable dialog</button>
<core-dialog id="overscroll-dialog" class="my-dialog" aria-label="Scrollable dialog title" hidden style="overscroll-behavior: contain">
<h1>Scrollable dialog title</h1>
<p style="max-height: 100px;overflow: scroll;border: 1px solid gray;">
Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.
</p>
<p style="max-height: 100px;overflow: scroll;border: 1px solid gray;">
Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.
</p>
<button data-for="close" type="button">Close</button>
</core-dialog>
Examples (React)
Class component
<div id="jsx-dialog"></div>
<script type="text/javascript">
class DialogContainerDemo extends React.Component {
constructor (props) {
super(props)
this.state = { hidden: true }
this.toggleDialog = this.toggleDialog.bind(this)
this.handleToggle = this.handleToggle.bind(this)
}
toggleDialog () {
this.setState({ hidden: !this.state.hidden })
}
handleToggle (event) {
this.setState({ hidden: event.target.hidden })
}
render () {
return (
<>
<button onClick={this.toggleDialog} type="button">Open React dialog</button>
<CoreDialog
className="my-dialog"
hidden={this.state.hidden}
onDialogToggle={this.handleToggle}
aria-label="React dialog"
>
<h1>Dialog for JSX</h1>
<p>
Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.
</p>
<button onClick={this.toggleDialog} type="button">Lukk</button>
</CoreDialog>
</>
)
}
}
ReactDOM.render(<DialogContainerDemo />, document.getElementById('jsx-dialog'))
</script>
Strict dialog
<div id="jsx-dialog-strict"></div>
<script type="text/javascript">
ReactDOM.render(
<>
<button data-for="dialog-strict-jsx" type="button">Open strict React dialog</button>
<CoreDialog
id="dialog-strict-jsx"
className="my-dialog"
aria-label="Strict React dialog"
hidden
strict
>
<h1>Strict dialog for JSX</h1>
<p>
Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.
</p>
<button data-for="close" type="button">Lukk</button>
</CoreDialog>
</>,
document.getElementById('jsx-dialog-strict')
)
</script>
No backdrop
<div id="jsx-dialog-no-backdrop"></div>
<script type="text/javascript">
ReactDOM.render(
<>
<button data-for="dialog-no-back-jsx" type="button">Open React dialog without backdrop</button>
<CoreDialog
id="dialog-no-back-jsx"
className="my-dialog"
aria-label="React dialog without backdrop"
backdrop="off"
hidden
>
<h1>React dialog without backdrop</h1>
<p>
Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.
</p>
<button data-for="close" type="button">Lukk</button>
</CoreDialog>
</>,
document.getElementById('jsx-dialog-no-backdrop')
)
</script>
Custom backdrop
NB! Do not wrap CoreDialog
with custom backdrop as direct children of React.Fragments (instead, wrap in a block element like <div>
), to ensure access to the backdrop element on mount.
<div id="jsx-dialog-custom"></div>
<script type="text/javascript">
ReactDOM.render(
<div>
<button data-for="dialog-cust-jsx" type="button">Open React dialog with custom backdrop</button>
<CoreDialog
id="dialog-cust-jsx"
className="my-dialog"
aria-label="React dialog with custom backdrop"
backdrop="custom-backdrop-jsx"
hidden
>
<h1>React dialog with custom backdrop</h1>
<p>
Nunc mi felis, condimentum quis hendrerit sed, porta eget libero. Aenean scelerisque ex eu nisi varius hendrerit. Suspendisse elementum quis massa at vehicula. Nulla lacinia mi pulvinar, venenatis nisi ut, commodo quam. Praesent egestas mi sit amet quam porttitor, mollis mattis mi rhoncus.
</p>
<button data-for="close" type="button">Lukk</button>
</CoreDialog>
<div
id="custom-backdrop-jsx"
className="my-backdrop"
style={{background:'rgba(0,0,50,.8)'}}
hidden
></div>
</div>,
document.getElementById('jsx-dialog-custom')
)
</script>
Installation
Using NPM provides own element namespace and extensibility.
Recommended:
npm install @nrk/core-dialog
Using static registers the custom element with default name automatically:
<script src="https://static.nrk.no/core-components/major/9/core-dialog/core-dialog.min.js"></script>
Remember to polyfill custom elements if needed.
Usage
HTML / JavaScript
<button data-for="my-dialog">Open</button>
<core-dialog id="my-dialog"
hidden <!-- Hide dialog by default -->
strict
backdrop="{on|off|String}"
aria-label="{String}">
<h1>Title of dialog</h1>
<p>Some content</p>
<button data-for="close">Close dialog</button>
</core-dialog>
import CoreDialog from '@nrk/core-dialog'
window.customElements.define('core-dialog', CoreDialog)
const myDialog = document.querySelector('core-dialog')
myDialog.hidden
myDialog.strict
myDialog.backdrop
myDialog.hidden = false
myDialog.strict = false
myDialog.backdrop = 'on' | 'off' | 'my-drop'
myDialog.style.zIndex = '9'
myDialog.close()
myDialog.show()
React / Preact
import CoreDialog from '@nrk/core-dialog/jsx'
<button data-for="my-dialog">Open</button> // Opens dialog with id="my-dialog"
<CoreDialog id="my-dialog"
hidden
strict
backdrop={Boolean|String}
aria-label={String}
ref={(comp) => {}}
forwardRef={(el) => {}}
onDialogToggle={Function}>
<h1>My React/Preact dialog</h1>
<p>Some content</p>
<button data-for="close"></button>
</CoreDialog>
Markup
Required focusable element
Your dialog must contain a tabbable element (e.g. visible <input>
, <button>
, <select>
, <textarea>
, <a>
, <summary>
, <audio>
, <video>
, <iframe>
, <area>
or with the contenteditable
or draggable
attributes) or a focusable element (with tabindex="-1"
) to ensure the users focus is navigated into the <core-dialog>
.
As a best practice; if your dialog contains a form element, use autofocus
.
If you dialog is without form elements, start your dialog
content with <h1 tabindex="-1">Dialog title</h1>
.
Elements order
Though not strictly required, the <button>
opening a dialog should be placed directly before the <core-dialog>
itself. This eases the mental model for screen reader users. Othewise, use <button data-for="my-dialog-id"></button>
.
Backdrop
core-dialog
automatically creates a <backdrop>
element as next adjacent sibling if needed. If the backdrop
attribute is set to an id
(something else than true|false
), the element with the corresponding ID will be used as backdrop. Note that a backdrop is needed to enable click-outside-to-close. Custom backdrop example:
<core-dialog backdrop="my-backdrop"></core-dialog>
<div id="my-backdrop"></div>
Stacking
To manually control z-index of dialogs (and their corresponding backdrop element, set z-index from either HTML, CSS or JS. When set, the dialog will not try to place itself automatically over the topmost dialog and you are responsible for stacking order.
For example:
<style>
.my-dialog { z-index: 100 }
.my-backdrop { z-index: 90 }
</style>
<script>
myDialog.style.zIndex = 100
myBackdrop.style.zIndex = 90
</script>
<core-dialog style="z-index: 100" hidden>...</core-dialog>
<backdrop style="z-index: 90" hidden>...</backdrop>
Events
dialog.toggle
Fired when a dialog is toggled:
document.addEventListener('dialog.toggle', (event) => {
event.target
})
Styling
.my-dialog {}
.my-dialog[hidden] {}
.my-dialog:not([hidden]) {}
.my-dialog + backdrop {}
.my-dialog + backdrop[hidden] {}
Note: There is a z-index limit for the backdrop at 2000000000. Do not use higher z-index values in your site in order for core-dialog
to work properly. The limit exists because some browser extensions, like ghostery have absurdly high z-indexes. The issue is further explained here.