Skip to content
HyperUX Experimental
Demo

Tabs pattern with roving tabindex, arrow-key navigation, and optional URL hash sync.

General Settings

Manage your account name, email, and preferences.

Security Settings

Update your password and two-factor authentication options.

Notification Settings

Choose which notifications you want to receive.


Decoupled panels — tablist and panels in separate scopes:

General Settings

Manage your account name, email, and preferences.

Security Settings

Update your password and two-factor authentication options.

Notification Settings

Choose which notifications you want to receive.


Alpine.js Tabs

huxTabs manages active tab state, keyboard focus movement, and optional URL hash synchronization. Use it to attach behavior to your own tablist and panel markup. Panels can either live inside the same x-data scope (owned) or in a separate scope via huxTabPanel (decoupled).

API

huxTabs(config)

Returns an Alpine data object with:

Internal helper methods are private implementation details and are not part of the supported API contract.

huxTabPanel(config)

Returns an Alpine data object for a decoupled tab panel with:

Options

huxTabs

huxTabPanel

Quick Start

<div
  x-data="huxTabs({
    tabItems: [
      { label: 'General', id: 'general' },
      { label: 'Security', id: 'security' }
    ],
    useHash: true
  })"
>
  <div role="tablist" aria-label="Settings">
    <template x-for="tabItem in tabItems" x-bind:key="tabItem.id">
      <button
        type="button"
        role="tab"
        x-bind:id="`tab-${tabItem.id}`"
        x-bind:aria-controls="`panel-${tabItem.id}`"
        x-bind:aria-selected="(activeTabId === tabItem.id).toString()"
        x-bind:tabindex="activeTabId === tabItem.id ? '0' : '-1'"
        x-on:click="selectTab(tabItem.id)"
        x-on:keydown.right.prevent="focusNextTab()"
        x-on:keydown.left.prevent="focusPreviousTab()"
        x-text="tabItem.label"
      ></button>
    </template>
  </div>

  <section
    id="panel-general"
    role="tabpanel"
    aria-labelledby="tab-general"
    x-show="activeTabId === 'general'"
    x-cloak
  >
    ...
  </section>
</div>

Common Usage Patterns

Initialize Active Tab From Hash

huxTabs({
  tabItems: [
    { label: 'General', id: 'general' },
    { label: 'Security', id: 'security' },
    { label: 'Notifications', id: 'notifications' },
  ],
  useHash: true,
})

Provide Explicit Initial Tab

huxTabs({
  tabItems: [
    { label: 'General', id: 'general' },
    { label: 'Security', id: 'security' },
  ],
  activeTab: 'security',
})

Decoupled Panels

Use id on huxTabs and huxTabPanel on each panel to place tablist and panels in separate DOM scopes.

<div
  x-data="huxTabs({
    id: 'settings',
    tabItems: [
      { label: 'General', id: 'general' },
      { label: 'Security', id: 'security' }
    ]
  })"
>
  <div role="tablist" aria-label="Settings">
    <template x-for="tabItem in tabItems" x-bind:key="tabItem.id">
      <button
        type="button"
        role="tab"
        x-bind:id="`tab-${tabItem.id}`"
        x-bind:aria-controls="`panel-${tabItem.id}`"
        x-bind:aria-selected="(activeTabId === tabItem.id).toString()"
        x-bind:tabindex="activeTabId === tabItem.id ? '0' : '-1'"
        x-on:click="selectTab(tabItem.id)"
        x-on:keydown.right.prevent="focusNextTab()"
        x-on:keydown.left.prevent="focusPreviousTab()"
        x-text="tabItem.label"
      ></button>
    </template>
  </div>
</div>

<section
  id="panel-general"
  role="tabpanel"
  aria-labelledby="tab-general"
  tabindex="0"
  x-data="huxTabPanel({ tabsId: 'settings', tabId: 'general' })"
  x-show="isActive"
  x-cloak
>
  ...
</section>

<section
  id="panel-security"
  role="tabpanel"
  aria-labelledby="tab-security"
  tabindex="0"
  x-data="huxTabPanel({ tabsId: 'settings', tabId: 'security' })"
  x-show="isActive"
  x-cloak
>
  ...
</section>

Behavior Contract

Error Handling

Accessibility Notes

Notes