Connection lost. Reconnecting… attempt 1 of 8
Paused. Your work is held on the server.
Could not reconnect.
This session has expired on the server.
An unhandled error has occurred.
Sedna.UI
v0.17.0 · main

Tabs

tier 2 — classes

.tabs swaps a region of content in place. A strip that changes a setting is the segmented control, and a strip of links to other addresses only looks like tabs — the last example.

The classes cannot supply the ARIA Tabs need role="tablist", role="tab", aria-selected and aria-controls, plus arrow-key movement between them — data-tabs supplies the movement, and the markup supplies the rest.

Tabs

The demo is live. data-tabs on the container hands the strip to sednaUi.tabs — clicking, arrow keys, Home/End, showing the matching aria-controls panel, and the roving tabindex that makes Tab step over the whole tablist. Mark the selected tab with aria-selected="true" rather than .tab--active, which is for a strip that is genuinely not a tablist. An app driving its tabs from C# uses data-tabs="managed" instead — below.

Twelve orders are waiting on a first response.

<div>
    <div class="tabs" role="tablist" data-tabs aria-label="Order views">
        <button class="tab" role="tab" aria-selected="true" id="t-open" aria-controls="p-open" type="button">
            <i class="ri-inbox-line"></i> Open <span class="tab-count">12</span>
        </button>
        <button class="tab" role="tab" aria-selected="false" id="t-mine" aria-controls="p-mine" type="button" tabindex="-1">
            Assigned to me <span class="tab-count">3</span>
        </button>
        <button class="tab" role="tab" aria-selected="false" id="t-all" aria-controls="p-all" type="button" tabindex="-1">
            Everything
        </button>
        <button class="tab" role="tab" aria-selected="false" aria-disabled="true" disabled type="button" tabindex="-1">
            Archive
        </button>
    </div>
    <div class="tab-panel" role="tabpanel" id="p-open" aria-labelledby="t-open">
        <p class="lede">Twelve orders are waiting on a first response.</p>
    </div>
    <div class="tab-panel" role="tabpanel" id="p-mine" aria-labelledby="t-mine" hidden>
        <p class="lede">Three are yours: ORD-4209, ORD-4244 and ORD-4250.</p>
    </div>
    <div class="tab-panel" role="tabpanel" id="p-all" aria-labelledby="t-all" hidden>
        <p class="lede">Everything in the queue, decided or not.</p>
    </div>
</div>

Tabs the app selects

Live — use the arrow keys. With data-tabs="managed" the script owns only the keyboard: an arrow, Home or End moves focus to a tab and clicks it, and your click handler changes the selection. Nothing the script does writes aria-selected, tabindex or hidden, so there is one writer. SednaTabs.Tab(key, selected, prefix) and Panel(…) render those attributes, with ids that name each other; give each tablist on a page its own prefix.

Twelve orders are waiting on a reservation.

<div class="tabs" role="tablist" aria-label="Orders" data-tabs="managed">
    @foreach (var tab in Tabs)
    {
        var key = tab.Key;
        <button class="tab" type="button" @attributes="SednaTabs.Tab(key, _tab, Prefix)" @onclick="@(() => _tab = key)">
            @tab.Label
        </button>
    }
</div>

@foreach (var tab in Tabs)
{
    <div class="tab-panel" @attributes="SednaTabs.Panel(tab.Key, _tab, Prefix)">
        <p>@tab.Body</p>
    </div>
}

@code {
    private const string Prefix = "orders";

    private static readonly (string Key, string Label, string Body)[] Tabs =
    [
        ("open", "Open", "Twelve orders are waiting on a reservation."),
        ("held", "Held", "Three orders are held for stock."),
        ("done", "Dispatched", "Forty-one orders left the warehouse today."),
    ];

    // The selected tab is the app's state. data-tabs="managed" hands the keyboard to the
    // script, which moves focus and clicks; the click handler above changes this, and
    // SednaTabs renders what follows from it.
    private string _tab = "open";
}

Tabs in a card

Live — switch between them. The tablist goes in .card-head and the panels in the body. Add .card-head--tabs to the head: it hands the head's padding to the strip, so the head's own border becomes the strip's line and the active tab's indicator meets the body edge — a plain .card-head draws two lines and anchors the indicator to nothing.

ItemQtyReserved
Desk lamp, matte22
Cable tray, 600 mm11
Monitor arm, dual20
<div class="card" style="max-width:520px">
    <div class="card-head card-head--tabs">
        <div class="tabs" role="tablist" data-tabs aria-label="ORD-4209 detail">
            <button class="tab" role="tab" aria-selected="true" id="c-lines" aria-controls="cp-lines" type="button">
                Lines <span class="tab-count">5</span>
            </button>
            <button class="tab" role="tab" aria-selected="false" id="c-history" aria-controls="cp-history" type="button" tabindex="-1">
                History
            </button>
        </div>
    </div>
    <div class="card-body">
        <div class="tab-panel" role="tabpanel" id="cp-lines" aria-labelledby="c-lines">
            <table class="table">
                <thead>
                    <tr><th>Item</th><th>Qty</th><th class="col-num">Reserved</th></tr>
                </thead>
                <tbody>
                    <tr><td>Desk lamp, matte</td><td>2</td><td class="col-num">2</td></tr>
                    <tr><td>Cable tray, 600 mm</td><td>1</td><td class="col-num">1</td></tr>
                    <tr><td>Monitor arm, dual</td><td>2</td><td class="col-num">0</td></tr>
                </tbody>
            </table>
        </div>
        <div class="tab-panel" role="tabpanel" id="cp-history" aria-labelledby="c-history" hidden>
            <ol class="timeline">
                <li>
                    <span class="timeline-when">09:15</span>
                    <span class="timeline-what">Three of five lines reserved automatically.</span>
                </li>
                <li>
                    <span class="timeline-when">09:18</span>
                    <span class="timeline-what">Held for a decision by Alex Fischer.</span>
                </li>
            </ol>
        </div>
    </div>
</div>

The same look over a row of <a>s to other addresses — a record's sections, each a page of its own. This is not a tablist: there is no panel to swap and no arrow-key contract, so it is a <nav> with an aria-label, .tab--active marks the current page and aria-current="page" is what announces it, as ActiveLink writes both. Leave role and data-tabs off.

<nav class="tabs" aria-label="ORD-4209">
    <a class="tab tab--active" href="#" aria-current="page">Summary</a>
    <a class="tab" href="#">Lines <span class="tab-count">5</span></a>
    <a class="tab" href="#">Shipments <span class="tab-count">2</span></a>
    <a class="tab" href="#">History</a>
</nav>