Tucano v0.37.2

Tabs

The template already brings the list drawn, the initial tab marked and the other panels with hidden — the page is born right before the script runs. JavaScript adds what HTML can't: the roles that make the screen reader announce "tab, 2 of 4", the link between tab and panel, and the keyboard, where the whole list is a single Tab stop.

Examples

The same markup, with one more class or attribute.

Segmented

is-segmented is the height of a field, for switching a list's view next to a filter.

128 contracts.

Segmented next to a field

Border and padding add up to the control's height: the row stays aligned.

Manual activation

data-manual="true": the arrows only move focus, and Enter or Space opens.

This is where the panel loaded through HTMX would go.

With icon and counter

The svg and the .tuc-badge is-plain inside the tab follow its color.

Project overview.

Many tabs

The list scrolls horizontally instead of wrapping onto two lines; the tab focused with the arrows scrolls into view.

August — the tab marked in the template.

Disabled tab

disabled or aria-disabled="true": it doesn't open, and the arrows skip it.

Being edited.

How to use

Write the classes, aria-selected="true" on the initial tab and hidden on the other panels. Tabs and panels are matched by order: the third tab opens the third panel.

<div class="tuc-tabs" data-tuc-tabs>
  <div class="tuc-tabs__list">
    <button class="tuc-tabs__tab" aria-selected="true">Details</button>
    <button class="tuc-tabs__tab">Address</button>
  </div>
  <div class="tuc-tabs__panel">…</div>
  <div class="tuc-tabs__panel" hidden>…</div>
</div>

With aria-selected on none of them, the first enabled tab opens. The script gives type="button" to tabs that have no type — without it, switching tabs inside a <form> would submit the form —, creates the missing ids and links tab and panel through aria-controls and aria-labelledby.

Why the classes come from the template, not the script

That way the list is drawn from the very first frame, with no placeholder block and no jump when JavaScript arrives. For the same reason hidden on the panels is required: without it, they would all show up stacked until the script runs.

With HTMX, panels on demand

To avoid loading the content of every tab at once, each tab fetches its own panel the first time it is opened. Turn on data-manual="true": in manual mode, opening is always a click — from the mouse, from Enter or from Space —, so hx-trigger on the tab covers every path.

<div class="tuc-tabs" data-tuc-tabs data-manual="true">
  <div class="tuc-tabs__list">
    <button class="tuc-tabs__tab" aria-selected="true">Summary</button>
    <button class="tuc-tabs__tab" hx-get="{% url 'customer-invoices' customer.pk %}"
            hx-target="#invoices" hx-trigger="click once">Invoices</button>
  </div>
  <div class="tuc-tabs__panel">{% include "customers/summary.html" %}</div>
  <div class="tuc-tabs__panel" id="invoices" hidden></div>
</div>

Why manual mode exists

With automatic activation, the arrow switches the panel instantly. With panels that load from the server, passing over four tabs to reach the one you want would fire four requests. In manual mode the arrow only moves focus, and the person decides when to open.

In JavaScript

const tabs = new Tucano.Tabs('#customer', {
  selected: 1,        // index of the initial tab
  manual: true,
  onChange: (index, { tab, panel }) => console.log(index, tab.textContent),
});

tabs.select(2);                     // opens the third; fires onChange
tabs.select(0, { silent: true });   // opens without firing anything
tabs.index;                         // index of the open tab, or -1
tabs.tabs;                          // the tabs, in order
tabs.panels;                        // the panels, in order

Every switch — by the person or by select() — fires tucano:change on the tabs element, and the event bubbles. select() on a disabled tab does nothing.

document.addEventListener('tucano:change', (e) => {
  if (!e.target.matches('.tuc-tabs')) return;
  e.detail.value;      // index of the open tab
  e.detail.tab;        // the tab button
  e.detail.panel;      // the panel
  e.detail.instance;   // the Tabs
});

Tabs inside a panel are a separate set: the component only looks at the list and the panels that are direct children of its element.

Keyboard and accessibility

The keyboard follows the ARIA APG tabs pattern. The whole list is a single Tab stop — otherwise keyboard users would pass through every tab before reaching the content of the open one.

KeyAction
TabEnters the open tab; the next Tab goes to the panel content
← →Previous and next tab, skipping disabled ones and wrapping around at the ends
Home EndFirst and last enabled tab
Enter SpaceIn manual mode, opens the focused tab

The list gets role="tablist", each tab role="tab" with aria-selected, and each panel role="tabpanel". A panel with nothing focusable inside gets tabindex="0", so Tab reaches the content; with a field or link inside, it doesn't — that would be a useless stop before it.

API

Generated from the code on every build — if something is not here, it does not exist.

Markup
[data-tuc-tabs]
In JS
new Tucano.Tabs(alvo, opcoes)
Attributes
data-manual
Methods
select destroy
Events
tucano:change

Options

The notes in this table come from comments in the source code, which are written in Portuguese.

OptionDefaultWhat for
selectednullindice da aba inicial; sem ele vale a marcada com aria-selected="true", ou a primeira
manualfalsesetas so movem o foco, e Enter ou Espaco trocam o painel — para painel que carrega por HTMX
onChangenull(index, detail) a cada troca feita pela pessoa ou por select()