Skip to content

Instantly share code, notes, and snippets.

@amcgregor
Last active May 17, 2025 20:25
Show Gist options
  • Save amcgregor/756da2b3360ff39ddf11e6a9a553a4cf to your computer and use it in GitHub Desktop.
Save amcgregor/756da2b3360ff39ddf11e6a9a553a4cf to your computer and use it in GitHub Desktop.
Semantic Accessible Tabbed Interfaces #pen
<dl role=tablist>
<dt role=tab tabindex=0 aria-selected=true>First Tab
<dd role=tabpanel>
<p>
Aenean lacinia bibendum nulla sed consectetur. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec sed odio dui. Cras mattis consectetur purus sit amet fermentum. Nullam id dolor id nibh ultricies vehicula ut id elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Nullam quis risus eget urna mollis ornare vel eu leo.
<p>
<input id=input>
<p>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam.
<p>
Maecenas sed diam eget risus varius blandit sit amet non magna. Nulla vitae elit libero, a pharetra augue. Donec sed odio dui. Vestibulum id ligula porta felis euismod semper. Nulla vitae elit libero, a pharetra augue.
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras mattis consectetur purus sit amet fermentum. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.
</dd>
<dt role=tab tabindex=0>Second Tab
<dd role=tabpanel>
<p>
Maecenas sed diam eget risus varius blandit sit amet non magna. Nulla vitae elit libero, a pharetra augue. Donec sed odio dui. Vestibulum id ligula porta felis euismod semper. Nulla vitae elit libero, a pharetra augue.
<p>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam.
<p id=paragraph>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam.
<p>
Maecenas sed diam eget risus varius blandit sit amet non magna. Nulla vitae elit libero, a pharetra augue. Donec sed odio dui. Vestibulum id ligula porta felis euismod semper. Nulla vitae elit libero, a pharetra augue.
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras mattis consectetur purus sit amet fermentum. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.
<p>
Aenean lacinia bibendum nulla sed consectetur. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec sed odio dui. Cras mattis consectetur purus sit amet fermentum. Nullam id dolor id nibh ultricies vehicula ut id elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Nullam quis risus eget urna mollis ornare vel eu leo.
</dd>
</dl>
<p>Jump to the <a href="#input">first input</a>.</p>
<p>Jump to a <a href="#paragraph">paragraph on the second tab</a>.</p>
<dl role=tablist class=below>
<dt role=tab tabindex=0 aria-selected=true>First Tab
<dd role=tabpanel>
<p>
Aenean lacinia bibendum nulla sed consectetur. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec sed odio dui. Cras mattis consectetur purus sit amet fermentum. Nullam id dolor id nibh ultricies vehicula ut id elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Nullam quis risus eget urna mollis ornare vel eu leo.
<p>
<input>
<p>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam.
<p>
Maecenas sed diam eget risus varius blandit sit amet non magna. Nulla vitae elit libero, a pharetra augue. Donec sed odio dui. Vestibulum id ligula porta felis euismod semper. Nulla vitae elit libero, a pharetra augue.
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras mattis consectetur purus sit amet fermentum. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.
</dd>
<dt role=tab tabindex=0>Second Tab
<dd role=tabpanel>
<p>
Maecenas sed diam eget risus varius blandit sit amet non magna. Nulla vitae elit libero, a pharetra augue. Donec sed odio dui. Vestibulum id ligula porta felis euismod semper. Nulla vitae elit libero, a pharetra augue.
<p>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam.
<p>
Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor. Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Nullam id dolor id nibh ultricies vehicula ut id elit. Cras justo odio, dapibus ac facilisis in, egestas eget quam.
<p>
Maecenas sed diam eget risus varius blandit sit amet non magna. Nulla vitae elit libero, a pharetra augue. Donec sed odio dui. Vestibulum id ligula porta felis euismod semper. Nulla vitae elit libero, a pharetra augue.
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras mattis consectetur purus sit amet fermentum. Cras mattis consectetur purus sit amet fermentum. Aenean lacinia bibendum nulla sed consectetur.
<p>
Aenean lacinia bibendum nulla sed consectetur. Cras justo odio, dapibus ac facilisis in, egestas eget quam. Donec sed odio dui. Cras mattis consectetur purus sit amet fermentum. Nullam id dolor id nibh ultricies vehicula ut id elit. Integer posuere erat a ante venenatis dapibus posuere velit aliquet. Nullam quis risus eget urna mollis ornare vel eu leo.
</dd>
</dl>
"use strict";
// Utilities
function getPreviousElement(elem, selector) {
var sibling = elem.previousElementSibling
if (!selector) return sibling
while (sibling) {
if ( sibling.matches(selector) ) return sibling
sibling = sibling.previousElementSibling
}
}
function getNextElement(elem, selector) {
var sibling = elem.nextElementSibling
if ( !selector ) return sibling
while ( sibling ) {
if ( sibling.matches(selector) ) return sibling
sibling = sibling.nextElementSibling
}
}
function findParent(elem, selector) {
var parent = elem.parentElement
while ( parent ) {
if ( parent.matches(selector) ) return parent
parent = parent.parentElement
}
return null
}
// Implementation
(function(){
document.body.addEventListener('focus', e => {
let parent = findParent(e.target, '[role=tablist]')
let container = findParent(event.target, 'dd[role=tabpanel]')
if ( !parent || !container ) return // We weren't contained within a tabset or tab.
if ( !container.previousSibling.matches("[aria-selected]") ) container.previousSibling.click()
}, {capture: true}) // Capture on the way down, not on the route back up.
// This should "switch tab" before focusing the element.
for ( let elem of [...document.querySelectorAll('[role=tablist] [role=tab]')] ) {
elem.addEventListener('click', e => {
// Many browsers expose aria-* attributes through appropriate DOMElement attributes such as ariaSelected.
// Assigning undefined would unset the attribute and remove it from the DOM.
// This is not so in Firefox; we need to use the more procedural approach over the declarative.
for ( let sibling of [...e.target.parentElement.querySelectorAll('[aria-selected]')] )
sibling.removeAttribute('aria-selected');
e.target.setAttribute('aria-selected', "true")
})
elem.addEventListener('keydown', e => {
var target = null;
switch ( event.key ) {
case ' ':
case 'Enter':
target = event.target
break
case 'ArrowLeft':
case 'h':
case 'k':
case 'w':
case 'a':
if ( e.target.attributes['aria-selected'] )
target = getPreviousElement(event.target, '[role=tab]')
break
case 'ArrowRight':
case 'l':
case 'j':
case 's':
case 'd':
if ( e.target.attributes['aria-selected'] )
target = getNextElement(event.target, '[role=tab]')
break
}
if ( !target ) return;
target.focus()
target.click()
e.preventDefault()
e.stopPropagation()
})
}
})()
// Functional CSS Required for VCL Component Operation
dl[role=tablist] {
display: flex;
flex-wrap: wrap;
> dt {
order: 1; z-index: 2;
&:focus { z-index: 6; }
&[aria-selected] { z-index: 4; }
&[aria-selected] + dd { display: block; }
}
> dd { order: 2; flex-basis: 100%; display: none; z-index: 1; }
&.below dt { order: 3; }
}
// Example Presentation
body { background-color: #333; color: #ddd; padding: 15px; }
p + * { margin-top: 10px; }
a, a:active, a:visited { color: #09e; }
dl[role=tablist] {
margin: 0 0 20px;
> dt, & dd { background-color: #444; border: 1px solid #ddd; margin: 0; padding: 8px 10px; border-top-right-radius: 10px; }
> dt { border-top-left-radius: 10px; border-bottom-width: 0; margin-right: -1px; cursor: pointer; }
> [aria-selected] { background-color: black; cursor: default; }
&.below dt { border-top-width: 0; border-bottom-width: 1px; border-top-left-radius: 0; border-top-right-radius: 0; border-bottom-left-radius: 10px; border-bottom-right-radius: 10px; }
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment