@ehsaneha/react-primitive-tab
v1.0.1
Published
A headless, type-safe, and hook-based tab primitive for React with full control over state management using react-observable-store.
Downloads
6
Maintainers
Readme
@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 for rock‑solid, hook‑friendly state management.
npm i @ehsaneha/react-primitive-tabQuick look
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>
</>
);
}<Tab initIndex>creates an internal store that keeps the active tab’s index.- All children rendered inside
<Tab>gain access to that store through React context. useTabTrigger({ index })returns[isSelected, activate]for toggling any UI element.<TabContent index>shows its children only when the given index is active.
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
SThe 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.
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
# npm
npm install @ehsaneha/react-primitive-tab
# pnpm
pnpm add @ehsaneha/react-primitive-tab
# yarn
yarn add @ehsaneha/react-primitive-tabPeer 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!
