<h2 class="rtds-sr-only" id="demo-heading">Navigazione a schede</h2>
<ul role="tablist" aria-labelledby="demo-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-1" aria-selected="true" aria-controls="tabpanel-demo-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-2" aria-selected="false" tabindex="-1" aria-controls="tabpanel-demo-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-3" aria-selected="false" tabindex="-1" aria-controls="tabpanel-demo-3">
<span class="rtds-tab__label">Voce 3</span>
</button>
</li>
</ul>
<div id="tabpanel-demo-1" role="tabpanel" tabindex="0" aria-labelledby="tab-demo-1" class="rtds-tablist__tabpanel">
<p class="rtds-text-base rtds-content-01">Contenuto del pannello 1. Inserire qui i componenti o il markup appropriato per questa sezione.</p>
</div>
<div id="tabpanel-demo-2" role="tabpanel" tabindex="0" aria-labelledby="tab-demo-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-3" role="tabpanel" tabindex="0" aria-labelledby="tab-demo-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",
"labelMode": "sr-only",
"tabs": [
{
"label": "Voce 1",
"content": "<p class=\"rtds-text-base rtds-content-01\">Contenuto del pannello 1. Inserire qui i componenti o il markup appropriato per questa sezione.</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>"
}
]
}
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)
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 tablistdefault: tre tab con heading sr-only (comportamento base)heading-visibile: heading visibile con rtds-heading-2aria-label: nessun heading, etichetta via aria-label diretta sul tablistcon-card: due tab — pannello 1 con @card, pannello 2 con @accordion; dimostra la composizione con componenti NJK realiuso-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 callerContesto di sviluppo Fractal/Nunjucks — Questa sezione riguarda i parametri del context
.config.ymlper la variantedefault. La variantecon-cardusa 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) |
| 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 |
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.
aria-labelledby sul pannello garantisce che lo screen reader annunci il nome del tab attivo quando il focus entra nel pannellotabindex="0" sul wrapper garantisce che il contenuto sia raggiungibile da tastierartds-tablist — barra tab (su <ul>) — stilizzata da @tablistrtds-tablist__tabpanel — pannello di contenuto (su <div role="tabpanel">)rtds-hidden — pannelli inattivi nascosti (aggiunta/rimossa da tablist.js)Quando labelMode: 'visible', l’<h2> non ha margine inferiore predefinito. Per distanziarlo dalla barra tab:
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>Sezione sviluppo Fractal/Nunjucks — Per usare il pattern in un template di pagina (
05-templates/,06-pages/) importaretabbedBaretabPaneldalla macrotabbed-interface.macro.njk. La macro gestisce internamente tutto il boilerplate ARIA — non scrivere a manorole="tabpanel",aria-labelledby,tabindex="0".
Il pattern prevede quattro passaggi:
from 'components/04-organisms/tabbed-interface/tabbed-interface.macro.njk' import tabbedBar, tabPaneltabbedBar(tabs, instanceId, heading) per renderizzare heading sr-only + barra tabcall tabPanel(instanceId, index) per ogni pannello — il contenuto è libero all’internotabbedBar(tabs, instanceId, heading, labelMode, ariaLabel)
tabs: array di oggetti { label, iconLeft?, iconRight? } — solo le label, non il contenutoinstanceId: 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à etichettaariaLabel: testo aria-label sul tablist; obbligatorio se labelMode: 'aria-label'tabPanel(instanceId, index, hidden=false) — chiamata con blocco call
instanceId: stesso valore passato a tabbedBarindex: 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)render, HTML diretto, testotablist.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 collidonocon-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@tablist, @tab, tablist.js