<h2 class="rtds-sr-only" id="macro-demo-heading">Contenuti per categoria</h2>
<ul role="tablist" aria-labelledby="macro-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-macro-demo-1" aria-selected="true" aria-controls="tabpanel-macro-demo-1">
<span class="rtds-tab__label">Notizie</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-macro-demo-2" aria-selected="false" tabindex="-1" aria-controls="tabpanel-macro-demo-2">
<span class="rtds-tab__label">Documenti</span>
</button>
</li>
</ul>
<div id="tabpanel-macro-demo-1" role="tabpanel" tabindex="0" aria-labelledby="tab-macro-demo-1" class="rtds-tablist__tabpanel">
<article class="rtds-card">
<div class="rtds-card__content">
<div class="rtds-card__heading">
<h3 class="rtds-card__title">
<a class="" href="">
Titolo della notizia di esempio nel pannello tab
</a>
</h3>
</div>
<div class="rtds-card__description ">
<p>
Testo di anteprima del contenuto presente nel pannello selezionato.
</p>
</div>
</div>
</article>
</div>
<div id="tabpanel-macro-demo-2" role="tabpanel" tabindex="0" aria-labelledby="tab-macro-demo-2" class="rtds-tablist__tabpanel rtds-hidden">
<p class="rtds-text-base rtds-content-01 rtds-mb-4">Elenco documenti disponibili per questa categoria.</p>
<div class="rtds-accordion rtds-accordion--default">
<div class="rtds-accordion__item">
<h3 class="rtds-accordion__title rtds-flex rtds-font-bold rtds-heading-3">
<button type="button" aria-expanded="false" class="rtds-accordion__trigger rtds-group rtds-flex rtds-items-center rtds-flex-1 rtds-transition-all" aria-controls="accordionSection-1" id="accordion1id">
<span class="rtds-accordion__trigger-label">
Documento 1
</span>
<span class="rtds-accordion__icon rtds-flex rtds-p-2 rtds-ml-auto rtds-h-auto rtds-transition-all rtds-background-02 rtds-rounded group-aria-expanded:rtds-background-primary group-aria-expanded:rtds-text-white ">
<svg class="rtds-icon rtds-fill-current rtds-w-4 rtds-h-4 md:rtds-w-8 md:rtds-h-8 lg:rtds-w-9 lg:rtds-h-9 is-show-more group-aria-expanded:rtds-hidden" aria-hidden="true" focusable="false" role="img">
<use href="../../icons.svg#mini--plus" />
</svg>
<svg class="rtds-icon rtds-fill-current rtds-w-4 rtds-h-4 md:rtds-w-8 md:rtds-h-8 lg:rtds-w-9 lg:rtds-h-9 is-show-less rtds-hidden group-aria-expanded:rtds-inline-block" aria-hidden="true" focusable="false" role="img">
<use href="../../icons.svg#mini--minus" />
</svg>
</span>
</button>
</h3>
<div id="accordionSection-1" role="region" aria-labelledby="accordion1id" class="rtds-accordion__pane rtds-transition-all" hidden>
<p>Contenuto del primo documento disponibile in questa sezione.</p>
</div>
</div>
<div class="rtds-accordion__item">
<h3 class="rtds-accordion__title rtds-flex rtds-font-bold rtds-heading-3">
<button type="button" aria-expanded="false" class="rtds-accordion__trigger rtds-group rtds-flex rtds-items-center rtds-flex-1 rtds-transition-all" aria-controls="accordionSection-2" id="accordion2id">
<span class="rtds-accordion__trigger-label">
Documento 2
</span>
<span class="rtds-accordion__icon rtds-flex rtds-p-2 rtds-ml-auto rtds-h-auto rtds-transition-all rtds-background-02 rtds-rounded group-aria-expanded:rtds-background-primary group-aria-expanded:rtds-text-white ">
<svg class="rtds-icon rtds-fill-current rtds-w-4 rtds-h-4 md:rtds-w-8 md:rtds-h-8 lg:rtds-w-9 lg:rtds-h-9 is-show-more group-aria-expanded:rtds-hidden" aria-hidden="true" focusable="false" role="img">
<use href="../../icons.svg#mini--plus" />
</svg>
<svg class="rtds-icon rtds-fill-current rtds-w-4 rtds-h-4 md:rtds-w-8 md:rtds-h-8 lg:rtds-w-9 lg:rtds-h-9 is-show-less rtds-hidden group-aria-expanded:rtds-inline-block" aria-hidden="true" focusable="false" role="img">
<use href="../../icons.svg#mini--minus" />
</svg>
</span>
</button>
</h3>
<div id="accordionSection-2" role="region" aria-labelledby="accordion2id" class="rtds-accordion__pane rtds-transition-all" hidden>
<p>Contenuto del secondo documento disponibile in questa sezione.</p>
</div>
</div>
</div>
</div>
{#
Uso della macro nei template di pagina (05-templates/, 06-pages/)
─────────────────────────────────────────────────────────────────
1. Importare le due macro:
{% from 'components/04-organisms/tabbed-interface/tabbed-interface.macro.njk'
import tabbedBar, tabPanel %}
2. Definire i tab e renderizzare la barra:
{% set tabDefs = [{ label: 'Notizie' }, { label: 'Documenti' }] %}
{{ tabbedBar(tabDefs, 'mio-id', 'Titolo sezioni', 'sr-only') }}
3. Ogni pannello con contenuto libero tramite caller():
{% call tabPanel('mio-id', 1) %}
{% render '@card', cardParams, true %}
{% endcall %}
{% call tabPanel('mio-id', 2, hidden=true) %}
{% render '@accordion', accordionParams, true %}
{% endcall %}
Nota: il parametro hidden=true è necessario per il secondo pannello e
successivi: aggiunge rtds-hidden sull'elemento role="tabpanel" così
da nasconderlo visivamente senza rimuoverlo dal DOM (accessibilità).
Il rendering qui sotto è il risultato equivalente per la preview.
#}
{% set _instanceId = 'macro-demo' %}
{% set _headingId = _instanceId ~ '-heading' %}
{% set _tabDefs = [{ label: 'Notizie' }, { label: 'Documenti' }] %}
<h2 class="rtds-sr-only" id="{{ _headingId }}">Contenuti per categoria</h2>
{% render '@tablist', { tabs: _tabDefs, instanceId: _instanceId, labelledby: _headingId }, true %}
<div
id="tabpanel-{{ _instanceId }}-1"
role="tabpanel"
tabindex="0"
aria-labelledby="tab-{{ _instanceId }}-1"
class="rtds-tablist__tabpanel"
>
{% render '@card', {
article: true,
categoryText: 'Notizie',
heading: 'Titolo della notizia di esempio nel pannello tab',
excerpt: 'Testo di anteprima del contenuto presente nel pannello selezionato.',
ctaIcon: 'mini--chevron-right',
ctaLinkLabel: 'Leggi di più'
}, true %}
</div>
<div
id="tabpanel-{{ _instanceId }}-2"
role="tabpanel"
tabindex="0"
aria-labelledby="tab-{{ _instanceId }}-2"
class="rtds-tablist__tabpanel rtds-hidden"
>
<p class="rtds-text-base rtds-content-01 rtds-mb-4">Elenco documenti disponibili per questa categoria.</p>
{% render '@accordion', {
classes: 'rtds-accordion--default',
iconClasses: 'rtds-background-02 rtds-rounded group-aria-expanded:rtds-background-primary group-aria-expanded:rtds-text-white',
showMoreIcon: 'mini--plus',
showLessIcon: 'mini--minus',
iconSize: 'rtds-w-4 rtds-h-4 md:rtds-w-8 md:rtds-h-8 lg:rtds-w-9 lg:rtds-h-9',
panelTitleFont: 'rtds-heading-3',
panels: [
{ label: 'Documento 1', text: '<p>Contenuto del primo documento disponibile in questa sezione.</p>' },
{ label: 'Documento 2', text: '<p>Contenuto del secondo documento disponibile in questa sezione.</p>' }
]
}, true %}
</div>
{
"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