Tabs
Alpine-powered tab set with APG keyboard navigation. Render tab companions in the `tablist` slot and tab-panel companions in the default slot.
Requires Alpine.js
Default
1{{ partial:components/primitives/tabs selected="overview" label="Project sections" }}
2 {{ slot:tablist }}
3 {{ partial:components/primitives/tab value="overview" label="Overview" /}}
4 {{ partial:components/primitives/tab value="details" label="Details" /}}
5 {{ partial:components/primitives/tab value="activity" label="Activity" /}}
6 {{ /slot:tablist }}
7 {{ partial:components/primitives/tab-panel value="overview" }}
8 <p class="text-muted-foreground">Overview content for the selected project.</p>
9 {{ /partial:components/primitives/tab-panel }}
10 {{ partial:components/primitives/tab-panel value="details" }}
11 <p class="text-muted-foreground">Details content with the deeper project context.</p>
12 {{ /partial:components/primitives/tab-panel }}
13 {{ partial:components/primitives/tab-panel value="activity" }}
14 <p class="text-muted-foreground">Recent activity and status updates.</p>
15 {{ /partial:components/primitives/tab-panel }}
16{{ /partial:components/primitives/tabs }}
{{ partial:components/primitives/tabs selected="overview" label="Project sections" }}
{{ slot:tablist }}
{{ partial:components/primitives/tab value="overview" label="Overview" /}}
{{ partial:components/primitives/tab value="details" label="Details" /}}
{{ partial:components/primitives/tab value="activity" label="Activity" /}}
{{ /slot:tablist }}
{{ partial:components/primitives/tab-panel value="overview" }}
<p class="text-muted-foreground">Overview content for the selected project.</p>
{{ /partial:components/primitives/tab-panel }}
{{ partial:components/primitives/tab-panel value="details" }}
<p class="text-muted-foreground">Details content with the deeper project context.</p>
{{ /partial:components/primitives/tab-panel }}
{{ partial:components/primitives/tab-panel value="activity" }}
<p class="text-muted-foreground">Recent activity and status updates.</p>
{{ /partial:components/primitives/tab-panel }}
{{ /partial:components/primitives/tabs }}
Slot labels
1{{ partial:components/primitives/tabs selected="drafts" label="Content status" }}
2 {{ slot:tablist }}
3 {{ partial:components/primitives/tab value="drafts" }}
4 Drafts
5 <span class="text-muted-foreground">3</span>
6 {{ /partial:components/primitives/tab }}
7 {{ partial:components/primitives/tab value="published" }}
8 Published
9 <span class="text-muted-foreground">12</span>
10 {{ /partial:components/primitives/tab }}
11 {{ /slot:tablist }}
12 {{ partial:components/primitives/tab-panel value="drafts" }}
13 <p class="text-muted-foreground">Draft entries waiting for review.</p>
14 {{ /partial:components/primitives/tab-panel }}
15 {{ partial:components/primitives/tab-panel value="published" }}
16 <p class="text-muted-foreground">Published entries visible on the site.</p>
17 {{ /partial:components/primitives/tab-panel }}
18{{ /partial:components/primitives/tabs }}
{{ partial:components/primitives/tabs selected="drafts" label="Content status" }}
{{ slot:tablist }}
{{ partial:components/primitives/tab value="drafts" }}
Drafts
<span class="text-muted-foreground">3</span>
{{ /partial:components/primitives/tab }}
{{ partial:components/primitives/tab value="published" }}
Published
<span class="text-muted-foreground">12</span>
{{ /partial:components/primitives/tab }}
{{ /slot:tablist }}
{{ partial:components/primitives/tab-panel value="drafts" }}
<p class="text-muted-foreground">Draft entries waiting for review.</p>
{{ /partial:components/primitives/tab-panel }}
{{ partial:components/primitives/tab-panel value="published" }}
<p class="text-muted-foreground">Published entries visible on the site.</p>
{{ /partial:components/primitives/tab-panel }}
{{ /partial:components/primitives/tabs }}
Companions
components/primitives/tab
renders the internal
_tab.antlers.html
trigger.
components/primitives/tab-panel
renders the internal
_tab-panel.antlers.html
panel.
They are documented here as Tabs companions and do not get public collection pages of their own.
1<div class="text-muted-foreground space-y-2 text-sm">
2 <p>
3 <code>components/primitives/tab</code>
4 renders the internal
5 <code>_tab.antlers.html</code>
6 trigger.
7 </p>
8 <p>
9 <code>components/primitives/tab-panel</code>
10 renders the internal
11 <code>_tab-panel.antlers.html</code>
12 panel.
13 </p>
14 <p>They are documented here as Tabs companions and do not get public collection pages of their own.</p>
15</div>
<div class="text-muted-foreground space-y-2 text-sm">
<p>
<code>components/primitives/tab</code>
renders the internal
<code>_tab.antlers.html</code>
trigger.
</p>
<p>
<code>components/primitives/tab-panel</code>
renders the internal
<code>_tab-panel.antlers.html</code>
panel.
</p>
<p>They are documented here as Tabs companions and do not get public collection pages of their own.</p>
</div>
Props
| Name | Type | Default | Description |
|---|---|---|---|
selected
|
string
|
Required initial quote-safe tab slug matching one `_tab` and one `_tab-panel` value | |
label
|
string
|
Tabs
|
Accessible label for the tablist |
class
|
string
|
Additional classes merged via tw_merge (root element only) |
Slots
| Name | Fallback / Default | Description |
|---|---|---|
default
|
Tab panels; render `components/primitives/tab-panel` companions here | |
tablist
|
Tab triggers; render `components/primitives/tab` companions here |
Source
1{{#
2 @name Tabs
3 @desc Alpine-powered tab set with APG keyboard navigation. Render tab companions in the `tablist` slot and tab-panel companions in the default slot.
4 @param selected string - Required initial quote-safe tab slug matching one `_tab` and one `_tab-panel` value
5 @param label string [Tabs] - Accessible label for the tablist
6 @param class string - Additional classes merged via tw_merge (root element only)
7 @slot tablist - Tab triggers; render `components/primitives/tab` companions here
8 @slot default - Tab panels; render `components/primitives/tab-panel` companions here
9#}}
10{{ _label = label ?? 'Tabs' }}
11{{ _class = 'w-full {class}'
12 | tw_merge }}
13<div
14 class="{{ _class }}"
15 x-data="{
16 selected: '{{ selected }}',
17 tabs: [],
18 refreshTabs() {
19 this.tabs = Array.from(this.$refs.tablist?.querySelectorAll('[role=tab]') ?? []).filter((tab) => {
20 return !tab.hasAttribute('disabled') && tab.getAttribute('aria-disabled') !== 'true';
21 });
22 },
23 selectTab(tab) {
24 if (!tab) return;
25
26 const value = tab.getAttribute('data-tab-value');
27 if (!value) return;
28
29 this.selected = value;
30 this.$nextTick(() => tab.focus());
31 },
32 focusTab(index) {
33 this.refreshTabs();
34 if (!this.tabs.length) return;
35
36 const boundedIndex = (index + this.tabs.length) % this.tabs.length;
37 this.selectTab(this.tabs[boundedIndex]);
38 },
39 focusFirst() { this.focusTab(0); },
40 focusLast() {
41 this.refreshTabs();
42 this.focusTab(this.tabs.length - 1);
43 },
44 focusNext() {
45 this.refreshTabs();
46 const currentIndex = this.tabs.indexOf(document.activeElement);
47 this.focusTab(currentIndex + 1);
48 },
49 focusPrev() {
50 this.refreshTabs();
51 const currentIndex = this.tabs.indexOf(document.activeElement);
52 this.focusTab(currentIndex - 1);
53 }
54 }"
55 x-id="['kern-tab']"
56 x-init="$nextTick(() => {
57 refreshTabs();
58 if (!tabs.some((tab) => tab.getAttribute('data-tab-value') === selected)) {
59 selected = tabs[0]?.getAttribute('data-tab-value') ?? selected;
60 }
61 })"
62>
63 <div
64 class="border-border flex flex-wrap gap-1 border-b"
65 x-ref="tablist"
66 role="tablist"
67 aria-label="{{ _label }}"
68 @keydown.right.prevent="focusNext()"
69 @keydown.left.prevent="focusPrev()"
70 @keydown.home.prevent="focusFirst()"
71 @keydown.end.prevent="focusLast()"
72 >
73 {{ slot:tablist }}
74 </div>
75 <div>
76 {{ slot }}
77 </div>
78</div>
{{#
@name Tabs
@desc Alpine-powered tab set with APG keyboard navigation. Render tab companions in the `tablist` slot and tab-panel companions in the default slot.
@param selected string - Required initial quote-safe tab slug matching one `_tab` and one `_tab-panel` value
@param label string [Tabs] - Accessible label for the tablist
@param class string - Additional classes merged via tw_merge (root element only)
@slot tablist - Tab triggers; render `components/primitives/tab` companions here
@slot default - Tab panels; render `components/primitives/tab-panel` companions here
#}}
{{ _label = label ?? 'Tabs' }}
{{ _class = 'w-full {class}'
| tw_merge }}
<div
class="{{ _class }}"
x-data="{
selected: '{{ selected }}',
tabs: [],
refreshTabs() {
this.tabs = Array.from(this.$refs.tablist?.querySelectorAll('[role=tab]') ?? []).filter((tab) => {
return !tab.hasAttribute('disabled') && tab.getAttribute('aria-disabled') !== 'true';
});
},
selectTab(tab) {
if (!tab) return;
const value = tab.getAttribute('data-tab-value');
if (!value) return;
this.selected = value;
this.$nextTick(() => tab.focus());
},
focusTab(index) {
this.refreshTabs();
if (!this.tabs.length) return;
const boundedIndex = (index + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[boundedIndex]);
},
focusFirst() { this.focusTab(0); },
focusLast() {
this.refreshTabs();
this.focusTab(this.tabs.length - 1);
},
focusNext() {
this.refreshTabs();
const currentIndex = this.tabs.indexOf(document.activeElement);
this.focusTab(currentIndex + 1);
},
focusPrev() {
this.refreshTabs();
const currentIndex = this.tabs.indexOf(document.activeElement);
this.focusTab(currentIndex - 1);
}
}"
x-id="['kern-tab']"
x-init="$nextTick(() => {
refreshTabs();
if (!tabs.some((tab) => tab.getAttribute('data-tab-value') === selected)) {
selected = tabs[0]?.getAttribute('data-tab-value') ?? selected;
}
})"
>
<div
class="border-border flex flex-wrap gap-1 border-b"
x-ref="tablist"
role="tablist"
aria-label="{{ _label }}"
@keydown.right.prevent="focusNext()"
@keydown.left.prevent="focusPrev()"
@keydown.home.prevent="focusFirst()"
@keydown.end.prevent="focusLast()"
>
{{ slot:tablist }}
</div>
<div>
{{ slot }}
</div>
</div>
Dependencies
Packages
1composer require marcorieser/tailwind-merge-statamic
2npm install alpinejs
composer require marcorieser/tailwind-merge-statamic npm install alpinejs