@ehsaneha/react-primitive-tab
Version:
A headless, type-safe, and hook-based tab primitive for React with full control over state management using react-observable-store.
152 lines (110 loc) • 4.78 kB
Markdown
# @ehsaneha/react‑primitive‑tab
A **headless**, zero‑dependency tab primitive for React.
It ships **only logic**—no markup, no styling—so you can render **exactly** the HTML you want and theme it however you like.
Under the hood it leverages [`react-observable-store`](https://www.npmjs.com/package/react-observable-store) for rock‑solid, hook‑friendly state management.
```bash
npm i @ehsaneha/react-primitive-tab
```
## Quick look
```tsx
import React from "react";
import { Tab, TabContent, useTabTrigger } from "@ehsaneha/react-primitive-tab";
export default function ProfileTabs() {
return (
<Tab initIndex="posts">
<TabList />
<TabPanels />
</Tab>
);
}
function TabList() {
const tabs = [
{ id: "posts", label: "Posts" },
{ id: "likes", label: "Likes" },
{ id: "about", label: "About" },
];
return (
<div className="flex gap-2 border-b">
{tabs.map((t) => (
<Trigger key={t.id} index={t.id} label={t.label} />
))}
</div>
);
}
function Trigger({ index, label }: { index: string; label: string }) {
const [selected, select] = useTabTrigger({ index });
return (
<button
className={selected ? "border-b-2 font-medium" : "opacity-60"}
onClick={select}
>
{label}
</button>
);
}
function TabPanels() {
return (
<>
<TabContent index="posts">/* … */</TabContent>
<TabContent index="likes">/* … */</TabContent>
<TabContent index="about">/* … */</TabContent>
</>
);
}
```
<details>
<summary>🎬 How it works</summary>
1. **`<Tab initIndex>`** creates an internal store that keeps the active tab’s index.
2. All children rendered inside `<Tab>` gain access to that store through React context.
3. **`useTabTrigger({ index })`** returns `[isSelected, activate]` for toggling any UI element.
4. **`<TabContent index>`** shows its children only when the given index is active.
</details>
## API
| Export | Description |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `Tab<S>` | Context provider. `initIndex` _(S)_ – initial tab key/value. |
| `useTabTrigger<S>( { index } )` | Hook → `[isSelected: boolean, activate: () ⇒ void]`. Call inside any descendant of `Tab` to build a trigger (button, link, etc.). |
| `TabContent<S>` | Renders children when its `index` matches the active one. |
| `useTabContext()` | Low‑level access to the raw context (`{ indexStore }`). Use only if you need custom behaviour. |
| `useTab` | Internal helper that creates the store; exposed for advanced composition. |
> **Generic type `S`**
> The index can be anything that’s comparable via `===`—string, number, enum, union, etc.
## Styling & accessibility
Because the library is headless you have full control:
- Render `button`, `a`, `li > button`, or any custom component.
- Add ARIA roles (`role="tablist"`, `role="tab"`, `aria-selected`, etc.) as you see fit.
- Apply your design‑system classes (Tailwind, CSS‑in‑JS, CSS Modules, …).
## TypeScript support
Everything is typed end‑to‑end.
Pass a union literal to `initIndex` and enjoy autocompletion & type‑safe triggers/contents.
```ts
type Section = "overview" | "settings" | "billing";
<Tab<Section> initIndex="overview"> … </Tab>;
```
## Why another tab library?
- **Headless first** – no hard‑coded markup or styles.
- **Tiny** – few lines of code, tree‑shake friendly.
- **Composable** – built with hooks, not render props.
- **Framework agnostic index** – use strings, numbers, enums, whatever.
- **Powered by observable stores** – avoids prop drilling and re‑renders only when needed.
## Installation
```bash
# npm
npm install @ehsaneha/react-primitive-tab
# pnpm
pnpm add @ehsaneha/react-primitive-tab
# yarn
yarn add @ehsaneha/react-primitive-tab
```
Peer dependency: **React ≥ 16.8** (hooks).
## License
This package is licensed under the MIT License. See LICENSE for more information.
Feel free to modify or contribute to this package!