# Tabs

Tabs make it easy to switch between different views.

[![npm](https://img.shields.io/badge/npm-v4.2.0-blue)](https://www.npmjs.com/package/@preline/tabs) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![Demo](https://img.shields.io/badge/demo-online-brightgreen)](https://preline.co/plugins/tabs.html)

## Contents

- [Overview](#overview)
- [Installation](#installation)
- [Basic usage](#basic-usage)
- [Accessibility notes](#accessibility-notes)
- [Configuration Options](#configuration-options)
- [JavaScript API](#javascript-api)
- [Events](#events)
- [Common Patterns](#common-patterns)
- [License](#license)

## Overview

The Tabs component allows users to switch between different content sections within the same context. It supports keyboard navigation, programmatic control, and customizable event types for activation.

**Key Features:**
- Multiple tab panels in a single group
- Keyboard navigation support (Arrow keys, Home, End, Enter)
- Customizable activation events (click, hover)
- Programmatic control via JavaScript API
- Event system for tab change tracking
- Accessibility attributes (ARIA) built-in

## Installation

To get started, install Tabs plugin via npm, else you can skip this step if you are already using Preline UI as a package.

```bash
npm i @preline/tabs
```

### CSS

Use [`@source`](https://tailwindcss.com/docs/functions-and-directives#source-directive) to register the plugin's JavaScript path for Tailwind CSS scanning, then [`@import`](https://tailwindcss.com/docs/functions-and-directives#import-directive) the plugin's CSS files into your Tailwind CSS file.

```css
@import "tailwindcss";

/* @preline/tabs */
@source "../node_modules/@preline/tabs/*.js";
@import "./node_modules/@preline/tabs/variants.css";
@import "./node_modules/@preline/tabs/theme.css";
```

### JavaScript

Include the JavaScript that powers the interactive elements near the end of your `</body>` tag:

```html
<script src="./node_modules/@preline/tabs/index.js"></script>
```

### Manual Initialization

Use the `non-auto` entry if you need manual initialization. In this mode, automatic initialization on page load is not included, so the component should be initialized explicitly.

```html
<script type="module">
  import HSTabs from "@preline/tabs/non-auto.mjs";

  new HSTabs(document.querySelector("#tabs"));
</script>
```

### Via Bundler

When using a bundler (Vite, webpack, etc.), import the plugin directly as an ES module.

`@preline/tabs` is the auto-init entry: it scans the DOM and initializes matching elements automatically.

```js
import "@preline/tabs";
```

`@preline/tabs/non-auto` is the manual entry: use it when you want explicit control over when initialization happens, either via `autoInit()` or by creating a specific instance yourself.

```js
import HSTabs from "@preline/tabs/non-auto";

HSTabs.autoInit();

// Or initialize a specific element manually
const el = document.querySelector("#tabs");
if (el) new HSTabs(el);
```

### TypeScript

This package ships with TypeScript type definitions. No additional `@types/` package is needed.

## Basic usage

The following example demonstrates the minimal HTML structure required for a tabs component. This is a base template without custom styling - you can apply your own CSS classes and styles as needed. The example shows three tabs with corresponding content panels, with the first tab active by default.

```html
<nav class="flex gap-x-2" aria-label="Tabs" role="tablist">
  <button type="button" class="active" id="hs-unstyled-tabs-item-first" aria-selected="true" data-hs-tab="#hs-unstyled-tabs-first" aria-controls="hs-unstyled-tabs-first" role="tab">
    Tab 1
  </button>
  <button type="button" id="hs-unstyled-tabs-item-second" aria-selected="false" data-hs-tab="#hs-unstyled-tabs-second" aria-controls="hs-unstyled-tabs-second" role="tab">
    Tab 2
  </button>
  <button type="button" id="hs-unstyled-tabs-item-third" aria-selected="false" data-hs-tab="#hs-unstyled-tabs-third" aria-controls="hs-unstyled-tabs-third" role="tab">
    Tab 3
  </button>
</nav>

<div class="mt-3">
  <div id="hs-unstyled-tabs-first" role="tabpanel" aria-labelledby="hs-unstyled-tabs-item-first">
    This is the <em>first</em> item's tab body.
  </div>
  <div id="hs-unstyled-tabs-second" class="hidden" role="tabpanel" aria-labelledby="hs-unstyled-tabs-item-second">
    This is the <em>second</em> item's tab body.
  </div>
  <div id="hs-unstyled-tabs-third" class="hidden" role="tabpanel" aria-labelledby="hs-unstyled-tabs-item-third">
    This is the <em>third</em> item's tab body.
  </div>
</div>
```

**Structure Requirements:**
- `role="tablist"`: Required on the container element (nav, div, etc.)
- `role="tab"`: Required on each tab button
- `role="tabpanel"`: Required on each content panel
- `data-hs-tab`: Required on each tab button, must be a valid CSS selector pointing to the corresponding tabpanel
- Unique `id` attributes for each tab button and its corresponding panel
- Proper ARIA attributes (`aria-selected`, `aria-controls`, `aria-labelledby`)

**Initial State:**
- To have a tab active by default, add the `active` class to the tab button and set `aria-selected="true"`
- For inactive tabs, add the `hidden` class to the tabpanel and set `aria-selected="false"`

## Accessibility notes

### Keyboard interactions

| Command | Description |
| --- | --- |
| ArrowLeft / ArrowRight | Selects the previous/next non-disabled tab. |
| ArrowUp / ArrowDown (in vertical mode) | Selects the previous/next non-disabled tab. |
| Home / End | Selects the first/last non-disabled tab. |
| Enter | Activates the selected tab. |

## Configuration Options

### Data Attributes

Data attributes are added directly to HTML elements to configure tab behavior.

| Attribute | Target Element | Type | Default | Description |
| --- | --- | --- | --- | --- |
| `data-hs-tabs` | Tablist container | object (JSON) | - | Contains configuration options for customizing tab behavior and appearance. |
| `:eventType` | Inside `data-hs-tabs` | `"click"` \| `"hover"` | `"click"` | Set `hover` to activate tabs on mouseover, or `click` for manual clicks (the default). |
| `data-hs-tab` | Tab button (trigger) | string (CSS selector) | - | Activate a tab by specifying the selector of the target tabpanel. This must be a valid CSS selector pointing to the tabpanel element. |
| `data-hs-tab-select` | Tablist container | string (ID) | - | You can pass a select ID there. Each option of this select must have a value equal to the tab ID (e.g., `<option value="#hs-tab-to-select-first">Tab 1</option>`). When you select this value, the corresponding tab will be opened. |

**Example:**
```html
<nav data-hs-tabs='{"eventType": "hover"}' aria-label="Tabs" role="tablist">
  <button data-hs-tab="#hs-tab-first" role="tab">Tab 1</button>
</nav>
```

### Tailwind Modifiers

| Name | Description |
| --- | --- |
| `hs-tab-active:*` | A modifier that allows you to set Tailwind classes when the tab is active. Can be applied to both the toggle button and the content panel. |

## JavaScript API

The `HSTabs` object is available in the global `window` object after the plugin is loaded.

### Instance Methods

These methods are called on a tabs instance.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `destroy()` | None | `void` | Destroys the tabs instance, removes all generated markup, classes, and event listeners. Use when removing tabs from DOM. |

### Static Methods

These methods are called directly on the `HSTabs` class.

| Method | Parameters | Return Type | Description |
| --- | --- | --- | --- |
| `HSTabs.getInstance(target, isInstance)` | `target`: `HTMLElement \| string` (CSS selector)<br>`isInstance`: `boolean` (optional) | `HSTabs \| { id: string \| number, element: HSTabs } \| null` | Returns the tabs instance associated with the target. If `isInstance` is `true`, returns collection item object `{ id, element }` where `element` is the `HSTabs` instance. If `isInstance` is `false` or omitted, returns the `HSTabs` instance directly. Returns `null` if tabs instance is not found. |
| `HSTabs.open(target)` | `target`: `HTMLElement` | `void` | Opens the tab identified by target. The target should be a tab button element. Static method to programmatically switch tabs. |

### Usage Examples

**Example 1: Destroying tabs instance**
```javascript
const instance = HSTabs.getInstance('#hs-tabs', true);

if (instance) {
  const { element } = instance;
  const destroyBtn = document.querySelector('#hs-destroy-btn');

  destroyBtn.addEventListener('click', () => {
    element.destroy();
  });
}
```

**Example 2: Opening tab programmatically (static method)**
```javascript
const openBtn = document.querySelector('#hs-open-btn');

openBtn.addEventListener('click', () => {
  const tabButton = document.querySelector('#hs-tab-first');
  
  if (tabButton) {
    HSTabs.open(tabButton);
  }
});
```

**Example 3: Getting instance and using methods (recommended pattern)**
```javascript
// Get the tabs instance
const instance = HSTabs.getInstance('#hs-tabs', true);

if (instance) {
  const { element } = instance;

  // Access instance properties
  console.log('Current tab:', element.current);
  console.log('Current content:', element.currentContent);

  // Clean up when removing from DOM
  function removeTabs() {
    element.destroy();
  }
}
```

## Events

Tabs instances emit events that can be listened to for lifecycle hooks and custom behavior.

| Event Name | When Fired | Callback Parameter | Description |
| --- | --- | --- | --- |
| `on:change` | When any tab is changed | `{ el: HTMLElement, prev: string, current: string }` | Fires when a tab is switched. Returns an object with:<br>- `el`: The toggle button element that was clicked<br>- `prev`: Previous tab ID<br>- `current`: Current tab ID |

### Event Usage Example

```javascript
// Get tabs instance
const instance = HSTabs.getInstance('#hs-tabs', true);

if (instance) {
  const { element } = instance;

  // Listen to change event
  element.on('change', ({ el, prev, current }) => {
    console.log('Tab changed:', {
      clickedButton: el,
      previousTab: prev,
      currentTab: current
    });
    // Perform actions after tab changes
    // e.g., load content, update UI, track analytics
  });
}
```

## Common Patterns

### Pattern 1: Hover Activation

Activate tabs on hover instead of click.

```html
<nav data-hs-tabs='{"eventType": "hover"}' aria-label="Tabs" role="tablist">
  <button data-hs-tab="#hs-tab-first" role="tab">Tab 1</button>
  <button data-hs-tab="#hs-tab-second" role="tab">Tab 2</button>
</nav>
```

### Pattern 2: Programmatic Control

Control tabs from external buttons.

```html
<nav id="hs-tabs-first" aria-label="Tabs" role="tablist">
  <button data-hs-tab="#hs-tab-content-first" role="tab">Tab 1</button>
  <button data-hs-tab="#hs-tab-content-second" role="tab">Tab 2</button>
</nav>

<button id="hs-open-tab-first">Open Tab 1</button>
<button id="hs-open-tab-second">Open Tab 2</button>

<script>
  document.querySelector('#hs-open-tab-first').addEventListener('click', () => {
    const tabButton = document.querySelector('[data-hs-tab="#hs-tab-content-first"]');

    if (tabButton) {
      HSTabs.open(tabButton);
    }
  });

  document.querySelector('#hs-open-tab-second').addEventListener('click', () => {
    const tabButton = document.querySelector('[data-hs-tab="#hs-tab-content-second"]');

    if (tabButton) {
      HSTabs.open(tabButton);
    }
  });
</script>
```

## License

Copyright (c) 2026 Preline Labs.

Licensed under the [MIT License](https://opensource.org/licenses/MIT).
