@get-set/gs-tabs
v1.0.2
Published
Get-Set Tabs
Maintainers
Readme
GSTabs
A dependency-free, feature-rich tabs / tab-panels component available in four flavours from one codebase:
- Native / vanilla JS — a
window.GSTabs(selector | element, params)factory plus awindow.GSTabsConfigueinstance registry. - jQuery —
$(selector).GSTabs(params)(auto-registered when jQuery is present). - Prototype —
element.GSTabs(params)on anyHTMLElement. - React — a
<GSTabs />component with a typed imperative ref handle.
All four share the exact same engine (actions/, constants/, helpers/, types/), so behaviour is identical across targets.
Features
- 5 visual variants —
line,underline,pill,enclosed,segment. - Animated active-indicator that smoothly slides between tabs (line / underline).
- Orientation
horizontal/verticaland tabPositiontop/bottom/left/right(left/right imply vertical). - 8 panel transitions —
none,fade,slide,slide-up,slide-down,scale,fade-scale,flip— with tunableduration+easing, all honoringprefers-reduced-motion. - Controlled + uncontrolled active tab (
activeTab/defaultActiveTab, by id or index). - Lazy panels (
lazy) that mount on first activation, with optionalkeepMounted. - Editable tabs —
closable(per-tab × button) andaddable(+ button withonAdd). - Draggable reorder of tabs.
- Per-tab icon, badge, disabled, closable.
- Overflow handling when tabs exceed the width —
scroll(prev/next buttons),menu,wrap,none. - Alignment —
start/center/end/stretch/justify(plus astretchshorthand). - Full keyboard navigation — Arrow keys (orientation + RTL aware), Home/End, Enter/Space, with a roving tabindex.
- ARIA — proper
tablist/tab/tabpanelroles,aria-selected,aria-controls,aria-labelledby,aria-orientation. - Theming —
light/dark/auto(follows OS) plus a customaccentcolor token. - RTL layout, 3 size presets (
sm/md/lg), per-partcustomClassoverrides. - Two data sources — a
tabsconfig array or markup children carryingdata-gs-*attributes. - Lifecycle callbacks —
beforeChange,onChange,onClose,onAdd,onReorder. - React-only
gsxprop for scoped inline styles.
Installation
npm i @get-set/gs-tabsCompatibility
| Target | Requirement |
|---|---|
| React (the <GSTabs> component) | React 16.8+ (Hooks), and 17 / 18 / 19. React is an optional peer dependency — you only need it for the component. |
| Native / jQuery / Prototype (window.GSTabs) | No framework. Any modern evergreen browser. |
| TypeScript | First-class — type declarations (.d.ts) ship in the package. |
| SSR / Next.js | Safe to import server-side (no DOM access at module load). Render the component inside a Client Component ('use client'). |
Package entry points
From package.json:
| Field | Value |
|---|---|
| name | @get-set/gs-tabs |
| main | dist/components/GSTabs.js |
| module | dist/components/GSTabs.js |
| types | dist/components/GSTabs.d.ts |
| exports['.'].import | ./dist/components/GSTabs.js (React / ESM) |
| exports['.'].require | ./dist-js/bundle.js (native window bundle) |
| files | dist, dist-js, styles, example.html |
Project layout
GSTabs/
├─ GSTabs.ts # native window.GSTabs factory (entry)
├─ actions/ # init / setActive / add / close / destroy / refresh …
├─ constants/ # animations, variants, sizes, defaultParams
├─ helpers/ # tabs, layout, keyboard, indicator, draggable, animate
├─ types/ # params, ref, global.d.ts
├─ components/
│ ├─ GSTabs.tsx # React <GSTabs> component
│ └─ styles/ # GSTabs.css / .scss / GSTabsCSS.ts (bundled string)
├─ styles/ # GSTabs.css / .scss (shippable stylesheet)
└─ example.html # native showcaseThe stylesheet is kept in 5 synced sources (styles/GSTabs.css, styles/GSTabs.scss, components/styles/GSTabs.css, components/styles/GSTabs.scss, components/styles/GSTabsCSS.ts). The React component injects GSTabsCSS.ts automatically; for the native / jQuery / prototype targets, include styles/GSTabs.css yourself.
Vanilla (window factory)
The factory accepts a CSS selector string OR a DOM element as its first argument, then a params object.
<link rel="stylesheet" href="node_modules/@get-set/gs-tabs/styles/GSTabs.css" />
<script src="node_modules/@get-set/gs-tabs/dist-js/bundle.js"></script>
<div id="tabs"></div>
<script>
// selector string …
const tabs = window.GSTabs('#tabs', {
reference: 'main',
variant: 'underline',
animation: 'slide',
tabs: [
{ id: 'home', label: 'Home', content: '<p>Welcome home.</p>' },
{ id: 'profile', label: 'Profile', badge: 3, content: '<p>Your profile.</p>' },
{ id: 'settings', label: 'Settings', content: '<p>Settings.</p>', disabled: false }
],
onChange: (id, prev) => console.log('changed', prev, '→', id)
});
// … OR an element (both are supported):
const el = document.getElementById('tabs');
// window.GSTabs(el, { … });
tabs.setActive('profile');
tabs.next();
console.log(tabs.getActive());
</script>Markup (data-attribute) mode
Instead of a tabs array, describe tabs with data-gs-tab children:
<div id="docs">
<div data-gs-tab data-gs-label="Overview" data-gs-id="ov">
<p>Overview content…</p>
</div>
<div data-gs-tab data-gs-label="API" data-gs-id="api" data-gs-badge="new">
<p>API content…</p>
</div>
<div data-gs-tab data-gs-label="Legacy" data-gs-id="legacy" data-gs-disabled>
<p>Legacy content…</p>
</div>
</div>
<script>window.GSTabs('#docs', { variant: 'pill' });</script>Supported data attributes: data-gs-tab (marks a tab), data-gs-label, data-gs-id, data-gs-icon, data-gs-badge, data-gs-disabled, data-gs-closable.
The instance registry
window.GSTabs('#tabs', { reference: 'main', tabs: [...] });
const instance = window.GSTabsConfigue.instance('main');
instance.setActive(1);jQuery
When jQuery is present on the page, $.fn.GSTabs is registered automatically by the bundle:
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="node_modules/@get-set/gs-tabs/dist-js/bundle.js"></script>
<div class="tabs"></div>
<script>
$('.tabs').GSTabs({
variant: 'segment',
tabs: [
{ id: 'a', label: 'Day', content: '<p>Day view</p>' },
{ id: 'b', label: 'Week', content: '<p>Week view</p>' },
{ id: 'c', label: 'Month', content: '<p>Month view</p>' }
]
});
</script>$('.tabs') may match multiple elements — each is initialized independently.
Prototype
Every HTMLElement gets a GSTabs method (also registered by the bundle):
<script src="node_modules/@get-set/gs-tabs/dist-js/bundle.js"></script>
<div id="tabs"></div>
<script>
document.getElementById('tabs').GSTabs({
variant: 'enclosed',
tabPosition: 'left',
tabs: [
{ id: 'g', label: 'General', content: '<p>General</p>' },
{ id: 's', label: 'Security', content: '<p>Security</p>' }
]
});
</script>Both the jQuery and prototype adapters call
new window.GSTabs(this, params)— passing the element (not a selector). The factory accepts an element as its first argument precisely so these adapters work.
React
'use client';
import { useRef } from 'react';
import GSTabs, { GSTabsHandle } from '@get-set/gs-tabs';
export default function Example() {
const ref = useRef<GSTabsHandle>(null);
return (
<>
<GSTabs
ref={ref}
reference="main"
variant="underline"
animation="slide"
defaultActiveTab="home"
onChange={(id, prev) => console.log(prev, '→', id)}
tabs={[
{ id: 'home', label: 'Home', content: <p>Welcome home.</p> },
{ id: 'profile', label: 'Profile', badge: 3, content: <p>Your profile.</p> },
{ id: 'settings', label: 'Settings', content: <p>Settings.</p> }
]}
/>
<button onClick={() => ref.current?.next()}>Next</button>
<button onClick={() => ref.current?.setActive('profile')}>Go to profile</button>
</>
);
}You can also render tabs from children with data-gs-tab:
<GSTabs variant="pill">
<div data-gs-tab data-gs-label="One">
<p>Panel one</p>
</div>
<div data-gs-tab data-gs-label="Two" data-gs-badge="new">
<p>Panel two</p>
</div>
</GSTabs>For rich labels/content in config mode, use labelNode / contentNode / iconNode:
<GSTabs
tabs={[
{ id: 'x', labelNode: <strong>Bold</strong>, contentNode: <MyPanel /> }
]}
/>The imperative handle (GSTabsHandle)
| Method | Signature | Description |
|---|---|---|
| setActive | (idOrIndex: string \| number) => void | Activate a tab by id or index. |
| next | () => void | Activate the next enabled tab (wraps). |
| prev | () => void | Activate the previous enabled tab (wraps). |
| getActive | () => string \| null | The currently active tab id. |
| add | (tab?: GSTabsItem, activate?: boolean) => string \| undefined | Add a tab (optionally activate). Returns its id. |
| close | (idOrIndex: string \| number) => void | Close (remove) a tab. |
| refresh | () => void | Re-measure the indicator / overflow. |
| destroy | () => void | Remove all tabs (uncontrolled teardown). |
The gsx prop (React only)
Scoped inline styles that are injected into <head> for this instance only (scoped via [data-key]) and removed on unmount:
<GSTabs
gsx={{ '.gs-tabs-tab': { fontWeight: '600' }, '.gs-tabs-indicator': { height: '4px' } }}
tabs={[...]}
/>Native imperative API
The window.GSTabs(...) factory returns a ref with the same actions as the React handle:
| Method | Description |
|---|---|
| setActive(idOrIndex) | Activate a tab by id or index. |
| next() | Activate the next enabled tab (wraps). |
| prev() | Activate the previous enabled tab (wraps). |
| getActive() | The currently active tab id (or null). |
| add(tab?, activate?) | Add a tab (optionally activate). Returns its id. |
| close(idOrIndex) | Close (remove) a tab. |
| refresh() | Rebuild from the current params, preserving the active tab. |
| destroy() | Tear down and empty the root, unregistering the instance. |
Props
Every option (native params & React props share the same names):
| Prop | Type | Default | Description |
|---|---|---|---|
| reference | string | auto GUID | Stable registry key. |
| tabs | GSTabConfig[] | undefined | Tab definitions. When omitted, tabs are read from data-gs-tab markup. |
| activeTab | string \| number | undefined | Controlled active tab (id or index). |
| defaultActiveTab | string \| number | first enabled | Uncontrolled initial active tab. |
| orientation | 'horizontal' \| 'vertical' | 'horizontal' | Tablist axis. |
| tabPosition | 'top' \| 'bottom' \| 'left' \| 'right' | 'top' | Tablist placement (left/right imply vertical). |
| variant | 'line' \| 'underline' \| 'pill' \| 'enclosed' \| 'segment' | 'line' | Visual variant. |
| size | 'sm' \| 'md' \| 'lg' | 'md' | Size preset. |
| align | 'start' \| 'center' \| 'end' \| 'stretch' \| 'justify' | 'start' | Tab distribution. |
| stretch | boolean | false | Shorthand for align: 'stretch'. |
| lazy | boolean | false | Mount a panel only on first activation. |
| keepMounted | boolean | true | Keep lazily-mounted panels after deactivation. |
| animation | GSTabsAnimation \| false | 'fade' | Panel transition (false disables). |
| animationDuration | number \| { enter?: number } | 260 | Transition duration in ms. |
| animationEasing | string | cubic-bezier(.4,0,.2,1) | Transition + indicator easing. |
| animatedIndicator | boolean | true | Animate the sliding indicator bar. |
| reducedMotion | 'auto' \| boolean | 'auto' | Honor prefers-reduced-motion. |
| closable | boolean | false | Show a × on every tab (per-tab closable overrides). |
| addable | boolean | false | Show a trailing + button. |
| addButtonLabel | string | '+' | Add button label. |
| draggable | boolean | false | Reorder tabs by dragging. |
| overflow | 'scroll' \| 'menu' \| 'wrap' \| 'none' | 'scroll' | Overflow handling. |
| theme | 'light' \| 'dark' \| 'auto' | 'auto' | Visual theme. |
| accent | string | #2563eb | Accent color (CSS var --gst-accent). |
| rtl | boolean | false | Right-to-left layout. |
| customClass | GSTabsCustomClass | undefined | Per-part class overrides. |
| className | string | undefined | Extra class on the root. |
| gsx | NestedCSS | undefined | React only. Scoped inline styles. |
| beforeChange | (nextId, prevId) => void \| boolean | — | Return false to cancel a change. |
| onChange | (id, prevId) => void | — | Fired after the active tab changes. |
| onClose | (id) => void | — | Fired when a tab is closed. |
| onAdd | () => GSTabConfig \| void | — | Fired by the + button; may return a new tab. |
| onReorder | (from, to) => void | — | Fired after a drag reorder. |
GSTabConfig (a single tab)
| Field | Type | Description |
|---|---|---|
| id | string | Stable id (auto-generated from the label when omitted). |
| label | string | Tab label (HTML string for native). |
| labelNode | ReactNode | React only. Rich label (takes precedence over label). |
| content | string | Panel content (HTML string for native). |
| contentNode | ReactNode | React only. Rich panel content. |
| icon | string | Leading icon (HTML string). |
| iconNode | ReactNode | React only. Rich icon. |
| badge | string \| number | Badge shown after the label. |
| disabled | boolean | Disable the tab (skipped in keyboard nav). |
| closable | boolean | Show a × on this tab. |
| className | string | Extra class on the tab button. |
customClass parts
root, tablist, tab, activeTab, panel, indicator, addButton, scrollButton.
Catalogs
Variants
| Variant | Look |
|---|---|
| line | Rail under the tablist + a thin sliding accent bar. |
| underline | A thicker sliding underline, no rail. |
| pill | Rounded, filled active tab. |
| enclosed | Card-like active tab joined to the panel. |
| segment | iOS-style segmented control on a tinted track. |
Panel animations
none, fade, slide, slide-up, slide-down, scale, fade-scale, flip.
Sizes
sm, md, lg.
Overflow modes
| Mode | Behaviour |
|---|---|
| scroll | Prev/next scroll buttons appear when tabs overflow. |
| menu | Overflow tabs collapse into a dropdown (styling hook). |
| wrap | Tabs wrap to multiple rows. |
| none | Overflow is clipped. |
Keyboard support
| Key | Action |
|---|---|
| ArrowRight / ArrowLeft | Move focus (horizontal). RTL-aware. |
| ArrowDown / ArrowUp | Move focus (vertical). |
| Home / End | First / last enabled tab. |
| Enter / Space | Activate the focused tab. |
| Tab | Enters/leaves the tablist (roving tabindex — only the active tab is tabbable). |
Focus follows selection (automatic activation). Disabled tabs are skipped.
License
ISC © Get-Set
