ITS Web Design e Strategie Digitali ITS Academy I-CREA @ CFP Bauer

Case study: un accordion che diventa schede

Guida alla costruzione della sezione «Anche questa pagina è un progetto» a partire dal design Figma, con HTML, CSS e JavaScript.

Riferimenti Figma

La sezione è disegnata in due frame della stessa pagina:

Tutti i valori (colori, dimensioni dei caratteri, spaziature, spessori delle linee, dimensioni delle icone) vanno presi da Figma. Questa guida dice che cosa costruire e come ragionarci.

Come leggere i valori da Figma: in Dev Mode seleziona un elemento e guarda il pannello delle proprietà a destra. Molti valori hanno il nome di una variabile, per esempio il colore del testo secondario o quello delle linee: il nome ti aiuta a capire quale token del progetto corrisponde. Il valore esadecimale è sempre visibile accanto al nome.


File di partenza

Verifica che la tua cartella contenga i seguenti fai:

Le icone delle card non sono file: le scriverai direttamente nell’HTML come SVG. In Figma sono le icone book, versions, code e triangle-down: copiale come SVG e incollale nel codice.

Se prosegui dal tuo progetto invece che da inizio, ti basta copiare le tre immagini nuove nella tua cartella assets/images.


0. Prima di iniziare: due modifiche alla lezione 02

Prima di aggiungere la sezione, sistema due cose della pagina che hai già.

Togliere le destinazioni provvisorie

Il <div class="chapters"> con le cinque sezioni vuote serviva solo a controllare che i link del menu funzionassero. Toglilo dall’HTML e togli le regole .chapters dal CSS. Al suo posto lascia un commento che ti ricordi la regola:

<!-- Ogni sezione del case study deve avere l'id della voce di menu corrispondente:
     concept, sitemap, assets, prototipi, highlights. -->

Un link href="#assets" porta all’elemento con id="assets". Quando costruirai le sezioni vere del case study, ognuna dovrà avere l’id della sua voce di menu, scritto con le stesse lettere. Fino ad allora i link del menu non portano da nessuna parte, e il menu mobile resta aperto quando scegli una voce: il codice della lezione 02 lo chiude solo se la destinazione esiste.

Una soglia comune a 1000px

La navigazione passa in linea da 63.25rem. Porta quel valore a 1000px: da questa lezione tutta la pagina cambia disposizione alla stessa larghezza, la navigazione e la nuova sezione.

1000px è una scelta di progetto, non un valore che arriva da Figma o da un dispositivo. Le soglie si scelgono guardando quando il contenuto non sta più bene nella disposizione precedente. Averne una sola rende la pagina più facile da provare e da spiegare.

Il valore compare in due punti che devono coincidere: la media query nel CSS e matchMedia nel JavaScript.

Che cosa fa window.matchMedia?
const desktopMedia = window.matchMedia('(min-width: 1000px)');

window.matchMedia riceve una condizione scritta come nelle media query del CSS e restituisce un oggetto che la controlla. Non cambia l’aspetto della pagina.

Il CSS usa @media per cambiare l’aspetto; JavaScript usa matchMedia quando deve cambiare anche il comportamento. Nella nuova sezione basterà il CSS.

Come controllo di aver fatto tutto?

1. La sezione e la sua intestazione

Guarda la parte alta della sezione in Figma: un’etichetta con il cursore verde, un titolo e un paragrafo, centrati, su fondo bianco, con linee sottili ai lati e sopra.

Quali elementi HTML usare?

Una <section> con id="questa-pagina", dopo la hero, dentro <main>. Dagli aria-labelledby con l’id del titolo: così la sezione prende il nome dal suo titolo.

Dentro, un <header> per l’introduzione. Il titolo è un <h2>, perché l’<h1> della pagina è nella hero. Il paragrafo è un <p>.

L’etichetta è la stessa della hero: <p class="eyebrow"> con l’SVG del cursore. Non serve una classe nuova.

Quali classi usare?

Lavoriamo dentro un design system: prima di scrivere una regola nuova, chiediti se esiste già una classe che fa la stessa cosa, o se la regola potrà servire anche in altre sezioni.

Le classi che si possono usare ovunque hanno nomi generici (color-muted, border-inline); quelle di un solo componente hanno il suo nome come prefisso.

Come disporre l’introduzione?

.case-study-intro è una colonna flex centrata (flex-direction: column, align-items: center, text-align: center) con gap fra i tre elementi.

Dagli una sola max-width, sul contenitore, e centralo con margin: 0 auto. Non servono larghezze massime separate su titolo e paragrafo. La larghezza massima è in em: dipende dalla lunghezza delle righe di testo.

Il padding verticale e orizzontale usa i token di spaziatura in pixel (capitolo 2).


2. Token e utility del design system

Prima di scrivere il componente, prepara le regole globali che userà. Guarda in Figma le spaziature della sezione, le dimensioni dei titoli e delle icone.

Pixel o em?

La regola del progetto è questa:

Figma ti dà i valori in pixel. Per trasformarli in em dividi per la dimensione del carattere dell’elemento: se il testo è di 16px e la distanza di 8px, scrivi 0.5em.

Quali token e utility aggiungere?

In :root:

Utility, una regola ciascuna:


3. Il tema chiaro

La hero e la navigazione sono scure, la sezione è bianca. I token in :root hanno i colori pensati per il tema scuro: su fondo bianco il testo secondario e le linee non vanno bene.

Le custom properties si ereditano

Una custom property si eredita come il colore del testo: il valore scritto in :root vale per tutta la pagina, quello scritto su un elemento vale per lui e per i suoi discendenti. Quindi non devi toccare .eyebrow: basta ridefinire i token sulla sezione.

.theme-light {
    --color-muted: …;
    --color-border-muted: …;
    --color-cursor: …;
    --color-focus: …;
    --color-accent: …;

    color: var(--color-black);
    background: var(--color-white);
}

I valori li trovi in Figma. Dentro la sezione .eyebrow legge i colori chiari, fuori quelli scuri.

Perché una classe theme-light e non regole su .case-study?

Il tema è una decisione del design system, non di questa sezione. Una classe con un nome generico si può mettere su qualunque altra sezione chiara. Dentro metti solo i token che cambiano con il tema, più il colore del testo e del fondo.

Prova: cambia --color-cursor su .theme-light. Il cursore della sezione cambia, quello della hero no.


4. Le voci: una sola struttura

Guarda le tre voci in Figma, sul desktop e sul mobile. Sul desktop sono tre schede affiancate: si vedono tutti i testi e la scheda scelta cambia l’immagine sopra. Sul mobile sono un accordion: si vede solo il testo della voce scelta, e l’immagine sopra è quella della voce scelta.

Una struttura o due?

Una. Nel sito React di riferimento il desktop e il mobile usano due strutture diverse, ma cambia solo la disposizione: gli stessi titoli, gli stessi testi, la stessa voce (.breakdownTabs-item) scelta. Se scrivi una sola struttura, il CSS la dispone in due modi con le media query, e quando la finestra cambia larghezza la voce scelta e il focus restano dove sono.

Quali elementi HTML usare?

Dentro un <button> l’HTML ammette solo contenuto in linea: niente titoli, niente paragrafi. Per questo il testo sta fuori dal pulsante, e il titolo della voce è uno <span>, non un <h3>.

Che cosa c’è dentro il pulsante?
<button class="breakdownTabs-button" type="button">
  <svg class="icon" …>…</svg>
  <span class="breakdownTabs-title">
    <span class="text-size-350">Studiare il sistema</span>
    <svg class="icon breakdownTabs-toggle" …>…</svg>
  </span>
</button>
Come segno la voce scelta?

Con una classe di stato, is-selected, sul <li> della prima voce (.breakdownTabs-item). Il prefisso is- distingue le classi che JavaScript aggiunge e toglie da quelle che descrivono un elemento.

Dai a ogni voce un id (breakdownTabs-item-study, breakdownTabs-item-layout, breakdownTabs-item-check): nel CSS ti serviranno per collegare ogni voce alla sua immagine.

Dove metto le immagini?

Sopra la lista, in un <div class="breakdownTabs-visuals">: tutte e tre, ognuna con la classe breakdownTabs-visual e una classe che richiama l’id della sua voce (.breakdownTabs-item): breakdownTabs-visual-study per breakdownTabs-item-study, e così via. Il CSS ne mostrerà una alla volta.

Ogni immagine ha il suo testo alternativo, che descrive che cosa mostra. Scrivi anche width e height con le dimensioni del file: il browser conosce la proporzione prima di scaricarla.

Tutto il componente sta in <div class="breakdownTabs border-block">, con dentro <div class="breakdownTabs-content container border-inline">.

Come controllo l’HTML prima del CSS?

Apri la pagina: vedi tre immagini, tre pulsanti con l’aspetto predefinito e tre testi. Con Tab raggiungi i tre pulsanti. Il fondo è bianco anche senza tema, perché i colori scuri sono sulla hero e sulla navigazione, non su body.


5. L’accordion del mobile

Parti dal mobile. Guarda in Figma la voce aperta e quelle chiuse: le linee che separano le voci, la linea più spessa e colorata sopra la voce aperta, il triangolo capovolto, il testo visibile solo nella voce aperta.

Scegliere l’immagine con :has()

Con + e ~ puoi raggiungere solo gli elementi che vengono dopo. :has() ti permette di partire dal contenitore e chiedere che cosa contiene:

.breakdownTabs-visual {
    display: none;
}

.breakdownTabs:has(#breakdownTabs-item-study.is-selected) .breakdownTabs-visual-study {
    display: block;
}

.breakdownTabs viene scelto quando contiene la voce con quell’id e la classe is-selected; da lì scendi alla sua immagine. Scrivi una regola per ciascuna delle tre voci.

Come trasformo il pulsante nell’intestazione della voce?

Togli l’aspetto predefinito: border: 0, background: none, color: inherit, font: inherit, text-align: left, width: 100%, cursor: pointer. I pulsanti non ereditano il carattere e il colore dalla pagina: devi chiederlo tu.

Il pulsante è una colonna flex (flex-direction: column, align-items: flex-start) con gap fra l’icona e la riga del titolo. La riga del titolo, .breakdownTabs-title, è a sua volta un flex orizzontale largo quanto il pulsante: justify-content: space-between porta il triangolo a destra, align-items: center lo centra sul titolo anche quando il titolo cambia dimensione.

Padding, gap e distanze vengono da Figma, in em. Scrivile come custom properties su .breakdownTabs (per esempio --breakdownTabs-button-padding): sul desktop le cambierai in un punto solo.

Come disegno le linee delle voci?

Ogni voce (.breakdownTabs-item) ha un bordo inferiore e una linea superiore creata con ::before: un elemento assoluto, largo quanto la voce, alto --border-width. Serve position: relative sulla voce.

Nella voce scelta la linea cambia colore e diventa più spessa. Invece di cambiare height, puoi usare transform: scaleY(…) con transform-origin in alto: la linea cresce senza spostare il contenuto.

Il passaggio del mouse mette in evidenza la linea allo stesso modo, ma solo dentro @media (hover: hover) and (pointer: fine): su uno schermo tattile, dopo un tocco, la linea resterebbe evidenziata.

Dove si vede il focus?

Il pulsante è solo una parte della voce (.breakdownTabs-item). Togli il contorno al pulsante e disegnalo sull’intera voce:

.breakdownTabs-button:focus-visible {
    outline: none;
}

.breakdownTabs-item:has(.breakdownTabs-button:focus-visible) {
    outline: var(--focus-outline-width) solid var(--color-focus);
    outline-offset: var(--focus-outline-offset);
}

:focus-visible si attiva con la tastiera, non dopo un clic del mouse. Usa gli stessi token del contorno di focus della lezione 02.

Come nascondo i testi senza rischi?

Nascondi i testi delle voci (.breakdownTabs-item) non scelte e mostra i triangoli solo se il componente ha la classe breakdownTabs-ready:

.breakdownTabs-ready .breakdownTabs-item:not(.is-selected) .breakdownTabs-text {
    display: none;
}

.breakdownTabs-ready .breakdownTabs-toggle {
    display: block;
}

Il triangolo di partenza ha display: none. La classe breakdownTabs-ready la aggiungerà JavaScript (capitolo 7). Senza JavaScript i pulsanti non cambiano voce: se i testi fossero già nascosti, nessuno potrebbe leggerli. Così, se lo script non si carica, la sezione resta leggibile per intero.

Per provare il CSS prima di scrivere il JavaScript, aggiungi a mano breakdownTabs-ready negli strumenti di sviluppo e sposta is-selected da una voce all’altra.


6. Le schede del desktop

Guarda il frame desktop: le stesse tre voci, in tre colonne uguali. Tutti i testi sono visibili, non ci sono triangoli né linee sotto le voci. Il titolo della scheda scelta è nero, gli altri sono grigi. Nel design tutta la colonna è cliccabile, anche il testo.

Come dispongo le tre colonne?

Tutto sta in @media (min-width: 1000px), la stessa soglia del menu.

.breakdownTabs-list {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
}

Tre colonne uguali: minmax(0, 1fr) permette alle colonne di restringersi anche con parole lunghe. Lo spazio fra le colonne è un token di spaziatura.

Dentro la media query cambia anche le custom properties del pulsante (padding, gap, distanza del testo) e le spaziature del contenitore e dell’immagine, prendendole dal frame desktop.

Come mostro tutti i testi?

Riscrivi lo stesso selettore del capitolo 5 con display: block, dentro la media query. I due selettori hanno la stessa specificità: vince quello scritto dopo, che vale solo da 1000px. Allo stesso modo nascondi il triangolo e togli il bordo inferiore alle voci (.breakdownTabs-item).

Il colore: le voci hanno color: var(--color-muted), la voce scelta color: var(--color-black). Il titolo lo eredita.

Come rendo cliccabile tutta la colonna?

Il testo non può stare dentro il pulsante, ma il pulsante può coprire tutta la voce (.breakdownTabs-item) con un suo pseudo-elemento:

.breakdownTabs-button::after {
    content: "";
    position: absolute;
    inset: 0;
}

Un elemento assoluto con inset: 0 si estende fino ai bordi del primo antenato posizionato: .breakdownTabs-item, che ha già position: relative. Un clic sullo pseudo-elemento è un clic sul pulsante.

Perché non serve matchMedia?

Perché la struttura è una sola: cambia la disposizione, non il documento né il comportamento. Il pulsante sceglie la voce (.breakdownTabs-item) a tutte le larghezze; il CSS decide come mostrarla.


7. JavaScript: scegliere una voce

Fin qui hai spostato is-selected a mano negli strumenti di sviluppo. Ora scrivi il codice che la sposta quando si preme un pulsante. Il codice è breve: il lavoro sta nel capire che cosa deve fare, un passo alla volta. Per ogni passo ci sono due aiuti, il secondo più esplicito del primo. Apri il secondo solo se il primo non ti basta, e cerca su MDN i nomi che trovi.

Passo 1: trovare gli elementi

Indizio

Ti servono tre cose: il contenitore del componente, tutte le voci (.breakdownTabs-item) e tutti i pulsanti. Conservale in tre costanti all’inizio della parte nuova di script.js, con nomi che dicano che cosa contengono. Nella lezione 02 hai già cercato elementi della pagina con un selettore CSS.

Un indizio in più

Esistono due metodi di document che ricevono un selettore CSS: uno restituisce il primo elemento che corrisponde, l’altro li restituisce tutti. Per il contenitore ti basta il primo; per voci (.breakdownTabs-item) e pulsanti ti servono tutti.

Quello che li restituisce tutti ti dà un elenco nell’ordine in cui gli elementi compaiono nell’HTML: la prima voce dell’elenco è la prima voce della pagina. Questo ti servirà al passo 3.

Cerca: document.querySelector, document.querySelectorAll, NodeList.

Passo 2: dire al CSS che JavaScript funziona

Indizio

Nel capitolo 5 hai scritto regole che valgono solo se il componente ha la classe breakdownTabs-ready. Ora è JavaScript che deve aggiungerla, appena trovato il contenitore.

Un indizio in più

Ogni elemento ha una proprietà che rappresenta l’elenco delle sue classi, con metodi per aggiungere, togliere e controllare una classe. Ti serve quello che aggiunge. Dopo averlo scritto, ricarica a 375px: i testi delle voci (.breakdownTabs-item) non scelte devono sparire e i triangoli comparire.

Cerca: Element.classList, classList.add.

Passo 3: scegliere una voce

Indizio

Scrivi una funzione che sceglie una voce (.breakdownTabs-item). Le serve un’informazione: quale voce scegliere. Passagliela come parametro, sotto forma di posizione nell’elenco delle voci.

Dentro la funzione devi passare in rassegna tutte le voci: a quella nella posizione ricevuta aggiungi is-selected, a tutte le altre la togli. Così, qualunque fosse la voce scelta prima, alla fine una sola ha la classe.

Un indizio in più

Non serve ricordare quale voce era scelta prima: togliendo la classe a tutte le altre, la vecchia scelta sparisce da sola.

Prima di collegare i pulsanti puoi provare la funzione dalla Console: chiamala con 2 e guarda se si apre la terza voce.

Cerca: for (MDN, JavaScript), NodeList.length, «strict equality», classList.remove.

Passo 4: collegare i pulsanti

Indizio

Ogni pulsante, quando viene premuto, deve chiamare la funzione del passo 3 passandole la propria posizione. Il primo pulsante passa 0, il secondo 1, il terzo 2.

Visto che anche i pulsanti sono in un elenco ordinato come le voci (.breakdownTabs-item), la posizione di un pulsante nel suo elenco è la stessa della sua voce.

Un indizio in più

Cerca: EventTarget.addEventListener, evento click, «let nei cicli for».

Passo 5: la tastiera

Indizio

Prova prima di scrivere altro: raggiungi un pulsante con Tab e premi Invio, poi Spazio. Che cosa succede?

Un indizio in più

Non serve altro codice. Un <button> genera l’evento click anche quando lo attivi da tastiera con Invio o Spazio: il gestore del passo 4 funziona già. È uno dei motivi per cui abbiamo usato un pulsante vero e non un elemento qualsiasi con un gestore di clic.

Cerca: «button activation behavior», elemento <button> su MDN, sezione sull’accessibilità.

Come controllo il JavaScript?


8. Schermi stretti

Confronta il frame mobile con il desktop: il titolo della sezione, i titoli delle voci e gli spazi attorno al componente sono più piccoli.

Come cambio caratteri e spazi?

Usa la stessa soglia della hero, @media (max-width: 640px), e soltanto quella: fra 641 e 999px la sezione è un accordion con i caratteri e gli spazi del desktop.

Dentro la media query cambia i token, non le regole:

Le misure in em delle voci (.breakdownTabs-item) si adattano da sole alla nuova dimensione dei caratteri.

La sezione non ha animazioni: i cambi di voce, di linea, di colore e di triangolo sono immediati.


Accessibilità: il prossimo passo

Il componente funziona con mouse, tocco e tastiera, e il testo di ogni voce viene letto subito dopo il suo pulsante. Però i pulsanti non dicono il proprio stato: un lettore di schermo legge «Comporre la pagina, pulsante», senza dire se la voce è scelta o aperta, e il cambio d’immagine sopra la lista non viene annunciato.

Non devi implementarlo adesso, ma è importante sapere che cosa manca e perché non lo aggiungiamo a metà. Un ruolo ARIA promette un comportamento preciso: se il comportamento manca, chi usa un lettore di schermo riceve istruzioni sbagliate. Meglio nessun attributo ARIA che uno scritto male.

Che cosa servirebbe sul desktop?

Sul desktop le voci (.breakdownTabs-item) si comportano come schede (tab):

Un tablist può contenere solo tab: i testi delle schede andrebbero spostati fuori dalla lista o collegati ai pulsanti in un altro modo.

E sul mobile?

Sul mobile le voci (.breakdownTabs-item) si comportano come un accordion. Le strade sono due:

In entrambi i casi il cambio di larghezza va seguito con matchMedia e il suo evento change, e il focus non deve andare perso quando una struttura viene nascosta.


Ordine consigliato di sviluppo

  1. Le due modifiche della lezione 02: togli le destinazioni provvisorie, porta la soglia a 1000px in CSS e JavaScript, controlla il menu.
  2. Tutto l’HTML della sezione: intestazione, immagini, lista, pulsanti, testi. Apri la pagina: deve essere brutta ma completa, e ogni pulsante raggiungibile con Tab.
  3. Token e utility in :root, poi il tema chiaro. Controlla che la hero non cambi.
  4. Il mobile: immagini con :has(), pulsanti, linee, triangolo, focus. Prova gli stati aggiungendo a mano breakdownTabs-ready e spostando is-selected.
  5. Il desktop dentro @media (min-width: 1000px).
  6. Il JavaScript, e le prove con mouse, tastiera e JavaScript disattivato.
  7. Gli schermi stretti sotto 640px, per ultimi.

Dopo ogni passo confronta la pagina con Figma alle due larghezze dei frame.


Riepilogo delle tecniche utilizzate

In questa sezione hai usato: