Creazione di un componente Impostazioni

Una panoramica di base su come creare un componente di impostazioni di cursori e caselle di controllo.

In questo post voglio condividere il mio pensiero sulla creazione di un componente Impostazioni per il web che sia reattivo, supporti più input da dispositivo e funzioni su tutti i browser. Prova la demo.

Demo

Se preferisci i video o vuoi un'anteprima dell'interfaccia utente e dell'esperienza utente di ciò che stiamo creando, ecco una spiegazione più breve su YouTube:

Panoramica

Ho suddiviso gli aspetti di questo componente nelle seguenti sezioni:

  1. Layout
  2. Colore
  3. Inserimento intervallo personalizzato
  4. Input casella di controllo personalizzata
  5. Considerazioni sull'accessibilità
  6. JavaScript

Layout

Questa è la prima demo della GUI Challenge che utilizza solo la griglia CSS. Ecco ogni griglia evidenziata con Chrome DevTools per la griglia:

Contorni colorati e overlay di spaziatura che aiutano a mostrare tutte le caselle che compongono il layout delle impostazioni

Solo per gap

Il layout più comune:

foo {
  display: grid;
  gap: var(--something);
}

Chiamo questo layout "solo per lo spazio" perché utilizza la griglia solo per aggiungere spazi tra i blocchi.

Cinque layout utilizzano questa strategia. Ecco tutti i layout visualizzati:

Layout a griglia verticali evidenziati con contorni e spazi riempiti

L'elemento fieldset, che contiene ogni gruppo di input (.fieldset-item), utilizza gap: 1px per creare i bordi sottili tra gli elementi. Nessuna soluzione complicata per il bordo.

Lacuna colmata
.grid {
  display: grid;
  gap: 1px;
  background: var(--bg-surface-1);

  & > .fieldset-item {
    background: var(--bg-surface-2);
  }
}
Trucco del bordo
.grid {
  display: grid;

  & > .fieldset-item {
    background: var(--bg-surface-2);

    &:not(:last-child) {
      border-bottom: 1px solid var(--bg-surface-1);
    }
  }
}

Ritorno a capo naturale della griglia

Il layout più complesso è risultato essere il macro layout, il sistema di layout logico tra <main> e <form>.

Centrare i contenuti di wrapping

Flexbox e griglia offrono entrambi la possibilità di align-items o align-content e, quando si tratta di elementi di wrapping, gli allineamenti del layout content distribuiscono lo spazio tra gli elementi secondari come gruppo.

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
}

L'elemento principale utilizza la notazione abbreviata per l'allineamento place-content: center, in modo che gli elementi secondari siano centrati verticalmente e orizzontalmente nei layout a una e due colonne.

Guarda nel video sopra come i "contenuti" rimangono centrati, anche se è stato eseguito il wrapping.

Repeat auto-fit minmax

Il <form> utilizza un layout a griglia adattivo per ogni sezione. Questo layout passa da una a due colonne in base allo spazio disponibile.

form {
  display: grid;
  gap: var(--space-xl) var(--space-xxl);
  grid-template-columns: repeat(auto-fit, minmax(min(10ch, 100%), 35ch));
  align-items: flex-start;
  max-width: 89vw;
}

Questa griglia ha un valore diverso per row-gap (--space-xl) rispetto a column-gap (--space-xxl) per dare un tocco personalizzato al layout responsivo. Quando le colonne sono impilate, vogliamo uno spazio ampio, ma non così ampio come se fossimo su uno schermo largo.

La proprietà grid-template-columns utilizza tre funzioni CSS: repeat(), minmax() e min(). Una Kravets ha scritto un ottimo post del blog sul layout, definendolo RAM.

Se lo confronti con quello di Una, il nostro layout presenta tre aggiunte speciali:

  • Passiamo una funzione min() aggiuntiva.
  • Specifichiamo align-items: flex-start.
  • Esiste uno stile max-width: 89vw.

La funzione extra min() è descritta molto bene da Evan Minto nel suo blog nel post Intrinsically Responsive CSS Grid with minmax() and min(). Ti consiglio di leggerlo. La correzione dell'allineamento flex-start serve a rimuovere l'effetto di allungamento predefinito, in modo che gli elementi secondari di questo layout non debbano avere altezze uguali, ma possano avere altezze naturali e intrinseche. Il video di YouTube fornisce una rapida panoramica di questa aggiunta dell'allineamento.

max-width: 89vw merita una piccola analisi in questo post. Ti mostro il layout con e senza lo stile applicato:

Che cosa succede? Quando viene specificato max-width, fornisce contesto, dimensionamento esplicito o dimensionamento definito per l'auto-fit algoritmo di layout per sapere quante ripetizioni può inserire nello spazio. Sebbene sembri ovvio che lo spazio sia "a larghezza intera", secondo la specifica della griglia CSS, è necessario fornire una dimensione o una dimensione massima definita. Ho fornito una dimensione massima.

Quindi, perché 89vw? Perché "funzionava" per il mio layout. Io e un paio di altri colleghi di Chrome stiamo esaminando il motivo per cui un valore più ragionevole, come 100vw, non è sufficiente e se si tratta effettivamente di un bug.

Spaziatura

La maggior parte dell'armonia di questo layout deriva da una tavolozza limitata di spaziature, 7 per l'esattezza.

:root {
  --space-xxs: .25rem;
  --space-xs:  .5rem;
  --space-sm:  1rem;
  --space-md:  1.5rem;
  --space-lg:  2rem;
  --space-xl:  3rem;
  --space-xxl: 6rem;
}

L'utilizzo di questi flussi si integra perfettamente con la griglia, CSS @nest e la sintassi di livello 5 di @media. Ecco un esempio, il set di stili del layout <main> completo.

main {
  display: grid;
  gap: var(--space-xl);
  place-content: center;
  padding: var(--space-sm);

  @media (width >= 540px) {
    & {
      padding: var(--space-lg);
    }
  }

  @media (width >= 800px) {
    & {
      padding: var(--space-xl);
    }
  }
}

Una griglia con contenuti centrati, moderatamente imbottita per impostazione predefinita (come sui dispositivi mobili). Man mano che diventa disponibile più spazio nel viewport, si espande aumentando il padding. Il CSS del 2021 sembra promettere bene.

Ricordi il layout precedente, "solo per il vuoto"? Ecco una versione più completa di come appaiono in questo componente:

header {
  display: grid;
  gap: var(--space-xxs);
}

section {
  display: grid;
  gap: var(--space-md);
}

Colore

L'uso controllato del colore ha contribuito a far risaltare questo design come espressivo ma minimalista. Io faccio così:

:root {
  --surface1: lch(10 0 0);
  --surface2: lch(15 0 0);
  --surface3: lch(20 0 0);
  --surface4: lch(25 0 0);

  --text1: lch(95 0 0);
  --text2: lch(75 0 0);
}

Assegno ai colori di superficie e testo dei numeri anziché nomi come surface-dark e surface-darker perché in una media query li invertirò e chiaro e scuro non avranno significato.

Li inverto in una media query delle preferenze come questa:

:root {
  ...

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --surface2: lch(100 0 0);
      --surface3: lch(98 0 0);
      --surface4: lch(85 0 0);

      --text1: lch(20 0 0);
      --text2: lch(40 0 0);
    }
  }
}

È importante avere una visione generale e una strategia prima di approfondire i dettagli della sintassi dei colori. Ma, dato che ho anticipato un po' i tempi, facciamo un passo indietro.

LCH?

Senza entrare troppo nel dettaglio della teoria del colore, LCH è una sintassi orientata all'uomo, che si adatta al modo in cui percepiamo il colore, non al modo in cui lo misuriamo con la matematica (come 255). Questo gli conferisce un vantaggio distinto, in quanto gli esseri umani possono scriverlo più facilmente e altri esseri umani saranno in sintonia con questi aggiustamenti.

Screenshot della pagina web pod.link/csspodcast, con l&#39;episodio Color 2: Perception aperto
Scopri di più sul colore percettivo (e altro ancora) nel podcast CSS

Per oggi, in questa demo, concentriamoci sulla sintassi e sui valori che sto modificando per creare la modalità Chiaro e Buio. Esaminiamo una superficie e un colore del testo:

:root {
  --surface1: lch(10 0 0);
  --text1:    lch(95 0 0);

  @media (prefers-color-scheme: light) {
    & {
      --surface1: lch(90 0 0);
      --text1:    lch(40 0 0);
    }
  }
}

--surface1: lch(10 0 0) si traduce in 10% luminosità, 0 croma e 0 tonalità: un grigio molto scuro e incolore. Poi, nella media query per la modalità Luce, la luminosità viene invertita in 90% con --surface1: lch(90 0 0);. Questa è l'essenza della strategia. Inizia modificando solo la luminosità tra i due temi, mantenendo i rapporti di contrasto richiesti dal design o ciò che può mantenere l'accessibilità.

Il vantaggio di lch() è che la luminosità è orientata all'uomo e possiamo sentirci bene con una variazione di %, che sarà percettivamente e costantemente % diversa. hsl(), ad esempio, non è affidabile.

Se ti interessa, puoi approfondire la tua conoscenza degli spazi colore e di lch(). Sta arrivando!

Al momento, i CSS non possono accedere a questi colori. Ripeto: non abbiamo accesso a un terzo dei colori nella maggior parte dei monitor moderni. E non si tratta di colori qualsiasi, ma dei colori più vivaci che lo schermo può visualizzare. I nostri siti web sono sbiaditi perché l'hardware del monitor si è evoluto più rapidamente delle specifiche CSS e delle implementazioni del browser.

Lea Verou

Controlli dei moduli adattivi con schema di colori

Molti browser, attualmente Safari e Chromium, includono controlli per il tema scuro, ma devi specificare in CSS o HTML che il tuo design li utilizza.

Quanto sopra mostra l'effetto della proprietà dal riquadro Stili di DevTools. La demo utilizza il tag HTML, che a mio parere è generalmente una posizione migliore:

<meta name="color-scheme" content="dark light">

Scopri di più in questo color-scheme articolo di Thomas Steiner. C'è molto di più da guadagnare rispetto ai semplici input delle caselle di controllo scure.

CSS accent-color

Di recente è stata registrata attivitàrelativa a accent-color sugli elementi del modulo, uno stile CSS singolo che può modificare il colore della tinta utilizzato nell'elemento di input dei browser. Scopri di più qui su GitHub. L'ho incluso negli stili di questo componente. Man mano che i browser lo supportano, le mie caselle di controllo saranno più in linea con i colori rosa e viola.

input[type="checkbox"] {
  accent-color: var(--brand);
}

Uno screenshot di Chromium su Linux con caselle di controllo rosa

Esplosioni di colori con gradienti fissi e messa a fuoco interna

Il colore risalta di più quando viene utilizzato con parsimonia e uno dei modi in cui mi piace ottenerlo è attraverso interazioni colorate dell'interfaccia utente.

Nel video precedente sono presenti molti livelli di feedback e interazione dell'interfaccia utente, che contribuiscono a dare personalità all'interazione:

  • Evidenziare il contesto.
  • Fornire un feedback dell'interfaccia utente su quanto è pieno il valore nell'intervallo.
  • Fornire un feedback della UI che indica che un campo accetta input.

Per fornire un feedback quando viene eseguita un'interazione con un elemento, CSS utilizza la pseudo classe :focus-within per modificare l'aspetto di vari elementi. Analizziamo .fieldset-item, è molto interessante:

.fieldset-item {
  ...

  &:focus-within {
    background: var(--surface2);

    & svg {
      fill: white;
    }

    & picture {
      clip-path: circle(50%);
      background: var(--brand-bg-gradient) fixed;
    }
  }
}

Quando uno degli elementi secondari di questo elemento ha focus-within:

  1. Allo sfondo .fieldset-item viene assegnato un colore di superficie con un contrasto più elevato.
  2. Il svg nidificato è riempito di bianco per un contrasto più elevato.
  3. Il <picture> clip-path nidificato si espande fino a formare un cerchio completo e lo sfondo viene riempito con la sfumatura fissa luminosa.

Intervallo personalizzato

Dato il seguente elemento di input HTML, ti mostrerò come ho personalizzato il suo aspetto:

<input type="range">

Questo elemento è composto da tre parti che dobbiamo personalizzare:

  1. Elemento / contenitore intervallo
  2. Track
  3. Pollice

Stili degli elementi dell'intervallo

input[type="range"] {
  /* style setting variables */
  --track-height: .5ex;
  --track-fill: 0%;
  --thumb-size: 3ex;
  --thumb-offset: -1.25ex;
  --thumb-highlight-size: 0px;

  appearance: none;         /* clear styles, make way for mine */
  display: block;
  inline-size: 100%;        /* fill container */
  margin: 1ex 0;            /* ensure thumb isn't colliding with sibling content */
  background: transparent;  /* bg is in the track */
  outline-offset: 5px;      /* focus styles have space */
}

Le prime righe di CSS sono le parti personalizzate degli stili e spero che l'etichettatura chiara sia utile. Il resto degli stili sono principalmente stili di reset, per fornire una base coerente per la creazione delle parti più complesse del componente.

Stili delle tracce

input[type="range"]::-webkit-slider-runnable-track {
  appearance: none; /* clear styles, make way for mine */
  block-size: var(--track-height);
  border-radius: 5ex;
  background:
    /* hard stop gradient:
        - half transparent (where colorful fill we be)
        - half dark track fill
        - 1st background image is on top
    */
    linear-gradient(
      to right,
      transparent var(--track-fill),
      var(--surface1) 0%
    ),
    /* colorful fill effect, behind track surface fill */
    var(--brand-bg-gradient) fixed;
}

Il trucco è "rivelare" il colore di riempimento vibrante. Questo viene fatto con il gradiente di interruzione netta nella parte superiore. La sfumatura è trasparente fino alla percentuale di riempimento e dopo utilizza il colore della superficie della traccia non riempita. Dietro questa superficie non riempita, c'è un colore a tutta larghezza, in attesa che la trasparenza lo riveli.

Stile di riempimento della traccia

Il mio design richiede JavaScript per mantenere lo stile di riempimento. Esistono strategie solo CSS, ma richiedono che l'elemento del cursore abbia la stessa altezza della traccia e non sono riuscito a trovare un'armonia entro questi limiti.

/* grab sliders on page */
const sliders = document.querySelectorAll('input[type="range"]')

/* take a slider element, return a percentage string for use in CSS */
const rangeToPercent = slider => {
  const max = slider.getAttribute('max') || 10;
  const percent = slider.value / max * 100;

  return `${parseInt(percent)}%`;
};

/* on page load, set the fill amount */
sliders.forEach(slider => {
  slider.style.setProperty('--track-fill', rangeToPercent(slider));

  /* when a slider changes, update the fill prop */
  slider.addEventListener('input', e => {
    e.target.style.setProperty('--track-fill', rangeToPercent(e.target));
  })
})

Penso che questo sia un bel miglioramento visivo. Il cursore funziona perfettamente senza JavaScript, la proprietà --track-fill non è obbligatoria, semplicemente non avrà uno stile di riempimento se non è presente. Se JavaScript è disponibile, compila la proprietà personalizzata osservando anche le modifiche apportate dall'utente e sincronizzando la proprietà personalizzata con il valore.

Ecco un ottimo articolo su CSS-Tricks di Ana Tudor, che mostra una soluzione solo CSS per il riempimento della traccia. Ho trovato molto stimolante anche questo range elemento.

Stili delle miniature

input[type="range"]::-webkit-slider-thumb {
  appearance: none; /* clear styles, make way for mine */
  cursor: ew-resize; /* cursor style to support drag direction */
  border: 3px solid var(--surface3);
  block-size: var(--thumb-size);
  inline-size: var(--thumb-size);
  margin-top: var(--thumb-offset);
  border-radius: 50%;
  background: var(--brand-bg-gradient) fixed;
}

La maggior parte di questi stili serve a creare un bel cerchio. Anche in questo caso, la sfumatura di sfondo fissa unifica i colori dinamici delle miniature, delle tracce e degli elementi SVG associati. Ho separato gli stili per l'interazione per isolare la tecnica box-shadow utilizzata per l'evidenziazione al passaggio del mouse:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

::-webkit-slider-thumb {
  

  /* shadow spread is initally 0 */
  box-shadow: 0 0 0 var(--thumb-highlight-size) var(--thumb-highlight-color);

  /* if motion is OK, transition the box-shadow change */
  @media (--motionOK) {
    & {
      transition: box-shadow .1s ease;
    }
  }

  /* on hover/active state of parent, increase size prop */
  @nest input[type="range"]:is(:hover,:active) & {
    --thumb-highlight-size: 10px;
  }
}

L'obiettivo era creare un'evidenziazione visiva animata e facile da gestire per il feedback degli utenti. Utilizzando un'ombra della casella posso evitare di attivare il layout con l'effetto. Lo faccio creando un'ombra non sfocata che corrisponda alla forma circolare dell'elemento del pollice. Poi modifico e faccio la transizione della dimensione della diffusione al passaggio del mouse.

Se solo l'effetto di evidenziazione fosse così semplice sulle caselle di controllo…

Selettori cross-browser

Ho scoperto di aver bisogno di questi selettori -webkit- e -moz- per ottenere la coerenza tra i browser:

input[type="range"] {
  &::-webkit-slider-runnable-track {}
  &::-moz-range-track {}
  &::-webkit-slider-thumb {}
  &::-moz-range-thumb {}
}

Casella di controllo personalizzata

Dato il seguente elemento di input HTML, ti mostrerò come ho personalizzato il suo aspetto:

<input type="checkbox">

Questo elemento è composto da tre parti che dobbiamo personalizzare:

  1. Elemento casella di controllo
  2. Etichette associate
  3. Effetto di evidenziazione

Elemento Casella di controllo

input[type="checkbox"] {
  inline-size: var(--space-sm);   /* increase width */
  block-size: var(--space-sm);    /* increase height */
  outline-offset: 5px;            /* focus style enhancement */
  accent-color: var(--brand);     /* tint the input */
  position: relative;             /* prepare for an absolute pseudo element */
  transform-style: preserve-3d;   /* create a 3d z-space stacking context */
  margin: 0;
  cursor: pointer;
}

Gli stili transform-style e position preparano lo pseudo-elemento che introdurremo in un secondo momento per definire lo stile dell'evidenziazione. Altrimenti, si tratta principalmente di piccole modifiche stilistiche da parte mia. Mi piace che il cursore sia un puntatore, mi piacciono gli offset del contorno, le caselle di controllo predefinite sono troppo piccole e se accent-color è supportato, porta queste caselle di controllo nello schema di colori del brand.

Etichette delle caselle di controllo

È importante fornire etichette per le caselle di controllo per due motivi. Il primo è rappresentare a cosa serve il valore della casella di controllo, per rispondere alla domanda "Per cosa è attivo o disattivato?" Il secondo riguarda l'esperienza utente: gli utenti web si sono abituati a interagire con le caselle di controllo tramite le etichette associate.

immissione
<input
  type="checkbox"
  id="text-notifications"
  name="text-notifications"
>
etichetta
<label for="text-notifications">
  <h3>Text Messages</h3>
  <small>Get notified about all text messages sent to your device</small>
</label>

Nell'etichetta, inserisci un attributo for che rimanda a una casella di controllo tramite ID: <label for="text-notifications">. Nella casella di controllo, raddoppia sia il nome che l'ID per assicurarti che venga trovato con strumenti e tecnologie diversi, come un mouse o uno screen reader: <input type="checkbox" id="text-notifications" name="text-notifications">. :hover, :active e altro ancora sono disponibili senza costi con la connessione, aumentando i modi in cui è possibile interagire con il modulo.

Evidenziazione casella di controllo

Voglio mantenere le mie interfacce coerenti e l'elemento cursore ha una bella evidenziazione delle miniature che vorrei utilizzare con la casella di controllo. La miniatura è stata in grado di utilizzare box-shadow e la relativa proprietà spread per aumentare e diminuire la scala di un'ombra. Tuttavia, questo effetto non funziona qui perché le nostre caselle di controllo sono quadrate, e dovrebbero esserlo.

Sono riuscito a ottenere lo stesso effetto visivo con uno pseudo elemento e una quantità purtroppo elevata di CSS complessi:

@custom-media --motionOK (prefers-reduced-motion: no-preference);

input[type="checkbox"]::before {
  --thumb-scale: .01;                        /* initial scale of highlight */
  --thumb-highlight-size: var(--space-xl);

  content: "";
  inline-size: var(--thumb-highlight-size);
  block-size: var(--thumb-highlight-size);
  clip-path: circle(50%);                     /* circle shape */
  position: absolute;                         /* this is why position relative on parent */
  top: 50%;                                   /* pop and plop technique (https://web.dev/centering-in-css#5-pop-and-plop) */
  left: 50%;
  background: var(--thumb-highlight-color);
  transform-origin: center center;            /* goal is a centered scaling circle */
  transform:                                  /* order here matters!! */
    translateX(-50%)                          /* counter balances left: 50% */
    translateY(-50%)                          /* counter balances top: 50% */
    translateZ(-1px)                          /* PUTS IT BEHIND THE CHECKBOX */
    scale(var(--thumb-scale))                 /* value we toggle for animation */
  ;
  will-change: transform;

  @media (--motionOK) {                       /* transition only if motion is OK */
    & {
      transition: transform .2s ease;
    }
  }
}

/* on hover, set scale custom property to "in" state */
input[type="checkbox"]:hover::before {
  --thumb-scale: 1;
}

Creare uno pseudo-elemento circolare è un'operazione semplice, ma posizionarlo dietro l'elemento a cui è collegato è stato più difficile. Ecco la situazione prima e dopo la correzione:

È sicuramente una microinterazione, ma per me è importante mantenere la coerenza visiva. La tecnica di scalabilità dell'animazione è la stessa che abbiamo utilizzato in altri contesti. Impostiamo una proprietà personalizzata su un nuovo valore e lasciamo che CSS la transizioni in base alle preferenze di movimento. La funzionalità principale è translateZ(-1px). Il genitore ha creato uno spazio 3D e questo pseudo-elemento secondario lo ha sfruttato posizionandosi leggermente indietro nello spazio Z.

Accessibilità

Il video di YouTube mostra in modo chiaro le interazioni con mouse, tastiera e screen reader per questo componente delle impostazioni. Ti fornirò alcuni dettagli.

Scelte per l'elemento HTML

<form>
<header>
<fieldset>
<picture>
<label>
<input>

Ciascuno di questi suggerimenti e consigli si riferisce allo strumento di navigazione dell'utente. Alcuni elementi forniscono suggerimenti per l'interazione, altri collegano l'interattività e altri ancora contribuiscono a modellare l'albero dell'accessibilità in cui si sposta un lettore di schermo.

Attributi HTML

Possiamo nascondere gli elementi non necessari per gli screen reader, in questo caso l'icona accanto al cursore:

<picture aria-hidden="true">

Il video precedente mostra il flusso dello screen reader su Mac OS. Nota come la selezione dell'input si sposta direttamente da un cursore all'altro. Questo perché abbiamo nascosto l'icona che avrebbe potuto essere una tappa intermedia per raggiungere il cursore successivo. Senza questo attributo, un utente dovrebbe fermarsi, ascoltare e superare l'immagine che potrebbe non essere in grado di vedere.

L'SVG è un insieme di calcoli matematici. Aggiungiamo un elemento <title> per un titolo senza costi al passaggio del mouse e un commento leggibile che descriva cosa creano i calcoli:

<svg viewBox="0 0 24 24">
  <title>A note icon</title>
  <path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/>
</svg>

A parte questo, abbiamo utilizzato un HTML contrassegnato in modo chiaro, in modo che i test del modulo funzionino molto bene con mouse, tastiera, controller per videogiochi e screen reader.

JavaScript

Ho già spiegato come viene gestito il colore di riempimento della traccia da JavaScript, quindi diamo un'occhiata al codice JavaScript correlato a <form>:

const form = document.querySelector('form');

form.addEventListener('input', event => {
  const formData = Object.fromEntries(new FormData(form));
  console.table(formData);
})

Ogni volta che il modulo viene modificato, la console registra il modulo come un oggetto in una tabella per una facile revisione prima dell'invio a un server.

Uno screenshot dei risultati di console.table(), in cui i dati del modulo vengono mostrati in una tabella

Conclusione

Ora che sai come ho fatto, come faresti tu? In questo modo si ottiene un'architettura dei componenti divertente. Chi realizzerà la prima versione con gli slot nel suo framework preferito? 🙂

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione Remix della community qui sotto.

Remix della community

  • @tomayac per il suo stile in merito all'area al passaggio del mouse per le etichette delle caselle di controllo. Questa versione non presenta spazi al passaggio del mouse tra gli elementi: demo e source.