@mshafiqyajid/react-tabs
Headless tabs hook and styled component for React. Line, solid, and pill variants with a ResizeObserver-driven sliding indicator. Closeable tabs, drag-to-reorder, horizontal scrolling, lazy/force mount, typeahead navigation, controlled and uncontrolled, full keyboard navigation, ARIA-compliant.
Playground #
Overview content goes here.
Features content goes here.
Pricing content goes here.
import { TabsStyled } from "@mshafiqyajid/react-tabs/styled";
import "@mshafiqyajid/react-tabs/styles.css";
<TabsStyled
tabs={tabs}
defaultValue="overview"
/>Install #
npm install @mshafiqyajid/react-tabs Quick start #
import { TabsStyled } from "@mshafiqyajid/react-tabs/styled";
import "@mshafiqyajid/react-tabs/styles.css";
const tabs = [
{ value: "overview", label: "Overview", content: <p>Overview</p> },
{ value: "features", label: "Features", content: <p>Features</p> },
];
<TabsStyled tabs={tabs} defaultValue="overview" /> Controlled #
const [tab, setTab] = useState("overview");
<TabsStyled tabs={tabs} value={tab} onChange={setTab} /> Variants #
<TabsStyled tabs={tabs} variant="line" /> {/* default */}
<TabsStyled tabs={tabs} variant="solid" />
<TabsStyled tabs={tabs} variant="pill" /> Disabled tab #
const tabs = [
{ value: "a", label: "Active", content: <p>A</p> },
{ value: "b", label: "Disabled", content: <p>B</p>, disabled: true },
]; Closeable tabs #
Add closable: true (or the alias closeable) to a tab item to render a × button. Handle removals with onTabClose (or alias onClose).
const [tabs, setTabs] = useState([
{ value: "a", label: "Tab A", content: <p>A</p>, closable: true },
{ value: "b", label: "Tab B", content: <p>B</p>, closable: true },
]);
<TabsStyled
tabs={tabs}
onTabClose={(value) => setTabs((t) => t.filter((tab) => tab.value !== value))}
/> Scrollable tab list #
Set scrollable to scroll the tab list horizontally instead of wrapping. Chevron buttons appear at the edges.
<TabsStyled tabs={manyTabs} scrollable /> Drag reorder #
Set sortable to enable HTML5 drag-to-reorder. The onReorder callback receives the new values array. Use reorderable instead if you prefer onReorder(fromIndex, toIndex).
const [tabs, setTabs] = useState(myTabs);
<TabsStyled
tabs={tabs}
sortable
onReorder={(values) =>
setTabs((prev) => values.map((v) => prev.find((t) => t.value === v)!))
}
/> Lazy / force mount #
// Only render panel content after first activation
<TabsStyled tabs={tabs} lazyMount />
// Always keep all panels in the DOM
<TabsStyled tabs={tabs} forceMount /> Headless #
import { useTabs } from "@mshafiqyajid/react-tabs";
const { activeValue, getTabProps, getPanelProps } = useTabs({
tabs: [{ value: "a" }, { value: "b" }],
defaultValue: "a",
});
<div role="tablist">
<button {...getTabProps("a")}>Tab A</button>
<button {...getTabProps("b")}>Tab B</button>
</div>
<div {...getPanelProps("a")}>Content A</div>
<div {...getPanelProps("b")}>Content B</div> API #
TabItem shape: { value, label, content, disabled?, closable?, closeable? }
| Prop | Type | Default | Description |
|---|---|---|---|
| tabs | TabItem[] | — | Tab definitions (see shape above) |
| variant | "line" | "solid" | "pill" | "line" | Visual style |
| size | "sm" | "md" | "lg" | "md" | Size variant |
| tone | "neutral" | "primary" | "success" | "danger" | "neutral" | Color tone |
| defaultValue | string | first enabled | Initially active tab (uncontrolled) |
| value | string | — | Controlled active tab |
| onChange | (value: string, reason: TabsChangeReason) => void | — | Called when active tab changes. reason is "click" | "keyboard" | "programmatic" |
| activation / activationMode | "automatic" | "manual" | "automatic" | Arrow keys move focus AND activate (auto) or only move focus (manual) |
| orientation | "horizontal" | "vertical" | "horizontal" | Affects keyboard nav direction |
| scrollable | boolean | false | Tab list scrolls horizontally with chevron buttons at edges |
| scrollActiveIntoView | boolean | true when scrollable | Auto-scroll active tab into view on activation |
| lazyMount | boolean | false | Only mount panel content after first activation |
| forceMount | boolean | false | Keep all panels mounted regardless of activation |
| onTabClose / onClose | (value: string) => void | — | Fires when the × button on a closable tab is clicked (aliases) |
| sortable | boolean | false | Enable drag reorder; onReorder receives the new values array |
| reorderable | boolean | false | Enable drag reorder; onReorder receives (fromIndex, toIndex) |
| onReorder | (values: string[]) => void or (from: number, to: number) => void | — | Called after drag reorder (signature depends on sortable vs reorderable) |
| renderTab | (ctx: TabsRenderTabContext) => ReactNode | — | Custom tab button content; button shell and a11y attrs stay owned by the component |
| renderPanel | (ctx: TabsRenderPanelContext) => ReactNode | — | Custom panel rendering |
| className | string | — | Extra class on the root element |
| style | CSSProperties | — | Inline style override |
Typeahead is built in: pressing a letter key while the tab list is focused jumps to the next tab whose string label starts with the typed buffer (600 ms reset).