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

Esercitazione Flashcard

Guida alla costruzione dell'interfaccia a partire dal design Figma, usando HTML semantico e CSS.

Riferimenti Figma

Usa i link qui sotto per ispezionare il design (colori, spaziature, dimensioni font, bordi, ecc.):

Tutti i valori esatti (colori, padding, font-size, border-width, ecc.) vanno presi direttamente da Figma. Questa guida indica cosa costruire e come affrontarlo, non i valori pixel per pixel.

Come leggere i valori da Figma: seleziona un elemento nel pannello Dev Mode e guarda il pannello delle proprietà a destra. Troverai colori (in hex), dimensioni, padding, margini, font-family, font-weight, font-size, line-height, border-radius, box-shadow e altro. Quando il design usa token come colors/neutral/900, il valore hex corrispondente è sempre visibile — usa quello.


File di partenza

Parti con:

Dovrai creare tu il file style.css.


0. Setup iniziale

Collegare il foglio di stile

Nel <head>, aggiungi un <link> al tuo style.css. Niente di nuovo qui.

Assicurati che l'HTML abbia anche <meta charset="UTF-8"> e il meta viewport <meta name="viewport" content="width=device-width, initial-scale=1.0">. Il viewport meta è essenziale: senza di esso, i browser mobile rendereranno la pagina come se fosse un desktop (tipicamente a 980px) e poi la zoomeranno fuori. Il risultato: tutto minuscolo e illeggibile.

Organizzare il CSS con @layer

Apriamo il nostro style.css: è vuoto. Cosa ci mettiamo come prima cosa? La primissima riga dichiara la struttura dell'intero foglio di stile con @layer:

@layer reset, base, layout, components, states;

Questa dichiarazione dice al browser: "le regole nel layer reset hanno la priorità più bassa, quelle in states la più alta." Non importa dove fisicamente si trovano le regole nel file — è il layer a determinare quale vince in caso di conflitto.

I cinque layer che useremo:

Layer Contenuto
reset Reset globale: box-sizing, margini zero, default per img
base Stili fondamentali: body, .container, .divider
layout Contenitori strutturali che organizzano lo spazio: header, main, wrapper dei filtri, navigazione
components Componenti con identità visiva: bottoni, tab, card, input, select, flashcard, statistiche
states Stati interattivi: :hover, :focus-visible, :checked

Perché questa suddivisione? Il vantaggio è che gli stati interattivi (layer states) vincono sempre sui componenti (layer components), indipendentemente dalla specificità dei selettori. Senza layer, se .btn:hover e .btn--primary:hover avessero la stessa specificità, conterebbe l'ordine nel file — fragile e facile da rompere riordinando il codice. Con i layer, entrambi sono in states e la specificità normale dei selettori li distingue correttamente.

Ogni sezione di regole va avvolta nel suo layer:

@layer reset {
  /* regole di reset qui */
}

@layer base {
  /* body, container, divider qui */
}

@layer layout {
  /* contenitori strutturali qui */
}

E così via per components e states.

Un dettaglio importante: puoi aprire lo stesso @layer più volte nel file. Il browser raccoglie tutte le regole e le unisce nello stesso layer. Questo significa che non sei obbligato a mettere tutti gli stati in un unico blocco @layer states { } in fondo al file. Puoi, per esempio, tenere gli stati di ogni componente vicini al componente stesso:

@layer components {
  .btn { /* stile base */ }
  .btn--primary { /* variante */ }
}

@layer states {
  .btn:hover { /* hover */ }
  .btn--primary:hover { /* hover variante */ }
}

@layer components {
  .checkbox { /* stile base */ }
}

@layer states {
  .checkbox:hover { /* hover */ }
  .checkbox:checked { /* checked */ }
}

La priorità tra layer resta quella dichiarata nella prima riga (reset, base, layout, components, states) — non cambia in base a quante volte apri un layer o dove lo apri nel file. Questa flessibilità ti permette di organizzare il codice per componente (ogni componente ha il suo blocco components + states vicini) oppure per layer (tutti i componenti insieme, poi tutti gli stati insieme). Entrambi gli approcci sono validi. In questa esercitazione useremo il secondo: ogni layer appare una sola volta nel file, con tutti i componenti raggruppati e tutti gli stati raggruppati alla fine. Questo rende più semplice ragionare sulla cascade quando si sta ancora costruendo l'interfaccia — sai sempre dove cercare: aspetto visivo in components, comportamento interattivo in states.

Come decidere se una regola va in layout o components? La distinzione è: se l'elemento ha solo proprietà strutturali (display: flex, gap, flex-wrap, width, padding) va in layout. Se ha identità visiva (background, border, border-radius, box-shadow, font-size, color) va in components. Alcuni elementi hanno entrambe le cose — in quel caso, mettili in components.

@font-face

Subito dopo la dichiarazione @layer, e fuori da qualsiasi layer, registriamo i font. Le @font-face sono registrazioni, non regole di stile in competizione — non hanno bisogno di partecipare alla cascade dei layer.

Per usare un font personalizzato nel browser, devi "registrarlo" tramite la regola @font-face. È una dichiarazione che dice al browser: "quando nel CSS trovi questo nome di font-family con questo peso, carica questo file."

Il design usa Poppins in quattro pesi. Ecco le dichiarazioni complete:

@font-face {
  font-family: "Poppins";
  src: url("./assets/fonts/Poppins/Poppins-Regular.ttf") format("truetype");
  font-weight: 400;
  font-style: normal;
}

@font-face {
  font-family: "Poppins";
  src: url("./assets/fonts/Poppins/Poppins-Medium.ttf") format("truetype");
  font-weight: 500;
  font-style: normal;
}

@font-face {
  font-family: "Poppins";
  src: url("./assets/fonts/Poppins/Poppins-SemiBold.ttf") format("truetype");
  font-weight: 600;
  font-style: normal;
}

@font-face {
  font-family: "Poppins";
  src: url("./assets/fonts/Poppins/Poppins-Bold.ttf") format("truetype");
  font-weight: 700;
  font-style: normal;
}

Scomponiamo ogni proprietà:

Un errore molto comune è registrare un solo peso (es. Regular) e poi usare font-weight: 700 nel CSS aspettandosi il grassetto. Senza la @font-face per il peso 700, il browser tenta di "simulare" il grassetto sinteticamente — il risultato è visivamente diverso e meno pulito rispetto al vero file Bold disegnato dal type designer. Registra sempre tutti i pesi che intendi usare.

L'ordine delle dichiarazioni @font-face nel CSS non conta ai fini del funzionamento. Ma per leggibilità, conviene ordinarle per peso crescente (400, 500, 600, 700) — è una convenzione che rende il codice più facile da navigare.

Reset di base

Tre cose fondamentali:

Un suggerimento in più: imposta display: block e max-width: 100% su un selettore img. Il primo previene il comportamento inline di default (che aggiunge spazio sotto le immagini), il secondo impedisce che le immagini escano dal loro contenitore. È un pattern che in produzione si applica praticamente sempre.

Il pattern .container

Questa è una tecnica fondamentale che userai in qualsiasi progetto web. L'obiettivo è creare una classe riutilizzabile che gestisce il contenimento orizzontale del contenuto.

Il problema che risolve: senza vincoli, il contenuto si espande a tutta la larghezza del viewport. Su un monitor da 2560px, una riga di testo lunga 2000px è illeggibile. Serve un limite.

La classe .container fa tre cose:

  1. Limita la larghezza massima del contenuto con max-width — il contenuto non supera mai questa misura
  2. Centra orizzontalmente con margin-left: auto e margin-right: auto — lo spazio in eccesso viene distribuito equamente ai lati
  3. Aggiunge padding laterale con padding-left e padding-right — su viewport più strette del max-width, il contenuto non tocca i bordi dello schermo

Come calcolare il max-width: guarda il frame Desktop su Figma. Nota la larghezza totale e il padding orizzontale. Il max-width del container dovrebbe essere la larghezza totale del frame meno il padding sui due lati — oppure, se preferisci, puoi impostare il max-width come la larghezza totale e poi aggiungere il padding. Entrambi gli approcci funzionano, ma il secondo è più semplice da ragionare perché max-width include il padding (grazie a box-sizing: border-box).

Importante: width: 100% va specificato insieme a max-width, perché max-width da solo non imposta una larghezza base — definisce solo il limite superiore.

Un errore che capita spesso: usare margin: 0 auto e chiedersi perché non centra nulla. Il motivo è che l'elemento ha width: auto (default) — occupa già il 100% del genitore, quindi non c'è spazio da distribuire ai margini. Il max-width crea il vincolo, il width: 100% fa sì che l'elemento provi ad essere largo quanto il genitore, e quando il genitore è più largo del max-width, scatta il centraggio.

Applicherai .container sia all'<header> che al <main>. In questo modo, entrambi condividono lo stesso allineamento e la stessa larghezza massima — è fondamentale per la coerenza visiva della pagina.

La struttura semantica della pagina

Prima di passare al CSS, fermiamoci un momento sulla struttura HTML del <body>. I due figli diretti del body sono:

Questa distinzione non è solo teorica: un utente di screen reader che visita la pagina può premere un tasto per saltare direttamente al contenuto principale (<main>), bypassando header e navigazione. Se tutto fosse in <div> anonimi, questa possibilità non esisterebbe.

Spaziatura verticale del body

Osserva in Figma come sono distanziati verticalmente header e contenuto principale. Un approccio è usare display: flex, flex-direction: column e gap sul body, con padding-top e padding-bottom per il respiro verticale.

Perché flex e non semplicemente margin sui figli? Perché gap è più dichiarativo — dici "lo spazio tra gli elementi è X" una volta sola, invece di mettere margin-top o margin-bottom su ciascun figlio. Inoltre, gap non crea spazio extra prima del primo elemento o dopo l'ultimo — un problema classico del margin approach che richiede workaround come :first-child / :last-child.


1. Header

Osserva l'header nel design Figma. Contiene due gruppi: il logo a sinistra e la navigazione a tab a destra. Prova a pensare a queste cose:

Scelta degli elementi HTML

L'elemento per l'intestazione della pagina è <header>. Dagli le classi header e container.

Per i tab, usa un <nav> con classe .tab-nav — l'elemento <nav> segnala un blocco di navigazione. Gli screen reader lo espongono come landmark "navigation", permettendo agli utenti di raggiungerlo con una scorciatoia. Non ogni gruppo di link è un <nav> — solo quelli che rappresentano la navigazione principale.

Dentro il <nav>, i tab sono <a> con classe .tab-button. Perché <a> e non <button>? Un <a> rappresenta un collegamento a una risorsa (un'altra pagina o vista), un <button> rappresenta un'azione in-page. Se i tab cambiano vista, sono concettualmente link. Se togglassero la visibilità di un pannello nella stessa pagina senza cambiare URL, un <button> sarebbe più appropriato.

Il tab attivo ha una classe aggiuntiva .tab-button--active (naming BEM: base .tab-button, modificatore --active).

Il logo è un'<img> con logo-large.svg. L'alt dovrebbe essere il nome dell'app, dato che il logo lo contiene.

Come evitare il "salto" del box model tra tab attivo e inattivo?

Se metti border: 1px solid ... solo sul tab attivo, i due tab avranno un box model diverso — il tab attivo sarà 2px più largo e più alto (1px per lato). Quando cambi tab, tutto "salta". È un dettaglio piccolo ma visibile.

Il trick: dai border: 1px solid transparent al .tab-button base. Il bordo occupa spazio identico in entrambi gli stati. Sul .tab-button--active cambi solo border-color e aggiungi background-color. Il box model resta identico.

Come stilizzare il contenitore dei tab e i singoli tab?

Il .tab-nav è un contenitore pill-shaped: sfondo bianco, bordo, border-radius: 999px, piccolo padding interno e un sottile box-shadow solido (niente blur, piccolo offset — un pattern ricorrente in tutta l'app). Layout: display: flex con piccolo gap e align-items: center.

Ogni .tab-button è un flex container centrato con altezza specifica, padding, border-radius pill, e testo in Poppins SemiBold. Controlla Figma per i valori.

Ricordati che i <a> hanno stili di default: sottolineatura (text-decoration) e colore blu. Dovrai reimpostarli. Aggiungi white-space: nowrap per evitare che il testo vada a capo internamente.

L'header stesso è un flex container con justify-content: space-between e align-items: center. Aggiungi flex-wrap: wrap con gap — su schermi stretti, logo e tab possono andare a capo senza traboccare.


2. Sezione Flashcard — struttura

Guarda la sezione flashcard nel design Figma. È la colonna principale della pagina — una card bianca che contiene tre zone distinte (filtri, flashcard vera e propria, navigazione), separate da linee orizzontali. Pensa a:

Quale elemento HTML per un raggruppamento tematico?

Usa una <section> con classe .flashcard-section. Perché non un <div>? Una <section> rappresenta un raggruppamento tematico di contenuto, tipicamente con un proprio titolo. In questo caso, filtri, flashcard e navigazione appartengono allo stesso contesto funzionale — "l'area di studio". Un <div> sarebbe semanticamente neutro. La regola pratica: se il contenuto ha un tema coerente e potresti dargli un titolo (anche se non lo mostri visivamente), è una <section>.

Dentro ci sono tre zone, separate da divisori <hr>:

  1. Filtri (form dei controlli) — sezione 3
  2. Area flashcard (la card + azioni) — sezioni 4 e 5
  3. Navigazione (Previous / Card counter / Next) — sezione 6

L'<hr> è semanticamente corretto qui: rappresenta una separazione tematica tra sezioni di contenuto. Dagli una classe .divider.

L'area flashcard (punto 2) è wrappata in un <div> con classe .flashcard-container — non è una sezione semantica separata, è un semplice contenitore per padding e layout.

Come stilizzare la sezione e i divisori?

La sezione è una card bianca con bordi asimmetrici — il bordo destro e inferiore sono più spessi. Per i bordi asimmetrici, usa border-style: solid, border-color e border-width: <top> <right> <bottom> <left> (ordine in senso orario). Controlla su Figma quali lati sono più spessi.

Layout interno: display: flex con flex-direction: column e align-items: center.

Per la larghezza: nel layout Desktop, la sezione ha una misura fissa. Non impostarla come width fisso — nella sezione 9 la trasformeremo in layout flessibile. Per ora, concentrati sulla struttura interna.

Per i divisori <hr> (classe .divider), l'<hr> ha stili di default del browser (bordi). Rimuovili con border: none, poi imposta height: 1px, background-color e width: 100%.


3. Form dei filtri

La barra sopra la flashcard contiene controlli per filtrare le card: un dropdown per la categoria, un checkbox "Hide Mastered", e un bottone per applicare. Pensa a:

Placeholder con selected disabled

Il <select> deve mostrare un testo placeholder che non è un'opzione valida:

<option value="" selected disabled>All Categories</option>

Raggruppare le opzioni con <optgroup>

Se le categorie sono logicamente raggruppabili, usa <optgroup> con l'attributo label:

<optgroup label="Programming">
  <option value="web-development">Web Development</option>
  <option value="javascript">JavaScript</option>
</optgroup>

Il browser renderizza il label come intestazione non selezionabile. L'<optgroup> non modifica il funzionamento del form — è puramente un'organizzazione visiva. Non è annidabile: un solo livello di raggruppamento.

Inventa pure le categorie — l'importante è la struttura.

Perché <form> e non <div>?

Avvolgere i filtri in un <form> ha un vantaggio concreto: se domani aggiungi un backend, il form invia i dati nativamente senza JavaScript.

Con method="get" (il default), i parametri finiscono nella URL come query string (es. ?category=css&hide-mastered=true). Questo è ideale per i filtri:

  1. L'utente può fare bookmark del risultato filtrato
  2. Il bottone "indietro" del browser riporta ai filtri precedenti

In produzione, method="get" per letture/filtri, method="post" per modifiche di stato. È una distinzione fondamentale.

Struttura HTML del form

Classe: .flashcard-header sul <form>. Contiene:

Dentro .flashcard-filters:

Per il checkbox: la label wrappa il checkbox — classe .checkbox-label sulla <label>. Il checkbox ha name, id e value, e la label ha for che punta all'id. Click sulla label = toggle del checkbox.

Il bottone usa le classi .btn e .btn--primary (variante gialla). Definisci una classe base .btn riutilizzabile per tutti i bottoni dell'app.

Come personalizzare l'aspetto del select?

Il <select> nativo ha un aspetto diverso su ogni browser — la freccia dropdown è impossibile da personalizzare senza un reset.

Il primo passo è appearance: none — rimuove lo stile nativo. Per ricreare la freccia:

Aggiungi bordo, border-radius pill, font-family e font-weight. Un dettaglio: i form element in HTML non ereditano font-family dal body — devi specificarlo esplicitamente.

Come personalizzare l'aspetto del checkbox?

Come per il select, il primo passo è appearance: none. Poi ricostruisci da zero:

Lo stato :checked lo vedremo nella sezione 8.

Come organizzare il layout del form?

Il .flashcard-header usa flexbox con justify-content: space-between (filtri a sinistra, bottone a destra), flex-wrap: wrap e gap. Su schermi stretti, il bottone va a capo.

width: 100% per occupare tutta la sezione, con padding interno.

Anche .flashcard-filters è un flex container con gap, align-items: center e flex-wrap: wrap — label, select e checkbox possono andare a capo indipendentemente.


4. La Flashcard

Osserva la flashcard nel design Figma. È l'elemento più complesso della pagina: ha un colore di sfondo, un pattern SVG sovrapposto, stelle decorative negli angoli, e il contenuto testuale al centro. L'intera card è cliccabile — il click rivela la risposta.

Fermati e prova a rispondere a queste domande prima di continuare:

CSS Grid per sovrapposizioni

Qui c'è una tecnica avanzata che abbiamo già incontrato. Le stelle decorative devono sovrapporsi al contenuto della card, posizionate agli angoli. L'approccio è usare CSS Grid per creare layer sovrapposti.

Metti display: grid sul container. Poi, su tutti i figli diretti, imposta:

grid-row: 1;
grid-column: 1;

Questo forza tutti i figli nella stessa cella della griglia — la cella 1,1 — sovrapponendoli. È come un sistema a livelli: ogni figlio è un layer sopra il precedente. L'ordine nel DOM determina quale sta sopra (l'ultimo nel codice sta in cima visivamente), o in alternativa si può usare la proprietà z-index.

Un approccio comodo è usare un selettore generico per tutti i figli diretti:

.flashcard > * { ... }

A questo punto, ogni figlio si posiziona indipendentemente nella cella usando align-self e justify-self. Per esempio, justify-self: end con align-self: start posiziona un elemento nell'angolo in alto a destra della cella. Per spostamenti più fini rispetto agli angoli esatti, aggiungi margin.

Non sai quale elemento HTML usare per la card cliccabile?

La card è un <button> con classe .flashcard e type="button" (non deve submittare nessun form). Perché <button> e non un <div> con un click handler?

Con un <div> dovresti aggiungere manualmente tabindex="0", role="button", e gestire i keypress — tutto lavoro che <button> fa gratis.

La flashcard è wrappata in un <div> con classe .flashcard-container che contiene sia la card che il form delle azioni (sezione 5). Questo container gestisce il padding e la spaziatura tra card e bottoni.

Hai scritto l'HTML della card ma non sai come organizzare i figli per sovrapporli tra loro?

Dentro la flashcard ci sono tre elementi fratelli, tutti figli diretti del <button>:

  1. .flashcard-body: il contenuto vero della card (tag, domanda, hint, progress bar)
  2. .flashcard-star--top: <img> della stella blu (angolo alto-destra)
  3. .flashcard-star--bottom: <img> della stella gialla (angolo basso-sinistra)

L'ordine conta: il body sta sotto, le stelle sopra (ultimo nel DOM = in cima visivamente).

Le stelle sono <img> con alt="" (attributo alt vuoto, non assente) — sono decorative, non informative. Un alt vuoto dice agli screen reader di ignorarle. Condividono una classe base .flashcard-star per le proprietà comuni.

Aggiungi pointer-events: none sulle stelle — non devono intercettare i click destinati al <button>. Senza questa proprietà, il click su una stella non attiverebbe il bottone sottostante.

Per posizionare le stelle agli angoli con il grid stacking:

Controlla Figma per le dimensioni delle stelle e l'offset esatto dagli angoli.

Hai bisogno di aiuto con lo sfondo e il pattern della card?

Il pattern è decorativo — non un <img>, ma una proprietà CSS. Usa background-color per il rosa e background-image per il pattern (pattern-flashcard-bg.svg).

Per coprire l'intera card: background-size: 100% 100% allarga il pattern per riempire sia larghezza che altezza. Con background-repeat: no-repeat eviti ripetizioni.

Aggiungi overflow: hidden per ritagliare il pattern agli angoli arrotondati, e un'altezza fissa (controlla Figma).

Un dettaglio che si dimentica spesso: i <button> non ereditano font-family e color dal body — devi impostarli esplicitamente. Aggiungi anche cursor: pointer per comunicare che è cliccabile.

Non sai come organizzare il contenuto interno della card?

Il .flashcard-body è un flex column con align-items: center e gap. Contiene, dall'alto in basso:

Che elemento usare per il "progresso"?

L'elemento <progress> è l'elemento HTML nativo per rappresentare il completamento di un'attività. Due attributi fondamentali:

<progress class="progress-bar" max="5" value="1">1/5</progress>

Il testo dentro <progress> è un fallback per browser che non supportano l'elemento.

Non confondere <progress> con <meter><progress> è per attività in corso (completamento), <meter> è per misurazioni scalari (temperatura, uso disco). Qui stiamo mostrando la padronanza come un percorso da 0 a 5, quindi <progress> è corretto.

Styling del <progress>: è uno degli elementi HTML più ostici da personalizzare. Il primo passo è appearance: none per rimuovere lo stile nativo.

Poi servono gli pseudo-elementi vendor — selettori che targettano parti interne dell'elemento specifiche di ciascun browser:

Per ciascuno, imposta background-color e border-radius. Sull'elemento <progress> stesso, imposta larghezza, altezza, bordo, border-radius e overflow: hidden.

Il testo accanto (.progress-text) ha lo stesso stile tipografico del tag badge — font piccolo con letter-spacing negativo.

Un consiglio: testa con diversi valori di value (0, 1, 3, 5). I casi value="0" (barra vuota) e value="5" (barra piena) sono quelli dove i problemi di border-radius emergono più facilmente.


5. Azioni sulla card

Sotto la flashcard ci sono due bottoni: "I Know This" (segna la card come padroneggiata) e "Reset Progress" (azzera il progresso). Entrambi modificano lo stato della card. Pensa a:

Il pattern dei submit multipli

Questo è un pattern fondamentale per i form con più azioni:

<input type="hidden" name="card-id" value="1" />
<button type="submit" name="action" value="master">I Know This</button>
<button type="submit" name="action" value="reset">Reset Progress</button>

Tre concetti:

  1. <input type="hidden">: invia card-id=1 senza mostrarlo all'utente — il server sa quale card è stata aggiornata, l'utente non ha bisogno di vedere quell'informazione
  2. Due submit con stesso name: solo il bottone cliccato invia la sua coppia name/value. Il server riceve action=master oppure action=reset — mai entrambi
  3. type="submit": entrambi devono essere type="submit" — con type="button" il click non invierebbe il form
Perché method="post" e non "get"?

Il trade-off: GET mette i parametri nella URL (visibili, bookmarkabili, ripetibili), POST li mette nel corpo della richiesta (non ripetibili con il bottone indietro).

Se "I Know This" usasse GET, l'utente potrebbe premere "Indietro" e ri-eseguire l'azione involontariamente. Con POST, il browser chiede conferma prima di ri-inviare. Per azioni che modificano dati, usa sempre POST.

Struttura HTML completa del form

Classe del form: .flashcard-actions, con method="post".

Ogni bottone contiene un'icona <img> (classe .btn-icon) e il testo. L'ordine nel markup determina l'ordine visivo — icona prima del testo.

Riusa le classi:

Hai considerato cosa succede se l'utente preme Enter dentro questo form? Con due submit button, il browser attiva il primo nell'ordine del DOM — "I Know This". Un comportamento ragionevole.

Come definire il sistema di bottoni (classe base + varianti)?

L'approccio è una classe base .btn con tutte le proprietà comuni:

E due classi modificatrici:

L'icona (.btn-icon) ha dimensioni fisse e flex-shrink: 0.

Il .flashcard-actions è un flex container centrato (justify-content: center) con gap e flex-wrap: wrap.


6. Navigazione

La barra in fondo alla sezione flashcard contiene tre elementi: un bottone "Previous", un contatore "Card 1 of 40", e un bottone "Next". Pensa a:

Scelta degli elementi HTML

Un <nav> con classe .flashcard-nav. Nota: questa è la seconda <nav> nella pagina. Gli screen reader elencano tutti i landmark "navigation" — l'utente non sa distinguerli. Il modo per risolvere è aggiungere un aria-label a ciascuno (es. aria-label="Navigazione principale" sull'header, aria-label="Navigazione flashcard" su questa). È un dettaglio di accessibilità da tenere a mente.

Contiene:

  1. Bottone "Previous" — icona chevron sinistra prima del testo
  2. <span> con classe .card-counter per "Card 1 of 40" — uno <span> è un contenitore inline senza significato semantico, la scelta giusta per un gancio di stile
  3. Bottone "Next" — icona chevron destra dopo il testo

L'ordine nel markup determina l'ordine visivo — nessun trick CSS necessario.

I bottoni sono type="button" (non inviano form) e usano la classe base .btn senza modificatori — la gerarchia "Border" del design system (solo bordo, niente shadow).

Come centrare il contatore tra i due bottoni?

Il .flashcard-nav usa flexbox con width: 100%, padding, flex-wrap: wrap e gap.

Per centrare il counter: dagli flex: 1 con text-align: center. Il flex: 1 lo espande per occupare tutto lo spazio tra i bottoni, e text-align: center centra il testo internamente.

Su viewport molto strette, con flex-wrap: wrap gli elementi andranno a capo. Non sarà perfetto — ma senza media query è un compromesso accettabile.

Il .card-counter ha font più piccolo e colore meno contrastato (controlla Figma) — è un'informazione secondaria.


7. Sezione Statistiche

La colonna destra (su desktop) mostra quattro card di statistiche: Total Cards, Mastered, In Progress, Not Started. Ogni card ha un'etichetta, un numero e un'area colorata con un'icona. Pensa a:

CSS Grid con auto-fill

Qui c'è una tecnica che potresti ricordare. Il problema: il numero di colonne deve adattarsi automaticamente allo spazio disponibile del contenitore — non del viewport.

La proprietà chiave è:

grid-template-columns: repeat(auto-fill, minmax(250px, 1fr))

Scomponiamola:

In pratica:

Il browser calcola tutto da solo. Il valore 250px nel minmax è il punto di rottura — sceglilo in base alla larghezza minima a cui una stat card è ancora leggibile.

Un dettaglio: auto-fill vs auto-fit. Con auto-fill, lo spazio extra viene riservato per colonne potenziali (anche vuote). Con auto-fit, le colonne vuote vengono collassate e lo spazio redistribuito. Per questo caso, auto-fill va bene.

Quale elemento per il contenuto complementare? E per le card ripetibili?

Un <aside> con classe .statistics-section — semanticamente appropriato perché le statistiche sono contenuto complementare. Se la sezione flashcard è il "contenuto principale" (dove l'utente studia), le statistiche sono "informazione di supporto" che arricchiscono l'esperienza ma non la definiscono.

Contiene:

Ogni stat card (.stat-card) è un <article> — un pezzo di contenuto autonomo e ripetibile. Dentro:

Come stilizzare la sezione e la griglia?

La .statistics-section è un flex column con padding e gap, bordi asimmetrici e border-radius (valori leggermente diversi dalla flashcard section — controlla Figma).

Per .stats-list, usa display: grid con repeat(auto-fill, minmax(250px, 1fr)) e gap.

Come stilizzare la singola stat card?

La .stat-card è un flex container orizzontale con due aree:

Classi modificatrici BEM per i colori: .stat-icon--blue, .stat-icon--teal, .stat-icon--pink, .stat-icon--magenta. Ognuna imposta solo background-color.

Le card hanno bordi asimmetrici (spessori diversi dalle sezioni — controlla Figma) e border-radius. L'overflow: hidden è importante: senza, il colore di sfondo dell'icona "esce" dagli angoli arrotondati.


8. Stati interattivi

Ogni elemento interattivo dell'interfaccia deve dare feedback visivo all'utente. Il hover comunica "questo è cliccabile", il focus comunica "sei qui con la tastiera", lo stato checked comunica "questo è selezionato". Senza questi feedback, l'interfaccia sembra statica.

Il file Figma contiene varianti dei componenti per ogni stato. Cercale: _Tab button base, Button (con le gerarchie Primary, Secondary, Border), _Checkbox base. Prova a:

Il focus ring con box-shadow multipli

Il pattern di focus è coerente in tutta l'app: un doppio anello ottenuto con box-shadow multipli:

box-shadow: 0 0 0 Xpx white, 0 0 0 Ypx <colore>

box-shadow accetta valori multipli separati da virgola. Entrambi hanno offset 0 e blur 0, ma spread diverso:

Il risultato è un doppio ring (colorato → bianco → bordo dell'elemento) — un pattern di accessibilità che garantisce visibilità su qualsiasi sfondo.

:focus-visible vs :focus

La differenza è importante:

Con :focus vedresti il ring anche dopo ogni click. Con :focus-visible, solo quando serve davvero. Aggiungi outline: none per rimuovere l'outline di default del browser.

Quali proprietà cambiano al hover per ciascun componente?

Prendi i valori esatti da Figma. Un dettaglio sulla cascade: siccome tutti gli hover sono nel layer states, .btn--primary:hover vince su .btn:hover per specificità (due classi vs una), indipendentemente dall'ordine nel file.

Come implementare il focus ring sui diversi componenti?

Per tab, checkbox, select e bottoni Border: usa il doppio ring con box-shadow. Ricordati outline: none.

Per bottoni Primary e Secondary: controlla su Figma se il focus usa un shadow direzionale (con offset, ma di colore diverso — es. blu) anziché il ring circolare. In produzione si mantiene coerenza, ma segui il design.

Come gestire lo stato :checked del checkbox e le sue combinazioni?

Quando il checkbox è spuntato:

Nota: appearance: none rimuove lo stile nativo ma il checkbox mantiene il comportamento — resta toggabile con click e Space.

Le combinazioni da coprire:

Se il focus ring sovrascrive il box-shadow dell'hover (entrambi usano la stessa proprietà), il focus ring vince — è un indicatore di accessibilità.


9. Layout responsivo (senza media query)

Il Figma mostra tre breakpoint (Desktop, Tablet, Mobile), ma non useremo media query. L'idea è diversa: non dire al browser quando cambiare layout, ma dargli regole flessibili che lo facciano adattare da solo.

Confronta i tre breakpoint su Figma e prova a identificare:

flex-wrap con flex-basis

Questa tecnica permette a due elementi di stare affiancati quando c'è spazio e impilarsi quando non ce n'è — senza breakpoint espliciti.

Scomponiamo flex: 1 1 500px:

Se due flex items con basis 500px e 300px sono in un container da 900px: 500 + 300 + gap ≈ 832px < 900, stanno entrambi su una riga. In un container da 700px: 832 > 700, non ci stanno — con flex-wrap: wrap, il secondo va a capo.

Il flex-basis è il punto di "rottura implicito". Scegli valori che riflettano la larghezza minima a cui ogni sezione funziona bene.

Come applicare il wrapping al layout principale?
  1. Sul .main-content: aggiungi flex-wrap: wrap (il display: flex e il gap li hai già)
  2. Sulla .flashcard-section: sostituisci la larghezza fissa con flex: 1 1 500px
  3. Sulla .statistics-section: sostituisci la larghezza fissa con flex: 1 1 300px

Aggiungi min-width: 0 su entrambe — previene un bug di flexbox dove un flex item non può essere più stretto del suo contenuto.

La classe .container della sezione 0 gestisce già il centraggio e il padding laterale su tutte le viewport. max-width + padding + margin: auto fa tutto da solo.

Le stat card in griglia si adattano già?

Se hai usato repeat(auto-fill, minmax(250px, 1fr)) per .stats-list:

La griglia risponde alla larghezza del suo contenitore, non del viewport. Nessun codice aggiuntivo.

Dove aggiungere flex-wrap agli altri componenti?

Aggiungi flex-wrap: wrap (con gap) a:

Cosa non riesci a fare senza media query?

Questo approccio è potente ma ha limiti concreti:

Queste sono le situazioni in cui servirebbero le media query — le imparerai prossimamente. Per ora, il layout si comporta ragionevolmente su tutte le dimensioni grazie alle proprietà intrinseche.


Ordine consigliato di sviluppo

Un approccio che funziona bene:

  1. Prima l'HTML, tutto: scrivi l'intera struttura HTML senza CSS. Apri la pagina nel browser — dovrebbe essere brutta ma funzionale: i link sono cliccabili, il form si può compilare, il select mostra le opzioni, il checkbox si può spuntare. Se il contenuto è accessibile senza CSS, la struttura è corretta

  2. Poi il CSS, per sezione: parti dalla dichiarazione @layer, poi le @font-face, poi procedi layer per layer — reset, base, layout, components. Dopo ogni componente, verifica nel browser

  3. Il layer states per ultimo: aggiungi gli stati interattivi (hover, focus, checked) nel layer states solo dopo che layout e componenti funzionano correttamente

  4. Il layout responsivo come ultimo step: le proprietà flex-wrap, flex-basis e auto-fill vanno aggiunte alla fine, quando tutti i componenti sono già stilizzati. Altrimenti stai cercando di risolvere due problemi contemporaneamente


Riepilogo delle tecniche utilizzate

In questa esercitazione hai esplorato:

Hai costruito un'interfaccia completa — con filtri, card interattiva, navigazione e statistiche — che si adatta a diverse dimensioni di schermo usando esclusivamente proprietà intrinseche del CSS. Sono concetti che userai costantemente in qualsiasi progetto web professionale.

La prossima volta che vedrai un sito o una web app, prova a identificare quali di queste tecniche vengono usate — il pattern container, i bordi asimmetrici, i focus ring, le griglie responsive. Ti accorgerai che sono ovunque.