Tabs
Navigation interface that organizes content into sections.
TailwindCSS
Vanilla JS
Account
Make changes to your account here. Click save when you are done.
Password
Change your password here. After saving, you will be logged out.
package showcase
import "github.com/axzilla/templui/components"
templ TabsDefault() {
@components.Tabs(components.TabsProps{
ID: "account-tabs",
}) {
@components.TabsList(components.TabsListProps{
Class: "w-full max-w-xs",
}) {
@components.TabTrigger(components.TabTriggerProps{
Value: "account",
IsActive: true,
}) {
Account
}
@components.TabTrigger(components.TabTriggerProps{
Value: "password",
}) {
Password
}
}
<div class="w-full max-w-xs mt-2">
@components.TabContent(components.TabContentProps{
Value: "account",
IsActive: true,
}) {
@AccountTab()
}
@components.TabContent(components.TabContentProps{
Value: "password",
}) {
@PasswordTab()
}
</div>
}
}
templ AccountTab() {
@components.Card() {
@components.CardHeader() {
@components.CardTitle() {
Account
}
@components.CardDescription() {
Make changes to your account here. Click save when you are done.
}
}
@components.CardContent() {
<div class="flex flex-col gap-4">
@components.Input(components.InputProps{
Type: components.InputTypeText,
Placeholder: "Name",
ID: "name",
Value: "John Doe",
})
@components.Input(components.InputProps{
Type: components.InputTypeEmail,
Placeholder: "Email",
ID: "email",
Value: "john.doe@example.com",
})
</div>
}
@components.CardFooter() {
@components.Button() {
Save changes
}
}
}
}
templ PasswordTab() {
@components.Card() {
@components.CardHeader() {
@components.CardTitle() {
Password
}
@components.CardDescription() {
Change your password here. After saving, you will be logged out.
}
}
@components.CardContent() {
<div class="flex flex-col gap-4">
@components.Input(components.InputProps{
Type: components.InputTypePassword,
Placeholder: "Current Password",
ID: "current_password",
})
@components.Input(components.InputProps{
Type: components.InputTypePassword,
Placeholder: "New Password",
ID: "new_password",
})
</div>
}
@components.CardFooter() {
@components.Button() {
Save password
}
}
}
}
package components
import (
"context"
"github.com/axzilla/templui/utils"
)
type TabsProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TabsListProps struct {
ID string
Class string
Attributes templ.Attributes
}
type TabTriggerProps struct {
ID string
Class string
Attributes templ.Attributes
Value string
IsActive bool
TabsID string
}
type TabContentProps struct {
ID string
Class string
Attributes templ.Attributes
Value string
IsActive bool
TabsID string
}
templ Tabs(props ...TabsProps) {
{{ var p TabsProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ tabsID := p.ID }}
if tabsID == "" {
{{ tabsID = utils.RandomID() }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={ utils.TwMerge("relative", p.Class) }
data-tabs
data-tabs-id={ tabsID }
{ p.Attributes... }
>
{{ ctx = context.WithValue(ctx, "tabsId", tabsID) }}
{ children... }
</div>
}
templ TabsList(props ...TabsListProps) {
{{ var p TabsListProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"relative flex items-center justify-center h-10 p-1 rounded-lg select-none bg-muted text-muted-foreground",
p.Class,
),
}
{ p.Attributes... }
>
{ children... }
<div
data-tabs-marker
data-tabs-id={ getTabsID(ctx) }
class="absolute left-0 z-10 h-full duration-300 ease-out"
>
<div class="w-full h-full bg-background rounded-md shadow-xs"></div>
</div>
</div>
}
templ TabTrigger(props ...TabTriggerProps) {
{{ var p TabTriggerProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ tabsID := p.TabsID }}
if tabsID == "" {
{{ tabsID = getTabsID(ctx) }}
}
<button
if p.ID != "" {
id={ p.ID }
}
type="button"
class={
utils.TwMerge(
"relative z-20 flex-1 inline-flex items-center justify-center h-8 px-3 text-sm font-medium transition-all rounded-md cursor-pointer whitespace-nowrap hover:text-foreground",
utils.If(p.IsActive, "text-foreground bg-background shadow-xs"),
p.Class,
),
}
data-tabs-trigger
data-tabs-id={ tabsID }
data-tabs-value={ p.Value }
data-state={ utils.IfElse(p.IsActive, "active", "inactive") }
{ p.Attributes... }
>
{ children... }
</button>
}
templ TabContent(props ...TabContentProps) {
{{ var p TabContentProps }}
if len(props) > 0 {
{{ p = props[0] }}
}
{{ tabsID := p.TabsID }}
if tabsID == "" {
{{ tabsID = getTabsID(ctx) }}
}
<div
if p.ID != "" {
id={ p.ID }
}
class={
utils.TwMerge(
"relative",
utils.If(!p.IsActive, "hidden"),
p.Class,
),
}
data-tabs-content
data-tabs-id={ tabsID }
data-tabs-value={ p.Value }
data-state={ utils.IfElse(p.IsActive, "active", "inactive") }
{ p.Attributes... }
>
{ children... }
</div>
}
func getTabsID(ctx context.Context) string {
if tabsID, ok := ctx.Value("tabsId").(string); ok {
return tabsID
}
return ""
}
templ TabsScript() {
<script nonce={ templ.GetNonce(ctx) }>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('[data-tabs]').forEach(tabs => {
const tabsId = tabs.dataset.tabsId;
const triggers = tabs.querySelectorAll(`[data-tabs-trigger][data-tabs-id="${tabsId}"]`);
const contents = tabs.querySelectorAll(`[data-tabs-content][data-tabs-id="${tabsId}"]`);
const marker = tabs.querySelector(`[data-tabs-marker][data-tabs-id="${tabsId}"]`);
// Initial setup
if (triggers.length > 0 && marker) {
const activeTab = tabs.querySelector(`[data-tabs-trigger][data-tabs-id="${tabsId}"][data-state="active"]`) || triggers[0];
// Set initial marker position
marker.style.width = activeTab.offsetWidth + 'px';
marker.style.height = activeTab.offsetHeight + 'px';
marker.style.left = activeTab.offsetLeft + 'px';
// Set initial active state if not already set
if (!activeTab.dataset.state || activeTab.dataset.state !== "active") {
activeTab.dataset.state = "active";
activeTab.classList.add('text-foreground', 'bg-background', 'shadow-xs');
// Find and show the corresponding content
const activeValue = activeTab.dataset.tabsValue;
const activeContent = tabs.querySelector(`[data-tabs-content][data-tabs-id="${tabsId}"][data-tabs-value="${activeValue}"]`);
if (activeContent) {
activeContent.dataset.state = "active";
activeContent.classList.remove('hidden');
}
}
}
// Event listeners
triggers.forEach(trigger => {
trigger.addEventListener('click', () => {
const value = trigger.dataset.tabsValue;
// Update trigger states
triggers.forEach(t => {
t.dataset.state = t.dataset.tabsValue === value ? "active" : "inactive";
t.classList.remove('text-foreground', 'bg-background', 'shadow-xs');
if (t.dataset.state === "active") {
t.classList.add('text-foreground', 'bg-background', 'shadow-xs');
}
});
// Update content states
contents.forEach(content => {
content.dataset.state = content.dataset.tabsValue === value ? "active" : "inactive";
if (content.dataset.state === "active") {
content.classList.remove('hidden');
} else {
content.classList.add('hidden');
}
});
// Update marker
if (marker) {
marker.style.width = trigger.offsetWidth + 'px';
marker.style.height = trigger.offsetHeight + 'px';
marker.style.left = trigger.offsetLeft + 'px';
}
});
});
});
});
</script>
}
Usage
1. Add the script to your page/layout:
// Option A: All components (recommended)
@utils.ComponentScripts()
// Option B: Just Tabs
@components.TabsScript()
2. Use the component:
@components.Tabs(components.TabsProps{...})