Due righe, qualsiasi progetto
Copia candela.css, candela.js e la cartella fonts/ nel progetto. I font sono self-hosted e dichiarati dentro candela.css: nessuna chiamata a Google Fonts (conformità GDPR), copertura latina e cirillica inclusa.
Un attributo, sette identità
La sorgente calda è una sola variabile: --cd-accent-rgb. Tinte, glow, bottoni e checkbox si rigenerano da soli con color-mix(). Provali — l'intera pagina cambia:
Vetro · 4 elevazioni
L'altezza si misura in sfocatura: blur, opacità e bordo crescono insieme. g1 righe e celle · g2 card e pannelli · g3 nav e menu · g4 modali. Mai vetro dentro vetro.
blur 12
blur 20
blur 32
blur 48
Bottoni
In hover si accendono, non si muovono: il primario passa da glow-2 a glow-3. Il caldo è l'azione del prodotto, il freddo la selezione dell'interfaccia.
Form
Il focus è sempre luce fredda; l'errore porta il proprio alone. Checkbox, radio e switch sono input nativi ridisegnati: funzionano nei form senza JS.
Il focus accende l'alone freddo.
IBAN incompleto: servono 27 caratteri.
Badge · Chip · Tag
Il punto luminoso rende lo stato leggibile a colpo d'occhio. I chip sono filtri selezionabili: attivi, prendono l'accent.
Card
La card in hover sale di 5px e guadagna glow: si avvicina alla luce. --flush azzera il padding per contenuti con media.
Card semplice
Contenitore g2 con hover.
Attico vista mare
Sanremo · 210 m² · terrazza
€ 1.450.000Modal
Basata su <dialog> nativo: focus trap e Esc gratis dal browser. Il backdrop sfoca la pagina — è l'unico g4 della scena.
Tre modi di chiudere, tutti dallo stesso gestore: la × in alto, Esc, e il click sull'area sfocata — che vale per modale, drawer e palette. Su desktop quel click ha una forma visibile: su tutta la parte sfocata il cursore diventa una manina, compare un pallino che si dilata e sale sopra il puntatore diventando il pulsante CLOSE. Vale allo stesso modo per modale, drawer, palette e menu a pillola. Lo mette il framework, non si scrive nel markup, e per chi ascolta non esiste: è la stessa azione della ×, con l'altra mano.
Dropdown
Menu su g3 (è un elemento fluttuante). Chiusura su click fuori, su voce selezionata e con Esc.
Tabs
Navigabili con le frecce, ARIA completo. La tab attiva prende accent + glow-1.
Contenuto della descrizione.
Superficie, classe energetica, spese.
Qui la mappa dell'immobile.
Accordion
<details> nativo ridisegnato: accessibile e indicizzabile. L'apertura e la chiusura sono animate — il click viene intercettato perché togliendo l'attributo open il browser nasconderebbe il contenuto all'istante. Apertura con ease-out, chiusura con ease-in-out, e senza JS resta il comportamento nativo.
Quanto costa la proposta?
Tempi del rogito
Toast
API a una chiamata; il contenitore si crea da solo con aria-live.
Tooltip
Solo CSS, da un attributo. Appare anche al focus da tastiera.
Tabella
La riga selezionata usa la luce fredda: è una selezione dell'interfaccia, non un'azione del prodotto.
| Immobile | Zona | Stato | Prezzo |
|---|---|---|---|
| Attico Duomo | Centro | attivo | € 1.450.000 |
| Villa Iris | Collina | trattativa | € 890.000 |
| Bilocale Porto | Marina | sospeso | € 320.000 |
Alert
Progress · Spinner · Skeleton
Avatar · Divider · Kbd
Reveal allo scroll
Un attributo fa entrare in scena l'elemento quando raggiunge la viewport; data-cd-stagger fa entrare i figli in sequenza. Con prefers-reduced-motion tutto appare fermo, e senza JS niente resta nascosto.
Entro per primo
Poi io
Per ultimo io
Spotlight
Un alone tenue segue il puntatore sulla superficie — questo pannello lo sta già facendo. Un solo listener delegato per tutta la pagina: aggiungi .cd-spot e basta.
Muovi il mouse qui
La luce ti segue.
Spot + hover
Si combinano senza conflitti.
Statistiche animate
Il contatore parte quando entra in viewport, con formattazione italiana dei numeri. Il testo nel markup è il fallback senza JS.
Stepper
Processi e timeline: i passi completati portano l'accent, quello corrente riceve glow-2. Perfetto per checkout e iter di compravendita.
Proposta d'acquisto
Accettata il 12 marzo.
Preliminare
Registrato, caparra versata.
Istruttoria mutuo
In corso — delibera attesa entro 20 giorni.
Rogito
Da fissare dal notaio.
Pricing
Il piano consigliato è l'unico che porta glow: la sorgente indica la scelta. Pronto per abbonamenti e listini.
- Sito one-page
- Hosting incluso
- Assistenza email
- Sito multipagina
- SEO + analytics
- Aggiornamenti mensili
- Assistenza prioritaria
- Tutto di Pro
- E-commerce
- Gestione contenuti
Gallery + Lightbox
Scorrimento a scatti con scroll-snap; il click apre l'immagine piena in un lightbox creato al volo. Pensata per gli immobili: sostituisci le immagini demo con le foto reali.
Drawer
Pannello laterale su <dialog> con uscita animata. È anche la nav mobile: sotto gli 860px nella barra in alto compare il burger, provalo restringendo la finestra.
Command palette
Il gesto dei prodotti moderni: ⌘ K (o Ctrl+K) apre la ricerca comandi — funziona adesso, su questa pagina. Filtro live, frecce per muoversi, Invio per eseguire. La selezione usa la luce fredda: è l'interfaccia che sceglie, non il prodotto.
Empty state
Uno schermo vuoto è un invito ad agire, non un vicolo cieco.
Nessun immobile in archivio
Aggiungi il primo annuncio per vederlo comparire qui con foto e stato.
Hero + Source
La firma del sistema come componente riutilizzabile: la sorgente pulsante vista attraverso un pannello g3. Un solo .cd-source per pagina — è il glow-4, la sorgente.
Marquee + Quote
La striscia scorre in loop (duplica il contenuto nel markup, in pausa all'hover) e la testimonianza porta il segno dell'accent. I bordi della striscia sfumano con una mask.
Hanno venduto il nostro attico in sei settimane, gestendo tutto: foto, visite, notaio.
Cookie banner
Il banner e i suoi due tasti, che portano la stessa classe perché rifiutare deve costare quanto accettare. Quello vero è comparso in basso a sinistra su questa pagina; Candela.consent('docs-demo', null) lo fa tornare. Il modello completo — finalità, pannello, scadenza e soprattutto la primitiva che blocca le risorse di terzi — sta in Consenso.
Il banner di questa pagina è vero e sta in fondo al documento: ricorda la scelta per 180 giorni, la lega alla versione dell'informativa e comanda le risorse di terzi. Da qui lo puoi riaprire invece di guardarne una copia inerte.
Direzione
Il sistema funziona da destra a sinistra con un attributo e nient'altro. Nel foglio non ci sono più lati fisici: margin-inline-start al posto di margin-left, inset-inline-end al posto di right. Prova qui sotto: cambia solo il verso della pagina, non il markup.
Guarda la barra in alto, il torna-su, il pannello del menu e la freccia del select: si specchiano tutti. Il centraggio no, ed è giusto così.
Modalità
Tre assi indipendenti, tutti ad attributi. La densità stringe il ritmo per CRM e pannelli; l'inchiostro "abisso" scurisce tutto per gli OLED; e l'accent vale anche su un singolo contenitore — la card qui sotto è in copper mentre la pagina resta com'è.
Sezione ritemata
Le variabili cascano dal contenitore: glow e bottoni seguono.
Validazione nativa
Zero JS: con :user-invalid l'errore compare solo dopo l'interazione, mai mentre scrivi la prima volta. Prova a inviare vuoto.
Scrollspy · Skip · Torna su · Scroll lock
L'indice qui a fianco si sta già evidenziando da solo (data-cd-spy sul contenitore dei link). Premi Tab da inizio pagina per vedere lo skip link; il bottone torna-su è comparso in basso a destra; e quando una modale è aperta la pagina dietro non scorre più.
Starter
Nel pacchetto trovi starter.html: un sito completo già montato — skip link, nav con burger e drawer, hero con sorgente, marquee, card spotlight, statistiche, form validato, footer, cookie banner, torna-su e palette. Per un progetto nuovo: copia la cartella, apri lo starter, sostituisci i testi. In produzione usa le build minificate candela.min.css e candela.min.js.
Grafici
SVG generati dal framework, zero dipendenze, un attributo con i valori. Si disegnano quando entrano in viewport; la temperatura segue le regole del sistema — accent per il prodotto, freddo/ok/bad per i segnali. --glow aggiunge l'alone.
Sparkline · accent + glow
Sparkline · cool
Barre
Anello
App shell
La struttura per CRM e pannelli: sidebar sticky in vetro con voce attiva, topbar, contenuto. Sotto i 940px la sidebar sparisce e il burger apre un drawer. Vedila montata e funzionante in template-dashboard.html, insieme a KPI, grafici e tabella.
Kit AI
Nel pacchetto c'è CANDELA-PROMPT.md: le istruzioni da dare a Claude a inizio progetto. Contiene le regole non negoziabili, l'inventario completo delle classi, le API e i "non fare" — così ogni sessione di lavoro produce codice coerente col sistema invece di reinventare bottoni e ombre. Mettilo nella cartella del progetto e chiedi all'AI di leggerlo prima di scrivere una riga.
Dish + Quantità
La riga di menu con il prezzo a puntini — il classico dei menu, ridisegnato — e il selettore quantità per e-commerce e ordini. Il template template-menu.html monta il menu completo in copper.
Con fagiolini e patate, come da tradizione.
Prima/dopo + Video leggero
Il confronto trascinabile per ristrutturazioni e restyling, e la facade video: non contatta nessuno finché non premi. Fino alla 11.1 l'anteprima arrivava da i.ytimg.com al caricamento della pagina, prima di qualunque scelta; ora senza data-cd-poster la facciata si disegna da sola e scrive il dominio che il clic aprirà. L'iframe parte al click, su youtube-nocookie.com.
Countdown + Condividi
Il conto alla rovescia per open house, lanci e offerte; il bottone condividi usa la Web Share nativa su mobile e copia il link altrove.
Stampa in chiaro
Su carta il dark è uno spreco di inchiostro: con ⌘P il framework spegne atmosfera, vetro e glow da solo — sfondo bianco, testo scuro, niente bottoni né banner. Un menu o una scheda immobile si stampano bene senza fare nulla. Provalo su questa pagina o su template-menu.html.
Verificato dal vivo
Questa versione non aggiunge componenti: corregge. Il framework è stato renderizzato in un browser reale e sottoposto ad audit. Le correzioni: i link dentro drawer e palette ora scrollano davvero alla destinazione dopo la chiusura (lo scroll-lock delle modali inghiottiva il salto all'ancora); la stampa mostra anche sezioni e grafici non ancora rivelati; il prima/dopo ha il focus visibile da tastiera; con trasparenza ridotta anche i backdrop di drawer, palette e lightbox diventano pieni; la rotellina resta contenuta dentro pannelli e menu; il countdown è localizzabile per i progetti in altre lingue.
Misurato (12.1): axe-core 4.13 — 0 violazioni su 112 misure: 9 pagine × 2 viewport (1280 e 390 px), ciascuna anche negli stati aperti — menu, vista della ricerca, modale, drawer, palette, lightbox, banner del consenso e pannello delle preferenze — più due misure da destra a sinistra per pagina. Zero elementi oltre il viewport a 320 px, e dalla 12.1 quello zero è dimostrato: il controllo veniva provato iniettando uno sbordo di 260 px, e finché il <body> contava fra i contenitori che ritagliano non poteva segnalarlo. Zero elementi focalizzabili ma invisibili. Zero errori a runtime, zero id doppi, zero riferimenti ARIA nel vuoto su tutte e nove le pagine, con gli overlay aperti (npm run integrita). Contrasto verificato sui pixel reali, non dedotto dal CSS, su 15 campioni × tutti e sette gli accent — 105 misure, minimo 4,72:1 contro una soglia di 4,5. Fino alla 12.0 il contrasto era misurato su una variante sola, e il marquee sopra l'orb stava a 4,05:1 senza che nessuno lo sapesse. Nessuna pagina contatta terzi al caricamento (npm run terzi), e nessuna regola larga scavalca un componente su 954 elementi distinti (npm run specificita). Si rimisura con npm run audit, contrast:tutti, integrita e specificita.
Perché gli stati contano: axe salta i sottoalberi visibility:hidden e inert, quindi su una pagina a riposo non vedrebbe mai il pannello del menu né gli overlay — misurare solo il caricamento statico avrebbe dato «0 violazioni» senza guardare dove sono i rischi. Fuori dalla portata degli strumenti, e da guardare a mano: forced-colors su Windows reale, l'ordine di lettura e la chiarezza delle etichette.
Un repo, tutti i siti
La cartella è pronta per Git: LICENSE.txt, CHANGELOG.md e gli strumenti in tools/. Pubblicata su GitHub, jsDelivr la serve come CDN gratuito: correggi una volta, si aggiornano tutti i siti che puntano alla versione. I font viaggiano sul CDN con gli stessi percorsi relativi.
Nel pacchetto anche gli strumenti: theme-builder.html genera il tema di un cliente con verifica del contrasto live e download del CSS; npm run build rigenera le versioni minificate, il bundle e lo specimen a partire dai sorgenti, e npm run check verifica che nessuna pagina usi classi cd- inesistenti.
Liste che filtrano davvero
Fino alla 10.10 chip, paginazione e tabella erano gusci: si vedevano e non facevano niente. Ora filtro e paginazione lavorano sullo stesso insieme, in quest'ordine — impaginare prima di filtrare impaginerebbe anche ciò che è stato escluso.
Nessun immobile con questi criteri.
Poi: <th data-cd-sort> mette dentro un <button> vero e gestisce aria-sort — i numeri scritti all'italiana si ordinano da numeri, e data-cd-value sulla cella ordina per un valore diverso da quello mostrato. .cd-bulk[data-cd-bulk="#tabella"] conta le caselle, evidenzia le righe e gestisce lo stato indeterminato, parlando in luce fredda perché la selezione è interfaccia, non prodotto. .cd-range-dual è un intervallo a due maniglie che non si scavalcano. input.cd-file in .cd-file-drop accetta il trascinamento.
Segni d'interfaccia e marchi
Due famiglie, due attributi, una tabella sola. I segni sono disegnati con la grammatica del sistema — riquadro 24, tratto 1,6, ingombro ottico 17. I marchi sono i loghi veri: un logo ridisegnato per entrare in una famiglia resta un'approssimazione di quel logo.
I quindici marchi hanno due facce: il glifo solo, che prende il colore del testo, e il badge col disco di marca. Il default è mono e non per prudenza — quindici dischi saturi su un fondo scuro sono quindici buchi di luce che si contendono l'attenzione col contenuto. A colori hanno senso dove sono il contenuto. I quattro segni di contatto si accendono col tuo accent, così in una fila mista non resta metà accesa e metà spenta.
Il consenso che blocca davvero
Un banner che memorizza un booleano ed emette un evento non impedisce niente: è un filtro che non filtra. Il valore del componente non sta nel banner — sta nella primitiva che impedisce alle risorse di partire.
Accetta e rifiuta portano la stessa classe. Un rifiuto che pesa meno dell'assenso è la violazione più sanzionata d'Europa — ed è contro la regola 3 di questo sistema: l'accent è del prodotto, e «accetta i cookie» non è il prodotto. Al posto di una risorsa bloccata compare un segnaposto che nomina il terzo, ed è quell'informazione a rendere il clic una scelta invece di un riflesso. Lo sblocco è a caldo: nessun ricaricamento, nessuna posizione di scorrimento persa.
Il record porta la versione dell'informativa (mai quella del framework, o pubblicare la 12.1 rifarebbe la domanda a tutti) e una scadenza, 180 giorni di default. Un consenso vecchio in formato 11.2 viene migrato a statistiche e basta: i media di terzi non erano nemmeno nominati nel testo di allora, e un consenso che nessuno ha mai dato non si inventa. E va detto senza giri: senza un registro lato server non hai una prova, hai uno stato.
Combobox, data, segmentato, tooltip
I pezzi che mancavano perché il sistema reggesse un modulo qualunque.
Sotto il combobox c'è un <input> vero: la tastiera del telefono, il riempimento automatico e l'invio del modulo funzionano — la maggior parte delle implementazioni in giro sono un <div role="combobox"> e li perdono tutti e tre. Il segmentato ha radio veri sotto: frecce, gruppo e lettura arrivano dal browser. Il tooltip data-cd-tip dalla 12.0 vive nel livello alto: dentro un contenitore con overflow:hidden non sparisce più, e ai bordi rientra.
Testo lungo e documento
Ogni sito ha una pagina che il sistema non copriva: il chi siamo, un articolo, le note legali. Quel testo esce da un CMS come tag nudi, e il framework aveva stili solo per le classi — quindi o si annotava ogni tag a mano, o la pagina più testuale del sito era l'unica senza tipografia.
Un titolo dentro la prosa
Misura di lettura a 66 caratteri, e il distacco sopra un titolo è maggiore di quello sotto: così il titolo appartiene al testo che introduce, non a quello che chiude.
È l'unico punto del sistema che stila i tag invece delle classi, ed è voluto.
CANDELA è scuro per costruzione: il principio è una sorgente di luce dietro il vetro, e vetro, bagliore e atmosfera discendono dal fondo scuro. Non c'è un tema chiaro e non è una mancanza — un CANDELA chiaro sarebbe un altro design system. Resta però il documento, e .cd-chiaro capovolge i token su un ramo: stessi nomi, altri valori, niente vetro (che senza una sorgente dietro sarebbe latte). Nessun componente sa di essere in chiaro e nessuno va riscritto.
Quello che il framework scrive da sé
Una barra di avanzamento senza ruolo è un <div> decorativo per axe: l'audit diceva «0 violazioni» mentre il componente era muto. È la stessa categoria del fuoco invisibile che la 10.10 aveva inseguito — un difetto che nessuno strumento segnala.
Il framework mette da sé: role="progressbar" coi valori su cd-progress — e nessun aria-valuenow quando la barra è indeterminata, perché dirlo è meglio che mentire con uno zero; role="status" sullo spinner; aria-describedby fra .cd-field e il suo .cd-hint; tabindex e role="region" sulle regioni scorrevoli; aria-current sull'ultima briciola; e l'ARIA completa dei tab.
Candela.loading() esiste perché l'istruzione «is-loading sempre insieme a disabled» era sbagliata: disabled toglie il fuoco al bottone appena premuto, e chi naviga da tastiera perde il posto nella pagina.
Un solo punto di chiusura
Modali, drawer, palette e lightbox non hanno più pulsanti propri: quando uno di essi si apre, la pillola si contrae nella × e chiude quello. L'utente impara una volta sola dove si chiude, e la × è sempre nello stesso punto dello schermo. Aprendo un overlay il menu si chiude da solo, così c'è sempre un solo livello e nessuna ambiguità.
Il nodo tecnico vale la pena di conoscerlo: le <dialog> modali vivono nel top layer del browser, che vince su qualunque z-index — una pillola lasciata nel body finirebbe sotto e sarebbe incliccabile. Perciò non viene sollevata: viene spostata dentro il dialog aperto e riportata a casa alla chiusura. Essendo position:fixed non si muove di un pixel, ma diventa visibile, cliccabile e inclusa nel focus trap nativo. Per lo stesso motivo le animazioni d'ingresso sono passate dal dialog al pannello interno: un transform sul dialog renderebbe la pillola relativa a lui.
Nelle pagine senza menu a pillola il framework crea da sé una × identica e la rimuove alla chiusura: la chiusura universale non dipende dalla presenza del menu.