Tabs

A set of layered sections of content known as tab panels that are displayed one at a time.

Loading...
<twig:Tabs defaultValue="overview" class="w-[400px]">
    <twig:Tabs:List>
        <twig:Tabs:Trigger value="overview">Overview</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="analytics">Analytics</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="reports">Reports</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="settings">Settings</twig:Tabs:Trigger>
    </twig:Tabs:List>
    <twig:Tabs:Content value="overview">
        <twig:Card>
            <twig:Card:Header>
                <twig:Card:Title>Overview</twig:Card:Title>
                <twig:Card:Description>
                    View your key metrics and recent project activity. Track progress
                    across all your active projects.
                </twig:Card:Description>
            </twig:Card:Header>
            <twig:Card:Content class="text-muted-foreground text-sm">
                You have 12 active projects and 3 pending tasks.
            </twig:Card:Content>
        </twig:Card>
    </twig:Tabs:Content>
    <twig:Tabs:Content value="analytics">
        <twig:Card>
            <twig:Card:Header>
                <twig:Card:Title>Analytics</twig:Card:Title>
                <twig:Card:Description>
                    Track performance and user engagement metrics. Monitor trends and
                    identify growth opportunities.
                </twig:Card:Description>
            </twig:Card:Header>
            <twig:Card:Content class="text-muted-foreground text-sm">
                Page views are up 25% compared to last month.
            </twig:Card:Content>
        </twig:Card>
    </twig:Tabs:Content>
    <twig:Tabs:Content value="reports">
        <twig:Card>
            <twig:Card:Header>
                <twig:Card:Title>Reports</twig:Card:Title>
                <twig:Card:Description>
                    Generate and download your detailed reports. Export data in
                    multiple formats for analysis.
                </twig:Card:Description>
            </twig:Card:Header>
            <twig:Card:Content class="text-muted-foreground text-sm">
                You have 5 reports ready and available to export.
            </twig:Card:Content>
        </twig:Card>
    </twig:Tabs:Content>
    <twig:Tabs:Content value="settings">
        <twig:Card>
            <twig:Card:Header>
                <twig:Card:Title>Settings</twig:Card:Title>
                <twig:Card:Description>
                    Manage your account preferences and options. Customize your
                    experience to fit your needs.
                </twig:Card:Description>
            </twig:Card:Header>
            <twig:Card:Content class="text-muted-foreground text-sm">
                Configure notifications, security, and themes.
            </twig:Card:Content>
        </twig:Card>
    </twig:Tabs:Content>
</twig:Tabs>

Installation

bin/console ux:install tabs --kit shadcn

That's it!

Install the following Composer dependencies:

composer require twig/extra-bundle twig/html-extra:^3.12.0 tales-from-a-dev/twig-tailwind-extra:^1.0.0

Copy the following file(s) into your Symfony app:

import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['trigger', 'tab'];
    static values = { activeTab: String };

    open(e) {
        this.activeTabValue = e.currentTarget.dataset.tabId;
    }

    activeTabValueChanged() {
        this.triggerTargets.forEach((trigger) => {
            const isActive = trigger.dataset.tabId === this.activeTabValue;
            trigger.dataset.state = isActive ? 'active' : 'inactive';
            trigger.ariaSelected = isActive;
        });

        this.tabTargets.forEach((tab) => {
            tab.dataset.state = tab.dataset.tabId === this.activeTabValue ? 'active' : 'inactive';
        });
    }
}
{# @prop defaultValue string define the open Tabs at initial rendering. Defaults to `` #}
{# @prop orientation 'horizontal'|'vertical' define the visual orientation. Defaults to `horizontal` #}
{# @block content The default block #}
{%- props defaultValue = '', orientation = 'horizontal' -%}

<div
    data-slot="tabs"
    data-controller="tabs"
    data-tabs-active-tab-value="{{ defaultValue }}"
    data-orientation="{{ orientation }}"
    class="{{ ('gap-2 group/tabs flex data-[orientation=horizontal]:flex-col ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes }}
>
    {% block content %}{% endblock %}
</div>
{# @prop value string Unique suffix identifier for generating Tabs internal IDs #}
{# @block content The default block #}
{%- props value -%}

{%- set _tab_id = 'tab-' ~ value -%}
{%- set _tab_title_id = _tab_id ~ '-title' -%}
{%- set _tab_description_id = _tab_id ~ '-description' -%}
{%- set open = defaultValue is same as(value) -%}
<div
    id="{{ _tab_description_id }}"
    data-slot="tabs-content"
    data-tabs-target="tab"
    data-tab-id="{{ value }}"
    role="tabpanel"
    aria-labelledby="{{ _tab_title_id }}"
    data-state="{{ open ? 'active' : 'inactive' }}"
    class="{{ ('text-sm flex-1 outline-none data-[state=inactive]:hidden ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes }}
>
    {%- block content %}{% endblock -%}
</div>
{# @prop variant 'default'|'line' The visual style variant. Defaults to `default` #}
{# @block content The default block #}
{%- props variant = 'default' -%}
{%- set style = html_cva(
    base: 'rounded-lg p-[3px] group-data-[orientation=horizontal]/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col',
    variants: {
        variant: {
            default: 'bg-muted',
            line: 'gap-1 bg-transparent',
        },
    },
) -%}

<nav
    data-slot="tabs-list"
    role="tablist"
    data-variant="{{ variant }}"
    class="{{ style.apply({variant: variant}, attributes.render('class'))|tailwind_merge }}"
{{ attributes }}>
    {% block content %}{% endblock %}
</nav>
{# @prop value string Unique suffix identifier for generating Tabs internal IDs #}
{# @block content The default block #}
{%- props value -%}

{%- set _tab_id = 'tab-' ~ value -%}
{%- set _tab_title_id = _tab_id ~ '-title' -%}
{%- set _tab_description_id = _tab_id ~ '-description' -%}
{%- set open = defaultValue is same as(value) -%}
<button
    id="{{ _tab_id }}"
    data-slot="tabs-trigger"
    data-action="click->tabs#open"
    data-tabs-target="trigger"
    data-tab-id="{{ value }}"
    role="tab"
    aria-controls="{{ _tab_description_id }}"
    aria-selected="{{ open ? 'true' : 'false' }}"
    data-state="{{ open ? 'active' : 'inactive' }}"
    class="{{ ('gap-1.5 rounded-md border border-transparent px-1.5 py-0.5 text-sm font-medium group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none [&_svg:not([class*=\'size-\'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent data-[state=active]:bg-background dark:data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 data-[state=active]:text-foreground after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100 ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes }}
>
    {%- block content -%}{%- endblock -%}
</button>

Happy coding!

Usage

<twig:Tabs defaultValue="account" class="w-[400px]">
    <twig:Tabs:List>
        <twig:Tabs:Trigger value="account">Account</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="password">Password</twig:Tabs:Trigger>
    </twig:Tabs:List>
    <twig:Tabs:Content value="account">Make changes to your account here.</twig:Tabs:Content>
    <twig:Tabs:Content value="password">Change your password here.</twig:Tabs:Content>
</twig:Tabs>

Examples

Line

Loading...
<twig:Tabs defaultValue="overview">
    <twig:Tabs:List variant="line">
        <twig:Tabs:Trigger value="overview">Overview</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="analytics">Analytics</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="reports">Reports</twig:Tabs:Trigger>
    </twig:Tabs:List>
</twig:Tabs>

Vertical

Loading...
<twig:Tabs defaultValue="account" orientation="vertical">
    <twig:Tabs:List>
        <twig:Tabs:Trigger value="account">Account</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="password">Password</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="notifications">Notifications</twig:Tabs:Trigger>
    </twig:Tabs:List>
</twig:Tabs>

Disabled

Loading...
<twig:Tabs defaultValue="home">
    <twig:Tabs:List>
        <twig:Tabs:Trigger value="home">Home</twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="settings" disabled>
            Disabled
        </twig:Tabs:Trigger>
    </twig:Tabs:List>
</twig:Tabs>

Icons

Loading...
<twig:Tabs defaultValue="preview">
    <twig:Tabs:List>
        <twig:Tabs:Trigger value="preview">
            <twig:ux:icon name="lucide:app-window" />
            Preview
        </twig:Tabs:Trigger>
        <twig:Tabs:Trigger value="code">
            <twig:ux:icon name="lucide:code" />
            Code
        </twig:Tabs:Trigger>
    </twig:Tabs:List>
</twig:Tabs>

API Reference

Tabs

Prop Type Description
defaultValue string define the open Tabs at initial rendering. Defaults to ``
orientation 'horizontal'|'vertical' define the visual orientation. Defaults to horizontal
Block Description
content The default block

Tabs:Content

Prop Type Description
value string Unique suffix identifier for generating Tabs internal IDs
Block Description
content The default block

Tabs:List

Prop Type Description
variant 'default'|'line' The visual style variant. Defaults to default
Block Description
content The default block

Tabs:Trigger

Prop Type Description
value string Unique suffix identifier for generating Tabs internal IDs
Block Description
content The default block