A collection of JavaScript libraries that adhere ceremoniously with WCAG guidelines. This package provides accessible, keyboard-navigable UI components with full ARIA support and responsive design capabilities.
Found a bug or have a feature request? Please open an issue on GitHub.
Philip Stier - Tech Lead, Senior Developer
npm install @atendesign/javascript-components
- π Fully accessible implementation
- β¨οΈ Keyboard navigation support
- π± Responsive design
- π¨ Customizable styling
- π Toggle state management
HTML
<div class="accordion">
<h3 class="accordion__heading">
<button id="accordion-trigger-01" class="accordion__trigger" aria-expanded="false" aria-controls="accordion-section-01>
Accordion Section
</button>
</h3>
<div id="accordion-section-01" class="accordion__content" role="region" aria-labelledby="accordion-trigger-01">
<p>This is the accordion content.</p>
</div>
</div>
JavaScript
import { Accordion } from '@atendesign/javascript-components';
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.accordion').forEach(accordion => {
const accordionComponent = new Accordion(accordion, {options});
accordionComponent.init();
});
});
| Option | Type | Default | Description |
|---|---|---|---|
selectors.trigger |
string | '.accordion__trigger[aria-controls]' |
CSS selector for accordion trigger buttons |
classes.expanded |
string | 'is-expanded' |
CSS class applied to expanded accordion state |
const accordionComponent = new Accordion(accordionElement, {
selectors: {
trigger: '.my-accordion__trigger[aria-controls]',
},
classes: {
expanded: 'is-open',
},
});
accordionComponent.init();aria-expandedon trigger button (automatically managed by the component)aria-controlson trigger button (must match content panel's ID)idon content panel (must match trigger's aria-controls value)aria-labelledbyon content panel (must match trigger button's ID)role="region"on content panel
EnterorSpace: Toggle accordion section- Mouse click: Toggle accordion section
- π Fully accessible implementation
- β¨οΈ Keyboard navigation support
- π± Responsive design
- π¨ Customizable styling
- π Tab state management
HTML
<div class="tab-content">
<div role="tablist" class="tab-content__navigation"></div>
<div id="tab-content-01"
class="tab-content__group is-expanded"
role="tabpanel"
aria-labelledby="tab-tab-content-01">
<button type="button"
id="tab-tab-content-01"
class="tab-content__trigger is-expanded"
role="tab"
aria-haspopup="true"
aria-selected="true"
aria-expanded="true"
aria-controls="tab-content-01">
Tab 01
</button>
<div class="tab-content__content">
<p>Text content goes here - Tab 01</p>
</div>
</div>
</div>
JavaScript
import { TabContent } from '@atendesign/javascript-components';
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.tab-content').forEach(tabContent => {
const tabContentComponent = new TabContent(tabContent);
tabContentComponent.init();
});
});
| Option | Type | Default | Description |
|---|---|---|---|
selectors.navigation |
string | '.tab-content__navigation' |
CSS selector for desktop tab navigation container |
selectors.group |
string | '.tab-content__group' |
CSS selector for tab groups |
selectors.trigger |
string | '.tab-content__trigger' |
CSS selector for tab trigger buttons |
classes.expanded |
string | 'is-expanded' |
CSS class applied to expanded tabs/groups |
breakpoint |
number | 768 |
Media breakpoint where tabs switch to accordions |
const tabContentComponent = new TabContent(tabContentElement, {
selectors: {
navigation: '.my-tabs__navigation',
group: '.my-tabs__group',
trigger: '.my-tabs__trigger',
},
classes: {
expanded: 'is-open',
},
breakpoint: 1024,
});
tabContentComponent.init();role="tablist"on tab containerrole="tab"on each tab buttonrole="tabpanel"on each content panelaria-selectedon tab buttons (automatically managed by the component)aria-controlson tab buttons (must match panel's ID)idon content panels (must match tab's aria-controls value)aria-labelledbyon content panels (must match tab button's ID)tabindexon tab buttons (automatically managed by the component)
Arrow Left/Right: Navigate between tabsHome: Move to first tabEnd: Move to last tabEnterorSpace: Activate focused tab- Mouse click: Activate tab
- π Fully accessible implementation
- β¨οΈ Keyboard navigation support
- π± Responsive design (desktop/mobile state)
- π¨ Customizable styling
- π Submenu expand/collapse management
- π±οΈ Click-outside to close
HTML
<nav class="menu">
<ul class="menu__items">
<li class="menu__item">
<a class="menu__link" href="#">Home</a>
</li>
<li class="menu__item">
<button class="menu__dropdown-trigger" aria-expanded="false">About</button>
<div class="menu__dropdown">
<ul class="menu__items">
<li class="menu__item">
<a class="menu__link" href="#">Our Team</a>
</li>
<li class="menu__item">
<a class="menu__link" href="#">Our Story</a>
</li>
</ul>
</div>
</li>
<li class="menu__item">
<a class="menu__link" href="#">Services</a>
</li>
</ul>
</nav>JavaScript
import { Menu } from '@atendesign/javascript-components';
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.menu').forEach(menu => {
const menuComponent = new Menu(menu);
menuComponent.mount();
});
});| Option | Type | Default | Description |
|---|---|---|---|
selectors.trigger |
string | '.menu__dropdown-trigger' |
CSS selector for dropdown trigger buttons |
selectors.items |
string | '.menu__items' |
CSS selector for the menu items container |
selectors.item |
string | '.menu__item' |
CSS selector for individual menu items |
selectors.link |
string | '.menu__link' |
CSS selector for menu item links |
selectors.dropdown |
string | '.menu__dropdown' |
CSS selector for dropdown containers |
classes.expanded |
string | 'is-expanded' |
CSS class applied to expanded dropdown state |
breakpoint |
number | 768 |
Viewport width (px) at which the menu switches between mobile and desktop behaviour |
const menuComponent = new Menu(menuElement, {
selectors: {
trigger: '.my-menu__dropdown-trigger',
items: '.my-menu__items',
item: '.my-menu__item',
link: '.my-menu__link',
dropdown: '.my-menu__dropdown',
},
classes: {
expanded: 'is-open',
},
breakpoint: 1024,
});
menuComponent.mount();aria-expandedon dropdown trigger buttons (automatically managed by the component)
| Key | Action |
|---|---|
Enter or Space |
Expand focused submenu and move focus to its first item |
Arrow Down |
Expand submenu (focus first item) or move to the next sibling item |
Arrow Up |
Expand submenu (focus last item) or move to the previous sibling item |
Arrow Right |
When inside a submenu, collapse it and move focus to the next top-level item |
Arrow Left |
When inside a submenu, collapse it and move focus to the previous top-level item |
Escape |
Collapse the open submenu and return focus to its trigger |
Home |
Close all submenus and focus the first top-level menu item |
End |
Close all submenus and focus the last top-level menu item |
| Mouse click | Toggle submenu / close all when clicking outside the menu |
| Method | Description |
|---|---|
mount() |
Attaches all event listeners and initialises the responsive media query |
destroy() |
Removes all event listeners and cleans up the component |
- π Scroll-aware positioning bounded within a parent container
- β‘ Optimised with
requestAnimationFramethrottling β one update per frame - π
ResizeObserverkeeps layout measurements accurate after DOM reflow - π¨ Customizable class names and top-spacing
- π Three-state lifecycle:
defaultβstuckβbottom
HTML
<div class="sticky-parent">
<aside class="sticky-element">
I stick within my parent!
</aside>
<div class="sticky-content">
<!-- tall content -->
</div>
</div>CSS
/* The parent receives position: relative automatically if not already set. */
.sticky-element.is-stuck {
position: absolute;
}
.sticky-element.is-bottom {
position: absolute;
bottom: 0;
}JavaScript
import { Sticky } from '@atendesign/javascript-components';
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.is-sticky').forEach(el => {
const stickyComponent = new Sticky(el);
stickyComponent.mount();
});
});| Option | Type | Default | Description |
|---|---|---|---|
topSpacing |
number | 40 |
Gap in pixels between the viewport top and the stuck element |
classes.stuck |
string | 'is-stuck' |
CSS class applied when the element is in the sticky (viewport-tracking) state |
classes.bottom |
string | 'is-bottom' |
CSS class applied when the element has reached the bottom boundary of its parent |
const stickyComponent = new Sticky(el, {
topSpacing: 80,
classes: {
stuck: 'is-pinned',
bottom: 'is-anchored',
},
});
stickyComponent.mount();The element transitions between three states as the page scrolls:
| State | Condition | Description |
|---|---|---|
default |
Before the sticky zone | Element stays in its natural document position |
stuck |
Inside the sticky zone | Tracks the viewport via position: absolute + an inline top offset |
bottom |
Past the parent's boundary | Anchored to the parent's bottom edge |
| Method | Description |
|---|---|
mount() |
Caches layout values, attaches scroll and resize listeners, and applies the initial state |
destroy() |
Removes the scroll listener and disconnects the ResizeObserver |
- π Fully accessible implementation
- β¨οΈ Full keyboard support with focus trapping
- π Body scroll locked while open
- π±οΈ Overlay click to close
- π Focus restored to the triggering element on close
- π Modal relocated to
<body>on mount for reliable fixed positioning
HTML
<button type="button" aria-controls="my-modal" aria-expanded="false">
Open Modal
</button>
<div id="my-modal"
class="modal"
role="dialog"
aria-modal="true"
aria-labelledby="my-modal-title"
aria-hidden="true">
<div class="modal__overlay">
<div class="modal__dialog">
<button type="button" class="modal__close" aria-label="Close modal">Close</button>
<h2 id="my-modal-title">Modal Title</h2>
<p>Modal content goes here.</p>
</div>
</div>
</div>JavaScript
import { Modal } from '@atendesign/javascript-components';
document.addEventListener('DOMContentLoaded', function() {
document.querySelectorAll('.modal').forEach(modal => {
const modalComponent = new Modal(modal);
modalComponent.mount();
});
});| Option | Type | Default | Description |
|---|---|---|---|
selectors.trigger |
string | '[aria-controls="{id}"]' |
CSS selector for trigger buttons (scoped to the whole document) |
selectors.closeButton |
string | '.modal__close' |
CSS selector for close buttons inside the modal |
selectors.overlay |
string | '.modal__overlay' |
CSS selector for the overlay element; clicking it closes the modal |
classes.expanded |
string | 'is-expanded' |
CSS class applied to the modal element when it is open |
const modalComponent = new Modal(modalElement, {
selectors: {
trigger: '.my-open-button',
closeButton: '.my-modal__close',
overlay: '.my-modal__overlay',
},
classes: {
expanded: 'is-open',
},
});
modalComponent.mount();idon the modal element (must match each trigger'saria-controlsvalue)role="dialog"on the modal elementaria-modal="true"on the modal elementaria-labelledbyon the modal element (should reference the dialog heading's ID)aria-hidden="true"on the modal element (automatically managed by the component)aria-controlson trigger buttons (must match the modal element's ID)aria-expandedon trigger buttons (automatically managed by the component)aria-labelor visible label on each close button
| Key | Action |
|---|---|
Escape |
Close the modal and return focus to the triggering element |
Tab |
Move focus to the next focusable element; wraps from last to first |
Shift+Tab |
Move focus to the previous focusable element; wraps from first to last |
| Method | Description |
|---|---|
mount() |
Relocates the modal to <body>, caches internal elements, and attaches all event listeners |
destroy() |
Removes all event listeners attached by mount() |