Account
Make changes to your account here. Click save when you're done.
Password
Change your password here. After saving, you'll be logged out.
<script>
window.basecoat = window.basecoat || {};
window.basecoat.registerTabs = function (Alpine) {
if (Alpine.components && Alpine.components.tabs) return;
Alpine.data("tabs", (initialTabIndex = 0) => ({
activeTabIndex: initialTabIndex,
tabs: [],
panels: [],
init() {
this.$nextTick(() => {
this.tabs = Array.from(this.$el.querySelectorAll(":scope > [role=tablist] [role=tab]:not([disabled])"));
this.panels = Array.from(this.$el.querySelectorAll(":scope > [role=tabpanel]"));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[initialTabIndex], false);
}
});
},
nextTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
prevTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex - 1 + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
selectTab(tab, focus = true) {
if (!tab || this.tabs.length === 0) return;
this.tabs.forEach((t, index) => {
const isSelected = t === tab;
t.setAttribute("aria-selected", isSelected);
t.setAttribute("tabindex", isSelected ? "0" : "-1");
if (isSelected) {
this.activeTabIndex = index;
this.activeTab = t;
if (focus) {
t.focus();
}
}
});
const panelId = tab.getAttribute("aria-controls");
if (!panelId) return;
this.panels.forEach((panel) => {
panel.hidden = panel.getAttribute("id") !== panelId;
});
},
$tablist: {
["@click"](event) {
const clickedTab = event.target.closest("[role=tab]");
if (clickedTab) {
this.selectTab(clickedTab);
}
},
["@keydown.arrow-right.prevent"]() {
this.nextTab();
},
["@keydown.arrow-left.prevent"]() {
this.prevTab();
},
},
}));
};
document.addEventListener("alpine:init", () => {
window.basecoat.registerTabs(Alpine);
});
</script>
<div class="tabs w-full" x-data="tabs(0)" id="demo-tabs-with-panels">
<nav role="tablist" aria-orientation="horizontal" x-bind="$tablist" class="w-full">
<button type="button" role="tab" id="-tab-1" aria-controls="-panel-1" aria-selected="true" tabindex="0">Account</button>
<button type="button" role="tab" id="-tab-2" aria-controls="-panel-2" aria-selected="false" tabindex="0">Password</button>
</nav>
<div role="tabpanel" id="-panel-1" aria-labelledby="-tab-1" tabindex="-1" aria-selected="true">
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
</div>
<div role="tabpanel" id="-panel-2" aria-labelledby="-tab-2" tabindex="-1" aria-selected="false" hidden>
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
</div>
</div>
Usage
HTML + Javascript
This component requires Javascript.
The component is structured as such:
- A
<div class="tabs">
which wraps around the entire component and holds it state (e.g. open/close). - A
<nav role="tablist">
that holds the tab buttons (<button role="tab">
). - A series of
<div role="tabpanel">
that holds the tab panels corresponding to each tab button.
You can include the Javascript code provided below, load it as an individual file or use the CLI. Some Alpine.js properties are also required on certain elements (e.g. x-bind
, x-data
, @click
).
<div class="tabs w-full" x-data="tabs(0)" id="demo-tabs-with-panels">
<nav role="tablist" aria-orientation="horizontal" x-bind="$tablist" class="w-full">
<button type="button" role="tab" id="-tab-1" aria-controls="-panel-1" aria-selected="true" tabindex="0">Account</button>
<button type="button" role="tab" id="-tab-2" aria-controls="-panel-2" aria-selected="false" tabindex="0">Password</button>
</nav>
<div role="tabpanel" id="-panel-1" aria-labelledby="-tab-1" tabindex="-1" aria-selected="true">
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
</div>
<div role="tabpanel" id="-panel-2" aria-labelledby="-tab-2" tabindex="-1" aria-selected="false" hidden>
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
</div>
</div>
<script>
window.basecoat = window.basecoat || {};
window.basecoat.registerTabs = function (Alpine) {
if (Alpine.components && Alpine.components.tabs) return;
Alpine.data("tabs", (initialTabIndex = 0) => ({
activeTabIndex: initialTabIndex,
tabs: [],
panels: [],
init() {
this.$nextTick(() => {
this.tabs = Array.from(this.$el.querySelectorAll(":scope > [role=tablist] [role=tab]:not([disabled])"));
this.panels = Array.from(this.$el.querySelectorAll(":scope > [role=tabpanel]"));
if (this.tabs.length > 0) {
this.selectTab(this.tabs[initialTabIndex], false);
}
});
},
nextTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex + 1) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
prevTab() {
if (this.tabs.length === 0) return;
let newIndex = (this.activeTabIndex - 1 + this.tabs.length) % this.tabs.length;
this.selectTab(this.tabs[newIndex]);
},
selectTab(tab, focus = true) {
if (!tab || this.tabs.length === 0) return;
this.tabs.forEach((t, index) => {
const isSelected = t === tab;
t.setAttribute("aria-selected", isSelected);
t.setAttribute("tabindex", isSelected ? "0" : "-1");
if (isSelected) {
this.activeTabIndex = index;
this.activeTab = t;
if (focus) {
t.focus();
}
}
});
const panelId = tab.getAttribute("aria-controls");
if (!panelId) return;
this.panels.forEach((panel) => {
panel.hidden = panel.getAttribute("id") !== panelId;
});
},
$tablist: {
["@click"](event) {
const clickedTab = event.target.closest("[role=tab]");
if (clickedTab) {
this.selectTab(clickedTab);
}
},
["@keydown.arrow-right.prevent"]() {
this.nextTab();
},
["@keydown.arrow-left.prevent"]() {
this.prevTab();
},
},
}));
};
document.addEventListener("alpine:init", () => {
window.basecoat.registerTabs(Alpine);
});
</script>
Jinja and Nunjucks
You can use the tabs()
Nunjucks or Jinja macro for this component.
{% set account_panel %}
<div class="card">
<header>
<h2>Account</h2>
<p>Make changes to your account here. Click save when you're done.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-account-name">Name</label>
<input type="text" id="demo-tabs-account-name" value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-account-username">Username</label>
<input type="text" id="demo-tabs-account-username" value="@peduarte" />
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save changes</button>
</footer>
</div>
{% endset %}
{% set password_panel %}
<div class="card">
<header>
<h2>Password</h2>
<p>Change your password here. After saving, you'll be logged out.</p>
</header>
<section>
<form class="form grid gap-6">
<div class="grid gap-3">
<label for="demo-tabs-password-current">Current password</label>
<input type="password" id="demo-tabs-password-current" />
</div>
<div class="grid gap-3">
<label for="demo-tabs-password-new">New password</label>
<input type="password" id="demo-tabs-password-new"/>
</div>
</form>
</section>
<footer>
<button type="button" class="btn">Save Password</button>
</footer>
</div>
{% endset %}
{% set tabsets_demo = [
{ tab: "Account", panel: account_panel },
{ tab: "Password", panel: password_panel }
] %}
{{ tabs(
id='demo-tabs-with-panels',
tabsets=tabsets_demo,
main_attrs={ "class": "w-full" },
tablist_attrs={ "class": "w-full" },
register_on=["alpine:init", "htmx:afterSwap"]
) }}