UNPKG

@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
# @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!