Core Toggle
@nrk/core-toggle
makes a <button>
toggle the visibility of next element sibling. Toggles can be nested and easily extended with custom animations or behavior through the toggle event.
Example
<button type="button">Popup VanillaJS</button>
<core-toggle class="my-dropdown" data-popup hidden>
<ul>
<li><a>Link</a></li>
<li>
<button type="button">Can also be nested</button>
<core-toggle class="my-dropdown" data-popup hidden>
<ul>
<li><a>Sub-link</a></li>
<li><input type="text" autofocus aria-label="Skriv her"></li>
</ul>
</core-toggle>
</li>
</ul>
</core-toggle>
<div id="jsx-toggle-popup"></div>
<script type="text/javascript">
ReactDOM.render(<>
<button type="button">Popup JSX</button>
<CoreToggle className='my-dropdown' hidden data-popup onToggleSelect={console.warn}>
<ul>
<li><button type="button">Select</button></li>
<li><a href='#'>Link</a></li>
<li>
<button type="button">Can also be nested</button>
<CoreToggle className='my-dropdown' hidden data-popup>
<ul>
<li><a href='#'>Sub-link</a></li>
</ul>
</CoreToggle>
</li>
</ul>
</CoreToggle>
</>, document.getElementById('jsx-toggle-popup'))
</script>
Installation
Using NPM provides own element namespace and extensibility.
Recommended:
npm install @nrk/core-toggle
Using static registers the custom element with default name automatically:
<script src="https://static.nrk.no/core-components/major/10/core-toggle/core-toggle.min.js"></script>
Remember to polyfill custom elements if needed.
Usage
HTML / JavaScript
<button type="button">Toggle VanillaJS</button>
<core-toggle
hidden <!-- Set hidden attribute to prevent FOUC -->
data-popup="{String?}"
>
Content
</core-toggle>
import CoreToggle from '@nrk/core-toggle'
window.customElements.define('core-toggle', CoreToggle)
const myToggle = document.querySelector('core-toggle')
myToggle.button
myToggle.popup
myToggle.hidden
myToggle.value
myToggle.popup = {Boolean|String}
myToggle.hidden = true
myToggle.value = 'Velg'
React / Preact
import CoreToggle from '@nrk/core-toggle/jsx'
<button type="button">Use with JSX</button>
<CoreToggle
hidden // Set hidden attribute to prevent FOUC
data-popup={Boolean|String} // Optional. If true, clicking outside open toggle will close it. Providing a string also enables select-behavior, by replacing value inside button with selected value, and suffixes provided string to aria-label on button
ref={(comp) => {}} // Optional. Get reference to React component
forwardRef={(el) => {}} // Optional. Get reference to underlying DOM custom element
value={String} // Optional. Set selected value in order to get correct aria-label. See "Example: Select"
onToggle={Function} // Optional. Toggle event listener. See event 'toggle'
onToggleSelect={Function} // Optional. Toggle select event listener. See event 'toggle.select'
>
Content // Content to be toggled. Accepts text, elements and components
</CoreToggle>
Markup
With for
Putting the toggle button directly before the content is highly recommended, as this fulfills all accessibility requirements by default. There might be scenarios though, where styling makes this DOM structure impractical. In such cases, give the <button>
a data-for
attribute (for
is deprecated), and the <core-toggle>
an id
with corresponding value. Make sure there is no text between the button and toggle content, as this will break the experience for screen reader users:
<div>
<button data-for="my-toggle" type="button">Toggle</button>
</div>
<core-toggle id="my-toggle" hidden>...</core-toggle>
Popup and button HTML
Using the data-popup
attribute in conjunction with embedded HTML in your toggle button (an SVG icon for instance) will only preserve text when updating the value/label for the button. To preserve the embedded HTML, put the actual button text inside a <span>
:
<button type="button">
<span>Toggle</span>
<svg style="width:1.5em; height:1.5em" aria-hidden="true"><use xlink:href="#nrk-heart"></use></svg>
</button>
<core-toggle data-popup="..." hidden>...</core-toggle>
Autofocus
If you have form elements inside a <core-toggle>
, you can optionally add a autofocus
attribute to the most prominent form element. This helps the user navigate quickly when toggle is opened.
Autoposition
When using core-toggle near the screen edges, the autoposition
attribute positions the toggled content where there is visual room around the toggle, using position:fixed
, relative to the screen edges.
This can be useful when core-toggle is used inside narrow and/or scrollable areas.
<p>This example illustrates how core-toggle with <code style="font-size: 16px">autoposition</code> draws relative to the screen edge marked by the red frame</p>
<iframe title="Examples for use of autoposition" src="core-toggle/autoposition.html" width="300" height="400" style="border: 2px solid red;"></iframe>
Events
toggle
Fired after open state changes:
document.addEventListener('toggle', (event) => {
event.target
})
toggle.select
Fired whenever an <a>
or <button>
element is selected inside a toggle with the data-popup
option enabled.
Useful for setting the value of the toggle button with the selected value.
document.addEventListener('toggle.select', (event) => {
event.target
event.detail
event.target.value = event.detail
})
Styling
Note: <core-toggle>
is display: inline
by default. Change this by for instance setting core-toggle:not([hidden]) { display: block | flex | grid }
or similar in your app. Not needed when position
or float
is used. All styling in documentation is example only. Both the <button>
and <core-toggle>
element receive attributes reflecting the current toggle state:
.my-button {}
.my-button[aria-expanded="true"] {}
.my-button[aria-expanded="false"] {}
.my-toggle-content {}
.my-toggle-content:not([hidden]) {}
.my-toggle-content[hidden] {}
Example: Expand
Content is only toggled when clicking the button. Great for accordions and expand/collapse panels.
<button type="button">Toggle VanillaJS</button>
<core-toggle hidden>Content</core-toggle>
<div id="jsx-toggle-default"></div>
<script type="text/jsx">
ReactDOM.render(<>
<button type="button">Toggle JSX</button>
<CoreToggle hidden onToggle={console.log}>Content</CoreToggle>
</>, document.getElementById('jsx-toggle-default'))
</script>
Example: Select
data-popup
-attribute is required for Select behavior
Listen to the toggle.select
event and update the button's value from the selected item
to create a component that behaves like a <select>
:
HTML and JavaScript
<button type="button">Episode 1</button>
<core-toggle class="my-select my-dropdown" hidden data-popup="Choose episode">
<ul>
<li><button type="button">Episode 1</button></li>
<li><button type="button">Episode 2</button></li>
<li><button type="button">Episode 3</button></li>
</ul>
</core-toggle>
<script>
document.addEventListener('toggle.select', (event) => {
if (!event.target.classList.contains('my-select')) return
event.target.value = event.detail
event.target.hidden = true
event.target.button.focus()
})
</script>
React class component
<div id="jsx-toggle-select"></div>
<script type="text/javascript">
class MyToggleSelect extends React.Component {
constructor (props) {
super(props)
this.state = { value: 'Episode 1' }
this.onSelect = this.onSelect.bind(this)
}
onSelect (event) {
event.target.hidden = true
this.setState({ value: event.detail.textContent })
}
render () {
return <>
<button type="button">{this.state.value}</button>
<CoreToggle className='my-dropdown' data-popup='Choose episode' hidden onToggleSelect={this.onSelect} value={this.state.value}>
<ul>
<li><button type="button">Episode 1</button></li>
<li><button type="button">Episode 2</button></li>
<li><button type="button">Episode 3</button></li>
</ul>
</CoreToggle>
</>
}
}
ReactDOM.render(<MyToggleSelect/>, document.getElementById('jsx-toggle-select'))
</script>
React function component
<div id="react-func-toggle-select"></div>
<script type="text/javascript">
const FuncToggle = () => {
const [selectVal, setSelectVal] = React.useState('Episode 1')
const handleSelect = (event) => {
event.target.hidden = true
setSelectVal(event.detail.textContent)
}
return (
<>
<button type="button">{selectVal}</button>
<CoreToggle
className='my-dropdown'
data-popup='Choose episode'
onToggleSelect={handleSelect}
value={selectVal}
hidden
>
<ul>
<li><button type="button">Episode 1</button></li>
<li><button type="button">Episode 2</button></li>
<li><button type="button">Episode 3</button></li>
</ul>
</CoreToggle>
</>
)
}
ReactDOM.render(<FuncToggle/>, document.getElementById('react-func-toggle-select'))
</script>
FAQ
Why not use <details>
instead?
Despite having a native <details>
element for expanding/collapsing content, there are several issues regarding browser support, styling, accessibility. Furthermore, polyfills often conflict with other standards such as <dialog>
.
The menu role is mainly inteded for context menues and toolbars in application interfaces, and has quite complex keyboard navigation requirements. As most end users will not expect application behavior in websites and internal web based systems, (implemented) attributes like aria-controls
and aria-labelledby
is sufficient for a good user experience.
Why does dropdowns not open on hover?
Both touch devices and screen readers will have trouble properly interacting with hoverable interfaces (unless more complex fallback logic is implemented). To achieve a consistent and accessible interface, <core-toggle>
is designed around click interactions.
Why is there no group-option to achieve a single open toggle?
Some expand/collapse interfaces like accordions behaves like a group - allowing only one expanded area at the time. This pattern however requires more logic and carefully designed animations to avoid confusion over expected scroll position.
Example: The user first opens "Toggle-1", and then "Toggle-2" (which closes "Toggle-1"). Since "Toggle-1" is placed above, the position "Toggle-2" now changes - potentially outside the viewport on smaller devices.
Note: If you do need to implement grouping, you can achieve this by reacting to the toggle event.