@data-slot/tabs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @data-slot/tabs
2
+
3
+ Headless tabs component for vanilla JavaScript. Accessible, unstyled, tiny.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @data-slot/tabs
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```html
14
+ <div data-slot="tabs" data-default-value="one">
15
+ <div data-slot="tabs-list">
16
+ <button data-slot="tabs-trigger" data-value="one">Tab One</button>
17
+ <button data-slot="tabs-trigger" data-value="two">Tab Two</button>
18
+ <button data-slot="tabs-trigger" data-value="three">Tab Three</button>
19
+ <div data-slot="tabs-indicator"></div>
20
+ </div>
21
+ <div data-slot="tabs-content" data-value="one">Content One</div>
22
+ <div data-slot="tabs-content" data-value="two">Content Two</div>
23
+ <div data-slot="tabs-content" data-value="three">Content Three</div>
24
+ </div>
25
+
26
+ <script type="module">
27
+ import { create } from "@data-slot/tabs";
28
+
29
+ const controllers = create();
30
+ </script>
31
+ ```
32
+
33
+ ## API
34
+
35
+ ### `create(scope?)`
36
+
37
+ Auto-discover and bind all tabs instances in a scope (defaults to `document`).
38
+
39
+ ```typescript
40
+ import { create } from "@data-slot/tabs";
41
+
42
+ const controllers = create(); // Returns TabsController[]
43
+ ```
44
+
45
+ ### `createTabs(root, options?)`
46
+
47
+ Create a controller for a specific element.
48
+
49
+ ```typescript
50
+ import { createTabs } from "@data-slot/tabs";
51
+
52
+ const tabs = createTabs(element, {
53
+ defaultValue: "one",
54
+ orientation: "horizontal",
55
+ onValueChange: (value) => console.log(value),
56
+ });
57
+ ```
58
+
59
+ ### Options
60
+
61
+ | Option | Type | Default | Description |
62
+ |--------|------|---------|-------------|
63
+ | `defaultValue` | `string` | First trigger's value | Initial selected tab |
64
+ | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | Tab orientation for keyboard nav |
65
+ | `onValueChange` | `(value: string) => void` | `undefined` | Callback when selected tab changes |
66
+
67
+ ### Controller
68
+
69
+ | Method/Property | Description |
70
+ |-----------------|-------------|
71
+ | `select(value)` | Select a tab by value |
72
+ | `value` | Currently selected value (readonly `string`) |
73
+ | `destroy()` | Cleanup all event listeners |
74
+
75
+ ## Markup Structure
76
+
77
+ ```html
78
+ <div data-slot="tabs" data-default-value="initial-tab">
79
+ <div data-slot="tabs-list">
80
+ <button data-slot="tabs-trigger" data-value="unique-id">Label</button>
81
+ <!-- Optional animated indicator -->
82
+ <div data-slot="tabs-indicator"></div>
83
+ </div>
84
+ <div data-slot="tabs-content" data-value="unique-id">Panel content</div>
85
+ </div>
86
+ ```
87
+
88
+ ### Optional Slots
89
+
90
+ - `tabs-indicator` - Animated highlight that follows the selected tab
91
+
92
+ ## Styling
93
+
94
+ ### Basic Styling
95
+
96
+ ```css
97
+ /* Hidden panels */
98
+ [data-slot="tabs-content"][hidden] {
99
+ display: none;
100
+ }
101
+
102
+ /* Active trigger */
103
+ [data-slot="tabs-trigger"][aria-selected="true"] {
104
+ font-weight: bold;
105
+ border-bottom: 2px solid currentColor;
106
+ }
107
+
108
+ /* Or use data-state */
109
+ [data-slot="tabs-trigger"][data-state="active"] {
110
+ color: blue;
111
+ }
112
+
113
+ [data-slot="tabs-trigger"][data-state="inactive"] {
114
+ color: gray;
115
+ }
116
+ ```
117
+
118
+ ### Animated Indicator
119
+
120
+ The indicator receives CSS variables for positioning:
121
+
122
+ ```css
123
+ [data-slot="tabs-indicator"] {
124
+ position: absolute;
125
+ left: var(--active-tab-left);
126
+ width: var(--active-tab-width);
127
+ height: 2px;
128
+ background: currentColor;
129
+ transition: left 0.2s, width 0.2s;
130
+ }
131
+
132
+ /* Vertical orientation */
133
+ [data-slot="tabs-list"][aria-orientation="vertical"] [data-slot="tabs-indicator"] {
134
+ top: var(--active-tab-top);
135
+ height: var(--active-tab-height);
136
+ width: 2px;
137
+ }
138
+ ```
139
+
140
+ ### CSS Variables
141
+
142
+ | Variable | Description |
143
+ |----------|-------------|
144
+ | `--active-tab-left` | Left offset of active trigger |
145
+ | `--active-tab-width` | Width of active trigger |
146
+ | `--active-tab-top` | Top offset of active trigger |
147
+ | `--active-tab-height` | Height of active trigger |
148
+
149
+ ### Tailwind Example
150
+
151
+ ```html
152
+ <div data-slot="tabs">
153
+ <div data-slot="tabs-list" class="relative flex border-b">
154
+ <button
155
+ data-slot="tabs-trigger"
156
+ data-value="one"
157
+ class="px-4 py-2 aria-selected:text-blue-600"
158
+ >
159
+ Tab One
160
+ </button>
161
+ <div
162
+ data-slot="tabs-indicator"
163
+ class="absolute bottom-0 h-0.5 bg-blue-600 transition-all"
164
+ style="left: var(--active-tab-left); width: var(--active-tab-width)"
165
+ ></div>
166
+ </div>
167
+ <div data-slot="tabs-content" data-value="one" class="p-4">
168
+ Content
169
+ </div>
170
+ </div>
171
+ ```
172
+
173
+ ## Keyboard Navigation
174
+
175
+ ### Horizontal Orientation
176
+
177
+ | Key | Action |
178
+ |-----|--------|
179
+ | `ArrowLeft` | Select previous tab |
180
+ | `ArrowRight` | Select next tab |
181
+ | `Home` | Select first tab |
182
+ | `End` | Select last tab |
183
+
184
+ ### Vertical Orientation
185
+
186
+ | Key | Action |
187
+ |-----|--------|
188
+ | `ArrowUp` | Select previous tab |
189
+ | `ArrowDown` | Select next tab |
190
+ | `Home` | Select first tab |
191
+ | `End` | Select last tab |
192
+
193
+ ## Accessibility
194
+
195
+ The component automatically handles:
196
+
197
+ - `role="tablist"` on list
198
+ - `role="tab"` on triggers
199
+ - `role="tabpanel"` on content
200
+ - `aria-orientation` on list
201
+ - `aria-selected` on triggers
202
+ - `aria-controls` linking triggers to panels
203
+ - `aria-labelledby` linking panels to triggers
204
+ - `tabindex` management (only selected tab is in tab order)
205
+
206
+ ## Events
207
+
208
+ Listen for changes via custom events:
209
+
210
+ ```javascript
211
+ element.addEventListener("tabs:change", (e) => {
212
+ console.log("Selected tab:", e.detail.value);
213
+ });
214
+ ```
215
+
216
+ ## License
217
+
218
+ MIT
219
+
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;l<u;l++)d=c[l],!a.call(e,d)&&d!==o&&t(e,d,{get:(e=>i[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},s=(n,r,a)=>(a=n==null?{}:e(i(n)),o(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n));const c=s(require(`@data-slot/core`)),l=`a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])`;function u(e,t={}){let{onValueChange:n,orientation:r=`horizontal`,activationMode:i=`auto`}=t,a=(0,c.getPart)(e,`tabs-list`),o=(0,c.getParts)(e,`tabs-trigger`),s=(0,c.getParts)(e,`tabs-content`),u=(0,c.getPart)(e,`tabs-indicator`);if(!a||o.length===0)throw Error(`Tabs requires tabs-list and at least one tabs-trigger`);let f=new Map;for(let e of s){let t=(e.dataset.value||``).trim();t&&f.set(t,e)}let p=[],m=new Map,h=new Map;for(let e of o){let t=(e.dataset.value||``).trim();if(!t)continue;let n=e.hasAttribute(`disabled`)||e.dataset.disabled!==void 0||e.getAttribute(`aria-disabled`)===`true`,r=f.get(t),i={el:e,value:t,disabled:n,panel:r};p.push(i),m.set(t,i),h.set(e,i)}let g=p.filter(e=>!e.disabled),_=new Map;g.forEach((e,t)=>_.set(e.value,t));let v=g[0]?.value||``,y=e,b=(t.defaultValue??y.dataset.defaultValue??``).trim(),x=m.get(b),S=x&&!x.disabled?b:v,C=[];a.setAttribute(`role`,`tablist`),r===`vertical`&&(0,c.setAria)(a,`orientation`,`vertical`);for(let e of p){let{el:t,disabled:n,panel:r}=e;t.setAttribute(`role`,`tab`);let i=(0,c.ensureId)(t,`tab`);if(t.tagName===`BUTTON`&&!t.hasAttribute(`type`)&&(t.type=`button`),n&&(t.setAttribute(`aria-disabled`,`true`),t.tagName===`BUTTON`&&(t.disabled=!0)),r){r.setAttribute(`role`,`tabpanel`),r.tabIndex=-1;let e=(0,c.ensureId)(r,`tabpanel`);t.setAttribute(`aria-controls`,e),r.setAttribute(`aria-labelledby`,i)}}let w=()=>{if(!u)return;let e=m.get(S);if(!e)return;let t=a.getBoundingClientRect(),n=e.el.getBoundingClientRect();u.style.setProperty(`--active-tab-left`,`${n.left-t.left}px`),u.style.setProperty(`--active-tab-width`,`${n.width}px`),u.style.setProperty(`--active-tab-top`,`${n.top-t.top}px`),u.style.setProperty(`--active-tab-height`,`${n.height}px`)},T=(t,r=!1)=>{if(t=t.trim(),S===t&&!r)return;let i=m.get(t);if(!i||i.disabled)if(r){if(t=v,!t)return}else return;let a=S!==t;S=t;for(let e of p){let n=e.value===t;(0,c.setAria)(e.el,`selected`,n),e.el.tabIndex=n&&!e.disabled?0:-1,e.el.dataset.state=n?`active`:`inactive`}for(let e of s){let n=(e.dataset.value||``).trim();if(!n)continue;let r=n===t;e.hidden=!r,e.dataset.state=r?`active`:`inactive`}e.setAttribute(`data-value`,t),u&&w(),a&&!r&&((0,c.emit)(e,`tabs:change`,{value:t}),n?.(t))};if(T(S,!0),u){let e=()=>requestAnimationFrame(w);C.push((0,c.on)(window,`resize`,e)),C.push((0,c.on)(a,`scroll`,e));let t=new ResizeObserver(e);t.observe(a),C.push(()=>t.disconnect())}C.push((0,c.on)(a,`click`,e=>{let t=e.target.closest?.(`[data-slot="tabs-trigger"]`);if(!t)return;let n=h.get(t);n&&!n.disabled&&T(n.value)}));let E=r===`horizontal`,D=E?`ArrowLeft`:`ArrowUp`,O=E?`ArrowRight`:`ArrowDown`;C.push((0,c.on)(a,`keydown`,e=>{let t=e.target.closest?.(`[data-slot="tabs-trigger"]`);if(!t)return;let n=h.get(t);if(!n||g.length===0)return;if(e.key===`Enter`||e.key===` `){e.preventDefault(),n.disabled||T(n.value);return}if(E&&e.key===`ArrowDown`&&n.value===S){let t=n.panel;if(t){e.preventDefault();let n=t.querySelector(l);(n||t).focus();return}}let r=_.get(n.value)??-1;r===-1&&(r=_.get(S)??0);let a=r;switch(e.key){case D:a=r-1,a<0&&(a=g.length-1);break;case O:a=r+1,a>=g.length&&(a=0);break;case`Home`:a=0;break;case`End`:a=g.length-1;break;default:return}e.preventDefault();let o=g[a];o&&(o.el.focus(),i===`auto`&&T(o.value))}));let k={select:e=>T(e),get value(){return S},updateIndicator:w,destroy:()=>{C.forEach(e=>e()),C.length=0,d.delete(e)}};return k}const d=new WeakSet;function f(e=document){let t=[];for(let n of(0,c.getRoots)(e,`tabs`)){if(d.has(n))continue;d.add(n),t.push(u(n))}return t}exports.create=f,exports.createTabs=u;
@@ -0,0 +1,48 @@
1
+ //#region src/index.d.ts
2
+ interface TabsOptions {
3
+ /** Initial selected tab value */
4
+ defaultValue?: string;
5
+ /** Callback when selected tab changes */
6
+ onValueChange?: (value: string) => void;
7
+ /** Tab orientation for keyboard navigation */
8
+ orientation?: "horizontal" | "vertical";
9
+ /**
10
+ * Activation mode for keyboard navigation
11
+ * - "auto": Arrow keys select tabs immediately (default)
12
+ * - "manual": Arrow keys move focus, Enter/Space activates
13
+ */
14
+ activationMode?: "auto" | "manual";
15
+ }
16
+ interface TabsController {
17
+ /** Select a tab by value */
18
+ select(value: string): void;
19
+ /** Currently selected value */
20
+ readonly value: string;
21
+ /** Update indicator position (call after layout changes) */
22
+ updateIndicator(): void;
23
+ /** Cleanup all event listeners */
24
+ destroy(): void;
25
+ }
26
+ /**
27
+ * Create a tabs controller for a root element
28
+ *
29
+ * Expected markup:
30
+ * ```html
31
+ * <div data-slot="tabs" data-default-value="two">
32
+ * <div data-slot="tabs-list">
33
+ * <button data-slot="tabs-trigger" data-value="one">Tab One</button>
34
+ * <button data-slot="tabs-trigger" data-value="two">Tab Two</button>
35
+ * </div>
36
+ * <div data-slot="tabs-content" data-value="one">Content One</div>
37
+ * <div data-slot="tabs-content" data-value="two">Content Two</div>
38
+ * </div>
39
+ * ```
40
+ */
41
+ declare function createTabs(root: Element, options?: TabsOptions): TabsController;
42
+ /**
43
+ * Find and bind all tabs components in a scope
44
+ * Returns array of controllers for programmatic access
45
+ */
46
+ declare function create(scope?: ParentNode): TabsController[];
47
+ //#endregion
48
+ export { TabsController, TabsOptions, create, createTabs };
@@ -0,0 +1,48 @@
1
+ //#region src/index.d.ts
2
+ interface TabsOptions {
3
+ /** Initial selected tab value */
4
+ defaultValue?: string;
5
+ /** Callback when selected tab changes */
6
+ onValueChange?: (value: string) => void;
7
+ /** Tab orientation for keyboard navigation */
8
+ orientation?: "horizontal" | "vertical";
9
+ /**
10
+ * Activation mode for keyboard navigation
11
+ * - "auto": Arrow keys select tabs immediately (default)
12
+ * - "manual": Arrow keys move focus, Enter/Space activates
13
+ */
14
+ activationMode?: "auto" | "manual";
15
+ }
16
+ interface TabsController {
17
+ /** Select a tab by value */
18
+ select(value: string): void;
19
+ /** Currently selected value */
20
+ readonly value: string;
21
+ /** Update indicator position (call after layout changes) */
22
+ updateIndicator(): void;
23
+ /** Cleanup all event listeners */
24
+ destroy(): void;
25
+ }
26
+ /**
27
+ * Create a tabs controller for a root element
28
+ *
29
+ * Expected markup:
30
+ * ```html
31
+ * <div data-slot="tabs" data-default-value="two">
32
+ * <div data-slot="tabs-list">
33
+ * <button data-slot="tabs-trigger" data-value="one">Tab One</button>
34
+ * <button data-slot="tabs-trigger" data-value="two">Tab Two</button>
35
+ * </div>
36
+ * <div data-slot="tabs-content" data-value="one">Content One</div>
37
+ * <div data-slot="tabs-content" data-value="two">Content Two</div>
38
+ * </div>
39
+ * ```
40
+ */
41
+ declare function createTabs(root: Element, options?: TabsOptions): TabsController;
42
+ /**
43
+ * Find and bind all tabs components in a scope
44
+ * Returns array of controllers for programmatic access
45
+ */
46
+ declare function create(scope?: ParentNode): TabsController[];
47
+ //#endregion
48
+ export { TabsController, TabsOptions, create, createTabs };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ import{emit as e,ensureId as t,getPart as n,getParts as r,getRoots as i,on as a,setAria as o}from"@data-slot/core";const s=`a[href],button:not([disabled]),input:not([disabled]),select:not([disabled]),textarea:not([disabled]),[tabindex]:not([tabindex="-1"])`;function c(i,c={}){let{onValueChange:u,orientation:d=`horizontal`,activationMode:f=`auto`}=c,p=n(i,`tabs-list`),m=r(i,`tabs-trigger`),h=r(i,`tabs-content`),g=n(i,`tabs-indicator`);if(!p||m.length===0)throw Error(`Tabs requires tabs-list and at least one tabs-trigger`);let _=new Map;for(let e of h){let t=(e.dataset.value||``).trim();t&&_.set(t,e)}let v=[],y=new Map,b=new Map;for(let e of m){let t=(e.dataset.value||``).trim();if(!t)continue;let n=e.hasAttribute(`disabled`)||e.dataset.disabled!==void 0||e.getAttribute(`aria-disabled`)===`true`,r=_.get(t),i={el:e,value:t,disabled:n,panel:r};v.push(i),y.set(t,i),b.set(e,i)}let x=v.filter(e=>!e.disabled),S=new Map;x.forEach((e,t)=>S.set(e.value,t));let C=x[0]?.value||``,w=i,T=(c.defaultValue??w.dataset.defaultValue??``).trim(),E=y.get(T),D=E&&!E.disabled?T:C,O=[];p.setAttribute(`role`,`tablist`),d===`vertical`&&o(p,`orientation`,`vertical`);for(let e of v){let{el:n,disabled:r,panel:i}=e;n.setAttribute(`role`,`tab`);let a=t(n,`tab`);if(n.tagName===`BUTTON`&&!n.hasAttribute(`type`)&&(n.type=`button`),r&&(n.setAttribute(`aria-disabled`,`true`),n.tagName===`BUTTON`&&(n.disabled=!0)),i){i.setAttribute(`role`,`tabpanel`),i.tabIndex=-1;let e=t(i,`tabpanel`);n.setAttribute(`aria-controls`,e),i.setAttribute(`aria-labelledby`,a)}}let k=()=>{if(!g)return;let e=y.get(D);if(!e)return;let t=p.getBoundingClientRect(),n=e.el.getBoundingClientRect();g.style.setProperty(`--active-tab-left`,`${n.left-t.left}px`),g.style.setProperty(`--active-tab-width`,`${n.width}px`),g.style.setProperty(`--active-tab-top`,`${n.top-t.top}px`),g.style.setProperty(`--active-tab-height`,`${n.height}px`)},A=(t,n=!1)=>{if(t=t.trim(),D===t&&!n)return;let r=y.get(t);if(!r||r.disabled)if(n){if(t=C,!t)return}else return;let a=D!==t;D=t;for(let e of v){let n=e.value===t;o(e.el,`selected`,n),e.el.tabIndex=n&&!e.disabled?0:-1,e.el.dataset.state=n?`active`:`inactive`}for(let e of h){let n=(e.dataset.value||``).trim();if(!n)continue;let r=n===t;e.hidden=!r,e.dataset.state=r?`active`:`inactive`}i.setAttribute(`data-value`,t),g&&k(),a&&!n&&(e(i,`tabs:change`,{value:t}),u?.(t))};if(A(D,!0),g){let e=()=>requestAnimationFrame(k);O.push(a(window,`resize`,e)),O.push(a(p,`scroll`,e));let t=new ResizeObserver(e);t.observe(p),O.push(()=>t.disconnect())}O.push(a(p,`click`,e=>{let t=e.target.closest?.(`[data-slot="tabs-trigger"]`);if(!t)return;let n=b.get(t);n&&!n.disabled&&A(n.value)}));let j=d===`horizontal`,M=j?`ArrowLeft`:`ArrowUp`,N=j?`ArrowRight`:`ArrowDown`;O.push(a(p,`keydown`,e=>{let t=e.target.closest?.(`[data-slot="tabs-trigger"]`);if(!t)return;let n=b.get(t);if(!n||x.length===0)return;if(e.key===`Enter`||e.key===` `){e.preventDefault(),n.disabled||A(n.value);return}if(j&&e.key===`ArrowDown`&&n.value===D){let t=n.panel;if(t){e.preventDefault();let n=t.querySelector(s);(n||t).focus();return}}let r=S.get(n.value)??-1;r===-1&&(r=S.get(D)??0);let i=r;switch(e.key){case M:i=r-1,i<0&&(i=x.length-1);break;case N:i=r+1,i>=x.length&&(i=0);break;case`Home`:i=0;break;case`End`:i=x.length-1;break;default:return}e.preventDefault();let a=x[i];a&&(a.el.focus(),f===`auto`&&A(a.value))}));let P={select:e=>A(e),get value(){return D},updateIndicator:k,destroy:()=>{O.forEach(e=>e()),O.length=0,l.delete(i)}};return P}const l=new WeakSet;function u(e=document){let t=[];for(let n of i(e,`tabs`)){if(l.has(n))continue;l.add(n),t.push(c(n))}return t}export{u as create,c as createTabs};
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@data-slot/tabs",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "sideEffects": false,
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": ["dist"],
22
+ "scripts": {
23
+ "build": "tsdown"
24
+ },
25
+ "dependencies": {
26
+ "@data-slot/core": "workspace:*"
27
+ },
28
+ "keywords": ["headless", "ui", "tabs", "vanilla", "data-slot"],
29
+ "license": "MIT"
30
+ }