YaiTabs


VanillaJS Lightweight Tabs Component

YaiTabs Demo Page

🎯 YaiTabs Event Bus

Powerful event-driven UI components powered by YEH


Event Delegation Architecture

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.

Active Listeners:

  • 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)
  • Total: 5 listeners for unlimited interactions

🖱️ Click Event Playground

Click any button to see the event bus in action:


Live Responses:

pending…

Nested Component Demo

This is a nested tab component! Try these:

pending…

Copy & Paste Magic

These components were copied & pasted:

pending…

⌨️ Input & Change Events

In this example, all unputs are throttled, set in the constructor.



Input Responses:

pending…

📝 Form Evolution



pending…

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.

What is the world record for nested tab components any way?


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
INIT1 0.10s- 2
10 100.10s104ms2
20 200.10s240ms2
30 300.10s344ms2
50 500.10s728ms2
70700.10s1,128ms2
Conclusion: LCP stays constant (O(1) scaling proven) while INP degrades due to browser DOM complexity, not event handling.

And what?


This section is 100%, but there are still more delegation secrets waiting to be discovered.

Follows WAI-ARIA Authoring Practices #



https://www.w3.org/WAI/ARIA/apg/

Lorem ipsum FAQ?


All FAQs are here, more FAQs incoming.

Event Delegation Architecture


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.


0

YaiTabs: Quick Overview

YaiTabs is a lightweight, accessible tab component with URL state, deep nesting, and a single-listener event model.


Highlights

  • URL State: Deep-link to any tab (even nested) and restore state on reload.
  • APG-compliant: Correct roles, relationships, keyboard model, and roving tabindex.
  • Single Listener: Each container uses one delegated handler—great for performance.
  • Dynamic Content: Optional aria-busy + aria-live for async loads.

Basic Markup

<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.

Performance Cheatsheet

Designed for large pages and many nested instances.


Why it’s fast

  1. Delegated Events: 2 listener per component; no N×M explosion with nesting.
  2. DOM Distance Cache: Resolves closest targets efficiently inside component.
  3. Idempotent Init: Prevents re-binding and duplicate listeners.

Practical Tips

  • Prefer data-history-mode="replace" to avoid bloating browser history on rapid tabbing.
  • For async loads, enable content cache if your UX benefits from instant back-navigation.
  • Use 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

Accessibility & Nested Examples

The example below demonstrates some nested containers with different nav positions. Hash state still resolves the exact open tab at each level.

A11y Basics

  • role="tablist" on the nav, role="tab" on buttons, role="tabpanel" on panels.
  • Each tab button has aria-controls and each panel aria-labelledby.
  • Inactive panels use aria-hidden="true" and tabindex="-1".

Deep Nesting Demo

Below we nest a top-nav some other navs left and right.

Animations

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.

URL State & Deep Links

  • Each container uses a unique data-ref-path key.
  • On open, the route map updates and writes to the hash.
  • On hashchange, containers reconcile their state (no loops: guarded writes).

Use data-history-mode="replace" to keep Back-stack clean on frequent tabbing.

API Basics
  • 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.

Async Content

When loading remote HTML:

  • Set aria-busy="true" before fetch; reset to false after.
  • Optionally set aria-live="polite" for subtle SR announcements.
  • Re-run accessibility setup on nested content if it includes new tab containers.

Consider caching for quick back-navigation.

Troubleshooting
  • Duplicate listeners? Ensure only one init per container file & no legacy helpers.
  • Stale hash? Remove the key when closing a tab or fallback to default.
  • Focus lost? Keep one tabindex="0" per tablist (roving tabindex).
Pre-Ship Checklist
  • Unique data-ref-path per container
  • Visible :focus-visible ring for all interactive elements
  • aria-orientation consistent with computed layout
  • Reduced-motion guard on heavy transitions
Design Notes
  • Prefer subtle elevation for active tabs and maintain a consistent hit-area (min-height ~40px).
  • Use semantic color tokens for easy theming.
Changelog (Excerpt)
  • 3.1: URL state for nested tabs; orientation via computed layout.
  • 3.0: Single-listener event model; idempotent accessibility setup.
  • 2.x: Roving tabindex + APG keyboard model.
A new Reality
<!--
    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.

Reduced Motion

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.

Keyboard Model

Compliant with the WAI-ARIA Authoring Practices:

  • Arrows move focus among tab buttons (horizontal vs. vertical respected).
  • Enter/Space activates and may move focus into the panel (configurable).
  • Home/End go to first/last tab; Esc bubbles to parent container.

Set aria-orientation based on computed layout for robustness.

Deep Nesting Demo 2

Below we nest a top-nav inside navs left and right.

Animations

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.

URL State & Deep Links

  • Each container uses a unique data-ref-path key.
  • On open, the route map updates and writes to the hash.
  • On hashchange, containers reconcile their state (no loops: guarded writes).

Use data-history-mode="replace" to keep Back-stack clean on frequent tabbing.

API Basics
  • 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.

Async Content

When loading remote HTML:

  • Set aria-busy="true" before fetch; reset to false after.
  • Optionally set aria-live="polite" for subtle SR announcements.
  • Re-run accessibility setup on nested content if it includes new tab containers.

Consider caching for quick back-navigation.

Troubleshooting
  • Duplicate listeners? Ensure only one init per container file & no legacy helpers.
  • Stale hash? Remove the key when closing a tab or fallback to default.
  • Focus lost? Keep one tabindex="0" per tablist (roving tabindex).
Pre-Ship Checklist
  • Unique data-ref-path per container
  • Visible :focus-visible ring for all interactive elements
  • aria-orientation consistent with computed layout
  • Reduced-motion guard on heavy transitions
Design Notes
  • Prefer subtle elevation for active tabs and maintain a consistent hit-area (min-height ~40px).
  • Use semantic color tokens for easy theming.
Changelog (Excerpt)
  • 3.1: URL state for nested tabs; orientation via computed layout.
  • 3.0: Single-listener event model; idempotent accessibility setup.
  • 2.x: Roving tabindex + APG keyboard model.

Reduced Motion

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.

Need a click handler for some VooDoo?

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>

The handler class

<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>

Default Accent


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"

Default content 2


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.

Default content 3


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.


Blue Accent


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.

Blue content 2


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.

Blue content 3


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.


Dark Scheme and Warning Accent


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).

Dark content 2


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.

Dark content 3


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.

Unique dark content


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.

Example 4: Nested Color Scheme Inheritance

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.

More red, but danger

Final nested content


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.

Lorem ipsum dolor holding place.


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.

Red content 2


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.

Red content 3


Tip: give unique IDs to data-open/data-tab pairs, set meaningful data-ref-paths, 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.

Final nested content


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.

Lorem ipsum dolor holding place.

Other…

Just more blue…

Placeholdings Inc.

Custom Properties Theming


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);
}

Lazy Content Incoming

How lazy, you ask? Ultra lazy!


YaiViewport

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.

  • Each section is a “lazy chunk”.
  • Auto-open only happens when the chunk becomes visible.
Tip: use this tab to compare manual vs. auto behavior.

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.

Next nested chunk ahead…

Chunk 2A — Manual branch

Switch here any time to compare with the auto-opening flow.

Scroll or switch to other tabs…

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.

Level 3 demo coming into view…

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.

Scroll a bit more to trigger the dark set…

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.

YaiViewport - Advanced Viewport Tracking System

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
};

ARIA Accessibility Summary


YaiTabs implements WCAG 2.1 AA compliant tab navigation with full screen reader support.


Implemented Features:


DeepSeek: "100% correct and follows WAI-ARIA Authoring Practices perfectly. This is exactly how ARIA should be implemented for tab interfaces. "

Keyboard Navigation FAQ


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.

What's the proper keyboard navigation pattern?

Follow the WAI-ARIA Authoring Practices:

  • TAB - Move between tab containers and content areas
  • - Navigate between tabs within a container
  • Enter/Space - Activate/deactivate the focused tab
  • Home/End - Jump to first/last tab in container

How do I navigate nested tabs?

Each tab container is independent:

  1. TAB to focus the main tab container
  2. Arrow keys to select different main tabs
  3. TAB again to enter the tab content
  4. TAB once more to reach nested tab container
  5. Arrow keys to navigate nested tabs

Why can't I TAB into hidden tab content?

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.