<h2 class="rtds-heading-2" id="demo-visible-heading">Navigazione a schede</h2>

<ul role="tablist" aria-labelledby="demo-visible-heading" class="rtds-tablist is-tablist is-manual rtds-border-b rtds-border-gray-01 rtds-mb-4 md:rtds-mb-10 rtds-grid md:rtds-flex">

    <li role="presentation">
        <button class="rtds-tab is-tab rtds-tab--default rtds-w-full md:rtds-w-auto" role="tab" id="tab-demo-visible-1" aria-selected="true" aria-controls="tabpanel-demo-visible-1">

            <span class="rtds-tab__label">Voce 1</span>

        </button>
    </li>

    <li role="presentation">
        <button class="rtds-tab is-tab rtds-tab--default rtds-w-full md:rtds-w-auto" role="tab" id="tab-demo-visible-2" aria-selected="false" tabindex="-1" aria-controls="tabpanel-demo-visible-2">

            <span class="rtds-tab__label">Voce 2</span>

        </button>
    </li>

    <li role="presentation">
        <button class="rtds-tab is-tab rtds-tab--default rtds-w-full md:rtds-w-auto" role="tab" id="tab-demo-visible-3" aria-selected="false" tabindex="-1" aria-controls="tabpanel-demo-visible-3">

            <span class="rtds-tab__label">Voce 3</span>

        </button>
    </li>

</ul>

<div id="tabpanel-demo-visible-1" role="tabpanel" tabindex="0" aria-labelledby="tab-demo-visible-1" class="rtds-tablist__tabpanel">
    <p class="rtds-text-base rtds-content-01">Contenuto del pannello 1.</p>
</div>

<div id="tabpanel-demo-visible-2" role="tabpanel" tabindex="0" aria-labelledby="tab-demo-visible-2" class="rtds-tablist__tabpanel rtds-hidden">
    <p class="rtds-text-base rtds-content-01">Contenuto del pannello 2.</p>
</div>

<div id="tabpanel-demo-visible-3" role="tabpanel" tabindex="0" aria-labelledby="tab-demo-visible-3" class="rtds-tablist__tabpanel rtds-hidden">
    <p class="rtds-text-base rtds-content-01">Contenuto del pannello 3.</p>
</div>
{% set _instanceId = instanceId if instanceId else 'demo' %}
{% set headingId = _instanceId ~ '-heading' %}
{% set _labelMode = labelMode if labelMode else 'sr-only' %}
{% if _labelMode == 'visible' %}
<h2 class="rtds-heading-2" id="{{ headingId }}">{{ heading }}</h2>
{% elif _labelMode == 'sr-only' %}
<h2 class="rtds-sr-only" id="{{ headingId }}">{{ heading }}</h2>
{% endif %}
{% if _labelMode == 'aria-label' %}
{% render '@tablist', { tabs: tabs, instanceId: _instanceId, ariaLabel: ariaLabel }, true %}
{% else %}
{% render '@tablist', { tabs: tabs, instanceId: _instanceId, labelledby: headingId }, true %}
{% endif %}
{% for tab in tabs %}
{% set panelId = 'tabpanel-' ~ _instanceId ~ '-' ~ loop.index %}
{% set tabId = 'tab-' ~ _instanceId ~ '-' ~ loop.index %}
<div
  id="{{ panelId }}"
  role="tabpanel"
  tabindex="0"
  aria-labelledby="{{ tabId }}"
  class="rtds-tablist__tabpanel{{ ' rtds-hidden' if not loop.first }}"
>
  {{ tab.content | safe }}
</div>
{% endfor %}
{
  "heading": "Navigazione a schede",
  "instanceId": "demo-visible",
  "labelMode": "visible",
  "tabs": [
    {
      "label": "Voce 1",
      "content": "<p class=\"rtds-text-base rtds-content-01\">Contenuto del pannello 1.</p>"
    },
    {
      "label": "Voce 2",
      "content": "<p class=\"rtds-text-base rtds-content-01\">Contenuto del pannello 2.</p>"
    },
    {
      "label": "Voce 3",
      "content": "<p class=\"rtds-text-base rtds-content-01\">Contenuto del pannello 3.</p>"
    }
  ]
}

Tabbed Interface

Il componente Tabbed Interface implementa il pattern completo a schede: barra di navigazione tab + pannelli di contenuto intercambiabili, gestiti via JavaScript con attivazione manuale.

Per la sola barra di navigazione tab (senza pannelli) usare @tablist. Per una lista di link a pagine diverse usare @tablist--navigazione.

Riferimento accessibilità: WAI-ARIA APG — Tabs Pattern (Manual Activation)


Comportamento

Renderizza: un heading opzionale (etichetta del gruppo), la barra tab via @tablist, e i pannelli <div role="tabpanel">. Il primo pannello è visibile; gli altri hanno classe rtds-hidden. tablist.js gestisce la visibilità dei pannelli e la navigazione da tastiera.

L’etichetta del gruppo si controlla via labelMode:

  • 'sr-only' (default) — <h2 class="rtds-sr-only"> referenziato da aria-labelledby sul tablist
  • 'visible'<h2 class="rtds-heading-2"> visibile, referenziato da aria-labelledby sul tablist
  • 'aria-label' — nessun heading; aria-label applicato direttamente sul tablist

Varianti

  • default: tre tab con heading sr-only (comportamento base)
  • heading-visibile: heading visibile con rtds-heading-2
  • aria-label: nessun heading, etichetta via aria-label diretta sul tablist
  • con-card: due tab — pannello 1 con @card, pannello 2 con @accordion; dimostra la composizione con componenti NJK reali
  • uso-macro: stesso rendering di con-card; il sorgente NJK (tab “Template” in Fractal) documenta come usare tabbed-interface.macro.njk in un template di pagina — import, tabbedBar(), e la chiamata a tabPanel() con blocco caller

Parametri di configurazione

Contesto di sviluppo Fractal/Nunjucks — Questa sezione riguarda i parametri del context .config.yml per la variante default. La variante con-card usa un template NJK separato e non accetta parametri da config.

Parametro Tipo Default Descrizione
heading stringa Testo del heading che etichetta il gruppo. Obbligatorio se labelMode è 'sr-only' o 'visible'
labelMode 'sr-only' | 'visible' | 'aria-label' 'sr-only' Modalità etichetta: heading nascosto, heading visibile, oppure aria-label diretto senza heading
ariaLabel stringa Testo dell’aria-label sul tablist. Obbligatorio se labelMode: 'aria-label'
instanceId stringa 'demo' Prefisso per gli ID generati. Usare valori univoci se ci sono più tabbed interface sulla stessa pagina
tabs array Lista schede. Ogni item: label (stringa, obbligatorio), content (stringa HTML, obbligatorio per la variante default)

Accessibilità

Struttura ARIA

Elemento Ruolo / Attributo Note
<h2 class="rtds-sr-only"> Presente se labelMode: 'sr-only'; etichetta il gruppo tramite aria-labelledby sulla <ul>
<h2 class="rtds-heading-2"> Presente se labelMode: 'visible'; heading visibile referenziato da aria-labelledby sulla <ul>
<ul> role="tablist" + aria-labelledby o aria-label aria-labelledby punta al heading se presente; aria-label se labelMode: 'aria-label'
<li> role="presentation" Rimuove la semantica lista ridondante nel widget ARIA
<button> role="tab" + aria-selected + aria-controls aria-selected="true" solo sul tab attivo; gli altri aria-selected="false"
Tab inattivi tabindex="-1" Esclusi dalla sequenza Tab lineare (roving tabindex)
<div> pannello role="tabpanel" + aria-labelledby + tabindex="0" aria-labelledby punta all’ID del tab corrispondente; tabindex="0" garantisce raggiungibilità con Tab anche se il pannello non ha elementi interattivi
Tasto Azione
Tab Sposta il focus sul tab attivo; poi sul pannello (tabindex="0") o sul primo elemento focusabile nel pannello
Arrow Left / Arrow Right Naviga tra i tab (sposta solo il focus — modalità manuale)
Enter / Space Attiva il tab focalizzato e mostra il pannello
Home Primo tab
End Ultimo tab

Attivazione manuale

Il pattern usa attivazione manuale: le frecce spostano il focus ma non attivano il pannello. L’utente attiva esplicitamente con Enter o Space. Questo evita richieste inutili quando il contenuto del pannello viene caricato in modo asincrono.

Note per gli screen reader

  • aria-labelledby sul pannello garantisce che lo screen reader annunci il nome del tab attivo quando il focus entra nel pannello
  • Se il pannello non ha elementi focusabili, tabindex="0" sul wrapper garantisce che il contenuto sia raggiungibile da tastiera

Stili CSS

  • rtds-tablist — barra tab (su <ul>) — stilizzata da @tablist
  • rtds-tablist__tabpanel — pannello di contenuto (su <div role="tabpanel">)
  • rtds-hidden — pannelli inattivi nascosti (aggiunta/rimossa da tablist.js)

Spaziatura heading visibile

Quando labelMode: 'visible', l’<h2> non ha margine inferiore predefinito. Per distanziarlo dalla barra tab:

  • aggiungere rtds-mb-4 (o altra utilità rtds-mb-*) direttamente sull’<h2> tramite il parametro headingClass nella macro (se estesa), oppure wrappare i due elementi in un <div> con classe di spaziatura verticale o griglia:
<div class="rtds-grid rtds-gap-4">
  <h2 class="rtds-heading-2" id="...">Titolo sezione</h2>
  <!-- barra tab + pannelli -->
</div>

oppure:

<div class="rtds-flex rtds-flex-col rtds-gap-4">
  ...
</div>

Uso in template NJK

Sezione sviluppo Fractal/Nunjucks — Per usare il pattern in un template di pagina (05-templates/, 06-pages/) importare tabbedBar e tabPanel dalla macro tabbed-interface.macro.njk. La macro gestisce internamente tutto il boilerplate ARIA — non scrivere a mano role="tabpanel", aria-labelledby, tabindex="0".

Il pattern prevede quattro passaggi:

  1. Importare le macro con from 'components/04-organisms/tabbed-interface/tabbed-interface.macro.njk' import tabbedBar, tabPanel
  2. Definire un array con solo le label dei tab (non il contenuto)
  3. Chiamare tabbedBar(tabs, instanceId, heading) per renderizzare heading sr-only + barra tab
  4. Usare un blocco call tabPanel(instanceId, index) per ogni pannello — il contenuto è libero all’interno

tabbedBar(tabs, instanceId, heading, labelMode, ariaLabel)

  • tabs: array di oggetti { label, iconLeft?, iconRight? } — solo le label, non il contenuto
  • instanceId: stringa univoca per la pagina (es. 'atti-consiglio')
  • heading: testo del heading; obbligatorio se labelMode è 'sr-only' (default) o 'visible'
  • labelMode: 'sr-only' (default) | 'visible' | 'aria-label' — modalità etichetta
  • ariaLabel: testo aria-label sul tablist; obbligatorio se labelMode: 'aria-label'

tabPanel(instanceId, index, hidden=false) — chiamata con blocco call

  • instanceId: stesso valore passato a tabbedBar
  • index: indice 1-based del pannello (1 per il primo, 2 per il secondo, ecc.)
  • hidden: true per i pannelli non attivi di default (tutti tranne il primo)
  • Il contenuto nel blocco è libero: altri componenti NJK via render, HTML diretto, testo

Note

  • tablist.js viene inizializzato su [role=tablist].is-manual — incluso nel bundle JS del progetto; gestisce esclusivamente aria-selected (non la classe is-active)
  • instanceId deve essere univoco per pagina: se due tabbed interface usano lo stesso instanceId, gli ID dei tab e dei pannelli collidono
  • La variante con-card usa un template NJK dedicato (tabbed-interface--con-card.njk) perché il contenuto dei pannelli è composito — ogni variante con contenuto diverso richiede il proprio file .njk
  • Dipende da: @tablist, @tab, tablist.js