A lightweight accessible tab UI library for vanilla JavaScript.
npm install @yama-dev/js-tab-ui
<div data-ui-tab data-ui-tab-init="overview">
<div role="tablist" aria-label="Example tabs">
<button type="button" data-ui-tab-trigger="overview">Overview</button>
<button type="button" data-ui-tab-trigger="details">Details</button>
<button type="button" data-ui-tab-trigger="disabled" class="--disabled">
Disabled
</button>
</div>
<section data-ui-tab-content="overview">Overview panel</section>
<section data-ui-tab-content="details">Details panel</section>
<section data-ui-tab-content="disabled">Disabled panel</section>
</div>
import { init } from '@yama-dev/js-tab-ui';
init();
Manual initialization:
<script src="./dist/js-tab-ui.umd.global.js"></script>
<script>
window.UiTab.init();
</script>
Automatic initialization:
<script src="./dist/js-tab-ui.auto.global.js" defer></script>
type UiTabOptions = {
rootSelector?: string;
triggerSelector?: string;
contentSelector?: string;
activeClass?: string;
disabledClass?: string;
hiddenAttribute?: string;
useHiddenAttribute?: boolean;
orientation?: 'horizontal' | 'vertical';
activation?: 'auto' | 'manual';
on?: {
init?: (detail: UiTabEventDetail) => void;
change?: (detail: UiTabEventDetail) => void;
};
};
Defaults:
init({
rootSelector: '[data-ui-tab]',
triggerSelector: '[data-ui-tab-trigger]',
contentSelector: '[data-ui-tab-content]',
activeClass: '--active',
disabledClass: '--disabled',
hiddenAttribute: 'hidden',
useHiddenAttribute: true,
orientation: 'horizontal',
activation: 'auto',
on: undefined
});
The auto bundle reads these root-level attributes:
<div
data-ui-tab
data-ui-tab-active-class="is-active"
data-ui-tab-disabled-class="is-disabled"
data-ui-tab-hidden-attribute="data-hidden"
data-ui-tab-use-hidden-attribute="false"
data-ui-tab-orientation="vertical"
data-ui-tab-activation="manual"
>
...
</div>
data-ui-tab-use-hidden-attribute="false" disables hidden attribute management. Any other value keeps it enabled.
const result = init();
const instance = result.instances[0];
instance.activate('details');
instance.destroy();
Calling init() or autoInit() again for the same root returns the existing instance instead of binding duplicate event listeners.
Initial tab priority:
data-ui-tab-initAfter initialization, the root element stores the active tab name in data-ui-tab-current.
Keyboard support:
ArrowRight / ArrowLeft: move between horizontal tabsArrowUp / ArrowDown: move between vertical tabsHome / End: move to the first or last enabled tabEnter / Space: activate the focused tabWith activation: 'manual' or data-ui-tab-activation="manual", arrow keys only move focus. Enter or Space activates the focused tab.
Use on.init and on.change when initializing from JavaScript:
init({
on: {
init(detail) {
console.log(detail.current);
},
change(detail) {
console.log(detail.previous, detail.current);
}
}
});
The root element also dispatches ui-tab:init after the initial tab has been applied and ui-tab:change when the active tab changes after initialization.
const root = document.querySelector('[data-ui-tab]');
root.addEventListener('ui-tab:init', (event) => {
console.log(event.detail.current);
});
root.addEventListener('ui-tab:change', (event) => {
console.log(event.detail.previous, event.detail.current);
});
Event detail:
type UiTabEventDetail = {
current: string;
previous: string | null;
trigger: HTMLElement;
content: HTMLElement | null;
instance: UiTabInstance;
};