Why does TAB skip some tab buttons?
This is the correct WAI-ARIA behavior! Only the active tab button is in the TAB order. Use Arrow keys to navigate between tabs within a container.
Powerful event-driven UI components powered by YEH
This entire tab component uses only 5 root listeners to handle all interactions. No matter how many buttons, inputs, or forms you add—even in nested tabs—they all use the same delegated listeners. Copy & paste any element and it works immediately without re-initialization.
click
— 1 listener (tab navigation + all button clicks, required)keydown
— 1 listener (keyboard shortcuts + tab navigation, required)change
— 1 listener (selects, checkboxes, radios, optional)input
— 1 listener (text inputs, live search, debounced, optional)submit
— 1 listener (all form submissions, optional)Click any button to see the event bus in action:
This is a nested tab component! Try these:
These components were copied & pasted:
In this example, all unputs are throttled, set in the constructor.
Open and click "Try Ynfiniti" "Load More Tabs →" repeatedly while you climb up the resulting DOM tree to experience Ynfiniti. Most frameworks allegedly crash after 4-5 levels. YaiTabs in any case keeps going: 4 levels, 40 levels, presumably 400,000 levels...
Instructions: Keep clicking "Load More Tabs →" inside each newly loaded tab to create infinite nesting. Watch the performance stay green!
Our only limitation is the browser's capacity to render nested elements recursively injected. The registered listener doesn't mind - it just waits for events triggered from within its assigned element, while the browser has to keep the whole world together in a div!
That's not even a challenge for YEH. It's impossible to overwhelm a single listener that basically does nothing but look in one direction, waiting for smoke signals from the almost lost, to finally guide them to their intended handler.
Technical reality: One listener handles infinite nesting with O(1) performance. No listener proliferation, no memory leaks, no performance degradation, full scope awareness. Not even 4,000,000 levels should matter - for the listener, nothing changes because it doesn't look for levels or elements, but for events triggered in their territory.
Stress tested the component with up to 70 dynamically loaded and hierarchically nested tab components with two event listeners added to the root component handling all interactions. LCP remains constant at 0.10 s – the browser DOM rendering becomes the bottleneck for our O(1) event system.
Level | Components | LCP | INP | Event Listeners |
---|---|---|---|---|
INIT | 1 | 0.10s | - | 2 |
10 | 10 | 0.10s | 104ms | 2 |
20 | 20 | 0.10s | 240ms | 2 |
30 | 30 | 0.10s | 344ms | 2 |
50 | 50 | 0.10s | 728ms | 2 |
70 | 70 | 0.10s | 1,128ms | 2 |
Conclusion: LCP stays constant (O(1) scaling proven) while INP degrades due to browser DOM complexity, not event handling. |
This section is 100%, but there are still more delegation secrets waiting to be discovered.
Each YaiTabs root component generally uses two event listeners (1*click and 1*keystroke), regardless of the number of tabs and nesting depth. However, you can also configure YaiTabs to add more eventListeners, which you can ovserve via hooks to handle events in that component. All listeners will be added once and shared between observers till session end.
Sidenote: This demo's ExampleHandler class uses a single <body>
click listener via YEH (required and included for YaiTabs anyway) to manage additional page interactions needed. They even work within Tab components.
The function, that updates existing links on this page with the correct URL is current state Example only.
Create full URLs by just using the data-ref-path (key) of the tab you're targeting. The "Accessibility & Nesting" tab below has data-ref-path = "main-tabs". In it, there is another tabs component with data-ref-path = "lvl-1-tabs". This values are custom set. When you have the key for the targeted path, call the static getter: -- `tabs.reconstructUrlFromRef('lvl-1-tabs', 4, container)`, which will return the hash needed to open that tab. -- `#main-tabs=3&lvl-1-tabs=2` Replace it in the URL bar and let YEH open that tab. Since data-ref-path is a static, not calculated key, this can be considereed links. The following Link Opens multiple tab components (inclusive the one in which you actually do read.)
YaiTabs is a lightweight, accessible tab component with URL state, deep nesting, and a single-listener event model.
tabindex
.aria-busy
+ aria-live
for async loads.
<div data-yai-tabs>
<nav data-controller>
<button data-tab-action="open" data-open="1">Tab A</button>
<button data-tab-action="open" data-open="2">Tab B</button>
</nav>
<div data-content>
<div data-tab="1">Content A…</div>
<div data-tab="2">Content B…</div>
</div>
</div>
Tip: Use unique data-ref-path attributes per component to enable location.hash handler.
Designed for large pages and many nested instances.
data-history-mode="replace"
to avoid bloating browser history on rapid tabbing.prefers-reduced-motion
CSS gate for transitions on low-motion users.“One listener per container keeps memory flat and GC friendly—even with O(n)+ levels of nested tabs.” Architecture Note
The example below demonstrates some nested containers with different nav positions. Hash state still resolves the exact open tab at each level.
role="tablist"
on the nav
, role="tab"
on buttons, role="tabpanel"
on panels.aria-controls
and each panel aria-labelledby
.aria-hidden="true"
and tabindex="-1"
.Below we nest a top-nav some other navs left and right.
Switch between fade
, slide-*
, zoom
, or flip
via data-behavior
. Keep transitions short (150–300ms) for snappy feel.
For content that shifts layout, consider overflow gating to avoid jank.
data-ref-path
key.hashchange
, containers reconcile their state (no loops: guarded writes).Use data-history-mode="replace"
to keep Back-stack clean on frequent tabbing.
data-default
marks the initial tab.data-open
sets the target panel ID.data-nav
controls nav position: top/bottom/left/right.Initialize only once per container; the component is idempotent by design.
When loading remote HTML:
aria-busy="true"
before fetch; reset to false
after.aria-live="polite"
for subtle SR announcements.Consider caching for quick back-navigation.
tabindex="0"
per tablist (roving tabindex).data-ref-path
per container:focus-visible
ring for all interactive elementsaria-orientation
consistent with computed layout<!--
Ca. 350 clicks
Reached level: 55
-->
<div data-yai-tabs data-nesting="55" />
Local metrics
Largest Contentful Paint (LCP) 0.16 s
Your local LCP value of 0.16 s is good. LCP elem. p
Cumulative Layout Shift (CLS) 0.17
Your local CLS value of 0.17 needs improvement.
Worst cluster 2 shifts Interaction to Next Paint (INP) 1,192 ms
Your local INP value of 1,192 ms is poor.
Respect user preferences with a simple CSS gate:
@media (prefers-reduced-motion: reduce) {
[data-yai-tabs] [data-tab] {
transition: none;
animation: none;
}
}
Keep parity in UX by retaining visual state changes without movement.
Compliant with the WAI-ARIA Authoring Practices:
Set aria-orientation
based on computed layout for robustness.
Below we nest a top-nav inside navs left and right.
Switch between fade
, slide-*
, zoom
, or flip
via data-behavior
. Keep transitions short (150–300ms) for snappy feel.
For content that shifts layout, consider overflow gating to avoid jank.
data-ref-path
key.hashchange
, containers reconcile their state (no loops: guarded writes).Use data-history-mode="replace"
to keep Back-stack clean on frequent tabbing.
data-default
marks the initial tab.data-open
sets the target panel ID.data-nav
controls nav position: top/bottom/left/right.Initialize only once per container; the component is idempotent by design.
When loading remote HTML:
aria-busy="true"
before fetch; reset to false
after.aria-live="polite"
for subtle SR announcements.Consider caching for quick back-navigation.
tabindex="0"
per tablist (roving tabindex).data-ref-path
per container:focus-visible
ring for all interactive elementsaria-orientation
consistent with computed layout
Respect user preferences with a simple CSS gate:
@media (prefers-reduced-motion: reduce) {
[data-yai-tabs] [data-tab] {
transition: none;
animation: none;
}
}
Keep parity in UX by retaining visual state changes without movement.
YaiTabs requires YEH to handle all events in a self-contained manner. It's an ES6 Class focused on event handling and delegation (~800 LOC).
Since YEH is already loaded for YaiTabs, you can leverage it for any additional event handling needs. This gives you the same O(1) performance benefits for any custom interactions. BTW, this example page uses YEH under the hood, too.
Here's a simple example showing how to create your own event handler using the same architecture:
<!--
Use "#app" or "body" as selector; with this
setup, you won't need any additional listeners for any added
listener after init. One listener on a parent element will handle
all triggered events in that parent practically forever.
For more details see: https://github.com/yaijs/yeh
-->
<div id="app">
<button data-action="save">Save</button>
<button data-action="delete">Delete</button>
<!--
Add any number of buttons here - one single listener handles
them all, whether added statically or dynamically later.
-->
</div>
<script type="module">// If class is not already in window.YEH
import {YEH} from 'https://cdn.jsdelivr.net/npm/@yaijs/yeh@1.0/+esm'
class YourHandler extends YEH {
// Add EventListeners (YEH)
constructor() {
super({
// Route all click events in `#app` to handleClick()
'#app': [{ type: 'click', handler: 'handleClick' }]
//= '#app': ['click']
});
}
// Route each click from here to its intended method using whatever
// technique you prefer, attributes, mappings, conditions...
handleClick(event, target) {
const action = target.dataset.action; // data-action=""
// If requested action is a valid function, call it
if (action && typeof this[action] === 'function') {
this[action](target, event);
}
}
// All data-action methods go here; leave listeners to YEH.
save(target) { console.log('Saving...', target, this); }
delete(target) { console.log('Deleting...', target, this); }
}
const handler = new YourHandler(); // One listener handles infinite elements!
console.log(handler); // Inspect the handler instance
</script>
YaiTabs supports unlimited nesting, dynamic content loading, and URL-based routing — all handled by just 2 listeners on each root, with nested components inheriting the same delegation. Built on YEH (YpsilonEventHandler) Foundation - O(1) Performance: Single listener per container regardless of complexity. Perfect Isolation: Container-scoped event handling using `:scope >` selectors.
data-theme="default"
data-color-accent="red"
Try dynamic panels with data-url
, data-delay
, and data-min-loading
for flicker-free fetches. Buttons can restore their label via data-restore-text
after loading completes. Zero Memory Leaks: Automatic cleanup with no listener proliferation. Framework Agnostic: Works with React, Vue, Angular, or Vanilla JS.
Customize UX with lifecycle hooks: setLoading
, contentReady
, and afterLoad
— perfect for spinners or analytics. 9 Animation Behaviors - 8 smooth effects (fade, slide-up, zoom, flip, swing, spiral, slice, blur, glitch, warp, elastic) + instant. 4 Navigation Positions - Top, left, right, bottom placement.
data-color-accent="blue"
maps to CSS tokens like --yai-tabs-color-accent
and --yai-tabs-shadow-medium
. Theme via custom properties; structure and layout are inherited automatically. WCAG 2.1 AA Compliance - Full ARIA implementation with screen reader support. Keyboard Navigation - Arrow keys, Home/End, Enter/Space support.
Choose from multiple behaviors (e.g. fade
, slide-*
, blur
, instant
) and place navigation top
, left
, right
, or bottom
. Dynamic Content Loading - Fetch remote content with abort controllers. Container Isolation - Unique IDs prevent cross-contamination.
Accessibility: ARIA roles, roving tabindex
, and full keyboard support (arrows, Home/End, Enter/Space). Auto-disambiguation - Optional feature to ensure unique IDs across nested components.
data-color-scheme="dark"
swaps semantic colors (e.g. --yai-tabs-color-text
, --yai-tabs-color-background
, --yai-loader-color
) for high-contrast UI while preserving component structure.
data-color-accent="warning"
swaps semantic colors of the tab buttons (e.g. --yai-tabs-color-accent
, --yai-tabs-shadow-medium
, --yai-tabs-shadow-subtle-color
).
Event Delegation Hierarchy: YaiTabs (Root) → 5 event listeners, nested components → 0 listeners (inherits).
Event delegation scales in O(1): nested tabs add zero new listeners and inherit the root’s two. All nested and dynamically added components utilize the root component's event listeners.
Control history via data-history-mode="replace|push"
and deep-link any level with data-ref-path
for shareable URLs. Components with [data-yai-tabs] are detected on page load, root components initialize with event listeners.
Build analytics on top: subscribe to emitted events like tabReady
, stateChange
, or even “super-subscribe” to all events. Nested components are processed as lightweight delegates, dynamic content integrates with existing event structure.
Schemes cascade through nested tabs; override any level via data-color-scheme
or switch themes with data-theme
(e.g. minimal
, pills
). Multi-Panel Dashboards - Open specific widget combinations across panels. Design Systems - Coordinate color schemes, components, and layouts simultaneously.
Panels use data-content
+ data-tab
, navigation uses data-controller
. Hidden panels become inert/ARIA-hidden for WCAG-friendly behavior. Complex Forms - Navigate between form sections with preserved field states. E-commerce - Show product variants, reviews, and recommendations in sync.
Per default, all data-tab container get the default padding set via CSS property --yai-content-padding: 20px;
. Documentation - Link to specific topics across multiple navigation levels. Deterministic URLs - Same state always generates same URL. Minimal Payload - Only stores essential tab identifiers.
Deep-link any combination of tabs using data-ref-path
(hash parameters). Programmatically build links with YaiTabs.reconstructUrlFromRef()
for shareable states. Instant Navigation - No page reloads, smooth tab transitions. Scalable - Handles 5+ levels of nesting effortlessly.
The live demo showcases 20+ components (root + nested) with constant listener count and smooth transitions. Performance Metrics:
Basic (3 tabs) - 1 component, 2 listeners, ~0.10s LCP, ~50KB memory.
Nested (20 tabs) - 20 components, 2 listeners, ~0.10s LCP, ~120KB memory.
Deep (70+ tabs) - 70+ components, 2 listeners, ~0.10s LCP, ~350KB memory.
Use AutoSwitch
to auto-cycle buttons for demonstrations and testing (configurable timeouts, abort, lifecycle hooks). This architecture enables truly infinite nesting without performance degradation, making YaiTabs ideal for complex applications with deep navigation hierarchies.
Prefer the Event Hook System to route click
, input
, change
, submit
, focus
, blur
through a single listener per root. YaiJS Utilities: AutoSwitch Testing Utility - Automated cycling through interactive elements. Configurable timing and behavior patterns. Event-driven architecture with lifecycle hooks.
Tip: give unique IDs to data-open
/data-tab
pairs, set meaningful data-ref-path
s, and use data-url-refresh
for fast-changing content. YaiViewport: Advanced viewport tracking utility for lazy loading, visibility detection, and scroll-based events. Supports custom thresholds and lifecycle hooks.
Spaceless content container.
Every event knows its container context — scalable apps with deep hierarchies, zero listener proliferation. Development Philosophy: "Mathematical Elegance Meets Developer Experience" - Each component proves that enterprise-grade functionality can be achieved with minimal code through revolutionary O(1) event handling architecture.
Other…
Just more blue…
Placeholdings Inc.
Override CSS variables using data-color-scheme
to switch design tokens like --yai-accent
, --yai-text
, and --yai-bg
while keeping structure/layout intact. Built-in schemes include red
, blue
, light
, and dark
.
/**
* Write / override specific properties or color schemes. The
* following properties are the actual defaults for YaiTabs. It
* contains all you need to change the appearance entirely.
*/
:root,
[data-color-scheme="light"] {
/* =========================
SEMANTIC COLOR SYSTEM
========================= */
/* Core Colors - Used for text, backgrounds, and states */
--yai-tabs-color-text: #49565b;
--yai-tabs-color-text-muted: #4a5568;
--yai-tabs-color-background: #ffffff;
--yai-tabs-color-surface: #f8fafc;
--yai-tabs-color-surface-alt: #f1f5f9;
--yai-tabs-nav-button-hover-background: #e2e2e2dd;
--yai-tabs-content-inactive-background: #e3e5e777;
--yai-tabs-content-inactive-color-muted: #555555bb;
/* Brand Colors - Semantic color palette */
--yai-tabs-color-primary: #3a59ae;
--yai-tabs-color-primary-contrast: #ffffff;
--yai-tabs-color-secondary: #7c3aed;
--yai-tabs-color-secondary-contrast: #ffffff;
--yai-tabs-color-accent: #dc2626;
--yai-tabs-color-accent-contrast: #ffffff;
--yai-tabs-color-success: #059669;
--yai-tabs-color-success-contrast: #ffffff;
--yai-tabs-color-warning: #b45309;
--yai-tabs-color-warning-contrast: #ffffff;
--yai-tabs-color-danger: #dc2626;
--yai-tabs-color-danger-contrast: #ffffff;
--yai-tabs-color-funky: #c026d3;
--yai-tabs-color-funky-contrast: #ffffff;
/* Interactive States */
--yai-tabs-color-focus: #2563eb;
--yai-tabs-color-focus-ring: rgba(37, 99, 235, 0.2);
--yai-loader-color: var(--yai-tabs-color-accent);
/* Component-specific variables */
--yai-tabs-nav-background: #e6e6e6;
--yai-tabs-nav-button-background: var(--yai-tabs-color-background);
--yai-tabs-nav-button-active-background: var(--yai-tabs-color-background);
--yai-tabs-content-background: var(--yai-tabs-color-background);
/* Closed tab state */
--yai-tabs-closed-text: "YaiTabs";
--yai-tabs-closed-align: center;
--yai-tabs-closed-timeout: .5s;
/* Layout & spacing , including fallback for: h2, h3, h4, h5, h6, p, pre, ul, ol */
--yai-tabs-padding-1: .75rem;
--yai-tabs-padding-2: 1rem;
--yai-tabs-padding-3: 1.5rem;
--yai-tabs-content-padding: 22px;
--yai-tabs-content-elements-margin: 0 0 16px;
--yai-tabs-content-line-height: 1.6;
--yai-tabs-content-min-height: 100px;
/* Nav buttons */
--yai-tabs-button-padding: 2px 20px;
--yai-tabs-button-min-height: 42px;
--yai-tabs-button-font-size: 95%;
/* Loader */
--yai-tabs-loader-speed: 1.1s;
--yai-tabs-loader-button-size: 12px;
--yai-tabs-loader-content-size: 32px;
}
How it works: Each color scheme selectively overrides the core design tokens, while inheriting all structural styles and layouts automatically. Authors & Contributors: Engin Ypsilon - Original YpsilonEventHandler architecture and YaiTabs concept. Claude-3.5-Sonnet - Implementation, optimization, and hook system architecture. DeepSeek-V3 - Comprehensive documentation and interactive demo examples. Grok-2 - Performance analysis and architectural insights. ChatGPT - Color scheme and design tokens.
/**
* Create custom color accents; replace "primary" with your accent name
*/
/* Nav button color (only applied to active tab buttons) */
[data-color-accent="primary"] {
--yai-tabs-color-accent: var(--yai-tabs-color-primary);
--yai-tabs-shadow-accent: rgba(49, 96, 199, 0.6);
}
/* Reverse color/background mode */
nav[data-controller][data-variant="primary"] button.active {
color: var(--yai-tabs-color-primary-contrast);
background-color: var(--yai-tabs-color-primary);
}
How lazy, you ask? Ultra lazy!
Unleash the laze in you! YaiJS has already reached unprecedented levels of laze, and we are just getting started.
Chunk 1 — Intro
This first panel is opened manually. Scroll down to let the next panel wake up on its own.
Chunk 2 — Lazy content starts
As soon as this area enters the viewport, Tab B 1 opens. Keep scrolling to let nested chunks take over one by one.
Threshold is low, so slow scrolling makes the sequence obvious.
Chunk 2A — Manual branch
Switch here any time to compare with the auto-opening flow.
Chunk 2B — Nested tabs inside
This panel contains another tab group. It won’t auto-open by itself; keep scrolling to reach the next lazy chunk.
Chunk 3A — Static panel
Use arrow keys inside the tablist to navigate this level.
Chunk 3B — Static panel
Just more content to click through; no auto-open here.
Chunk 3C — End of this branch
Nothing else to see here.
Chunk 2C — Auto-open example
This panel opens itself when it becomes visible. Below is one more nested set using a dark color scheme.
Chunk 4A — Dark / Manual
Open this one yourself to compare with the auto panel next to it.
Chunk 4B — Dark / Auto
Opens the moment this tablist comes into view. That’s the lazy content behavior in a nutshell.
You can still switch manually; auto triggers only fire when the button is actually in view.
High-performance viewport visibility tracking with spatial optimization for elements.
No Observer involved.
Implementing a lazy tab activator, when components become visible. The auto-open functionality on this page is for demonstration only.
// Requires peer-dependency YEH
const { YaiViewport } = window.YaiJS;
const defaultRegisteredEvents= {
window: [
{ type: 'load', options: { once: true } },
{ type: 'resize', throttle: throttle?.resize || 500 },
{ type: 'scroll', throttle: throttle?.scroll || 250 },
{ type: 'scrollend', throttle: throttle?.scrollend || 250 },
]
};
/* Init Viewport Tracker */
const yViewport = new YaiViewport({
throttle: {
esize: 500,
scroll: 150,
scrollend: 150
},
set: {
// CSS classes and data attributes for automatic state management
selector: {
// Set null to disable specific marker
pageTop: 'yvp-is-page-top', // CSS class added to body when at page top
pageEnd: 'yvp-is-page-end', // CSS class added to body when at page bottom
pageScrolled: 'yvp-is-scrolled', // CSS class added to body when scrolled past threshold
trackDistance: 'data-yvp-position', // Attribute for element's viewport position
isVisibleAttr: 'data-yvp-is-visible', // Boolean attribute for visibility state
isVisibleClass: 'yvp-is-visible', // CSS class when element is visible
hasBeenVisibleClass: 'yvp-was-visible', // CSS class added once element has been visible
isLeavingClass: 'yvp-is-leaving', // CSS class when element starts leaving viewport
isLeavingTopClass: 'yvp-is-leaving-top', // CSS class when element starts leaving from top
isLeavingBottomClass: 'yvp-is-leaving-bottom', // CSS class when element starts leaving from bottom
hasLeftClass: 'yvp-has-left', // CSS class when element left viewport
hasLeftTopClass: 'yvp-has-left-top', // CSS class when element left from top
hasLeftBottomClass: 'yvp-has-left-bottom', // CSS class when element left from bottom
}
},
// Threshold configuration - pixel offsets for detection
threshold: {
pageTop: 0, // Trigger pageTop when scrollY <= this value
pageEnd: 50, // Trigger pageEnd when near bottom (viewport height - this value)
pageScrolled: 0, // Trigger pageScrolled when scrollY > this value
// Global fallbacks
elementVisible: 0, // Element considered visible when within viewport + this buffer
elementHidden: 0, // Element considered hidden when outside viewport - this buffer
elementLeaving: 0, // Element starts leaving when beyond viewport + this buffer
elementLeft: 0, // Element completely left when beyond viewport + this buffer
// Direction-specific thresholds
elementVisibleTop: null, // Top visibility buffer
elementVisibleBottom: null, // Bottom visibility buffer
elementLeavingTop: null, // Top leaving threshold
elementLeavingBottom: null, // Bottom leaving threshold
elementLeftTop: null, // Top left threshold
elementLeftBottom: null, // Bottom left threshold
},
});
/**
* Track viewport
* Use our hook system to react to changes live
*
* Set any css selector for elements to "watch" and
* chain the hooks you want to react.
*/
yViewport.track(`
body main > .box,
body main > .section,
.lazy-tabs-section [data-yai-tabs]
`)
// After load hook, fires when the page is loaded.
.hook('afterLoad', ({ event, trackedElementsSize, trackedElements }) => {
console.log('Yai Viewport Tracker!', trackedElements);
})
// If any of the selected elements becomed visible. Can be used for all kinds of lazy stuff.
.hook('elementVisibleCheck', ({ element, rect, state, isLeaving }) => {
const button = element.querySelector(`.yvp-is-visible nav [data-inview-default]`);
if (button && !button.classList.contains('default-active')) {
button.classList.add('default-active');
requestAnimationFrame(() => {
button.click();
});
}
})
// React on special events
.hook('elementLeavingBottom', ({ element, rect, state }) => {
const button = element.querySelector(`.yvp-was-visible nav [data-inview-default]`);
if (button && button.classList.contains('active')) {
requestAnimationFrame(() => {
button.classList.add('default-active');
button.click();
});
}
})
// Element is out of view, left from bottom
.hook('elementLeftBottom', ({ element, rect, state }) => {
const button = element.querySelector(`.yvp-was-visible > nav .default-active`);
if (button) {
requestAnimationFrame(() => {
button.classList.remove('default-active');
if (button.classList.contains('active')) button.click();
});
}
});
/**
* Available hooks, lifecycle events
*/
const callbacks = {
pageTop: null, // Called when page reaches top
pageEnd: null, // Called when page reaches bottom
pageScrolled: null, // Called when page is scrolled past threshold
afterLoad: null, // Called once when page finishes loading
afterResize: null, // Called after viewport resize
afterScroll: null, // Called after scrolling
elementVisible: null, // Called when tracked element becomes visible
elementHidden: null, // Called when tracked element becomes hidden
elementVisibleCheck: null, // Permanent call to check visibility
elementLeaving: null, // Called when tracked element starts leaving viewport
elementLeavingTop: null, // Called when tracked element starts leaving viewport from top
elementLeavingBottom: null, // Called when tracked element starts leaving viewport from bottom
elementLeft: null, // Called when tracked element completely leaves viewport
elementLeftTop: null, // Called when tracked element completely leaves viewport from top
elementLeftBottom: null, // Called when tracked element completely leaves viewport from bottom
};
YaiTabs implements WCAG 2.1 AA compliant tab navigation with full screen reader support.
tablist
, tab
, tabpanel
aria-selected
, hidden
, tabindex
aria-controls
, aria-labelledby
aria-label="Tab navigation"
on tablistsDeepSeek: "100% correct and follows WAI-ARIA Authoring Practices perfectly. This is exactly how ARIA should be implemented for tab interfaces. "
This is the correct WAI-ARIA behavior! Only the active tab button is in the TAB order. Use Arrow keys to navigate between tabs within a container.
Follow the WAI-ARIA Authoring Practices:
Each tab container is independent:
Hidden tab panels have aria-hidden="true"
and tabindex="-1"
to prevent focus on inaccessible content. This ensures screen reader users
don't get trapped in hidden content.