Kern GitHub

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
Tabs owns the Alpine state and APG keyboard behavior. Put `tab` companions in the `tablist` slot and matching `tab-panel` companions in the default slot. ArrowLeft/ArrowRight/Home/End move focus and automatically activate the new tab. Companion `value` props are interpolated into JavaScript string literals, so keep them quote-safe slugs: letters, numbers, dashes, or underscores only. The tab panel includes `tabindex="0"` to match the APG reference pattern for panels without focusable content. If your panel contains links, buttons, form controls, or another focusable element, remove that `tabindex` from your copied source.

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 }}

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 }}

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>

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>

Dependencies

Packages

1composer require marcorieser/tailwind-merge-statamic
2npm install alpinejs