Faux Grid Tracks

Un po’ di tempo fa, è stata postata questa domanda su css-discuss:

L’articolo prosegue sotto

È possibile assegnare uno stile alle righe e alle colonne di una griglia [CSS], ossia alla griglia stessa? Devo fare un layout che usa un tabellone tipo quello del gioco del tris, completo di linee verticali e orizzontali del tabellone del tris, con testo/icone in ogni cella della griglia.

Mi aspetto che questa domanda salti fuori più e più volte man mano che si comincia ad esplorare Grid layout. La risposta breve è: no, non è possibile farlo. Ma è possibile simulare tale effetto, che è quello che vorrei esplorare.

Definire la griglia#section1

Dal momento che stiamo parlando di layout del tris, abbiamo bisogno di un elemento contenitore attorno ai nove elementi. Potremmo usare una lista ordinata, o un paragrafo con una serie di <span>, o una <section> con dei <div>. Scegliamo quest’ultima.

<section id="ttt">
	<div>1</div>
	<div>2</div>
	<div>3</div>
	<div>4</div>
	<div>5</div>
	<div>6</div>
	<div>7</div>
	<div>8</div>
	<div>9</div>
</section>

Prendiamo quei nove <div> e mettiamoli in una griglia tre per tre, con ogni riga alta cinque em e ogni colonna larga cinque em. Impostare la struttura della griglia è piuttosto semplice:

#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}

Ecco fatto! Grazie all’algoritmo di auto-flow intrinseco di Grid Layout, questo è sufficiente per mettere nove elementi <div> in nove celle della griglia. Da qui, creare l’aspetto di una griglia è questione di impostare i bordi sugli elementi <div>. Ci sono molti modi per farlo, ma io ho scelto questo:

#ttt > * {
	border: 1px solid black;
	border-width: 0 1px 1px 0;
	display: flex; /* flex styling to center content in divs */
	align-items: center;
	justify-content: center;
}
#ttt > *:nth-of-type(3n)  {
	border-right-width: 0;
}
#ttt > *:nth-of-type(n+7) {
	border-bottom-width: 0;
}

Il risultato è mostrato nel layout di base qui sotto.

Screenshot: il layout di base ha una griglia 3x3 con le linee che spezzano la griglia come un tabellone del gioco del tris.

Figura 1: il layout di base

Questo approccio ha il vantaggio di non essere basato sui nomi delle class e compagnia bella. Tuttavia si sgretola se il grid flow viene cambiato per essere colonnare, come possiamo vedere nella Figura 2.

#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
	grid-auto-flow: column;  /* a change in layout! */
}
Screenshot: se la griglia passa al flow order colonnare, i bordi impazziscono. Invece di un tabellone del tris, i bordi orizzontali più a destra si spostano sul fondo della griglia e quelli verticali più in basso si spostano sul lato destro.

Figura 2: il layout di base con grid flow colonnare.

Se il flow è colonnare, allora le regole per l’applicazione dei bordi si ribaltano, così:

#ttt > *:nth-of-type(3n) {
	border-bottom-width: 0;
}
#ttt > *:nth-of-type(n+7) {
	border-right-width: 0;
}

Questo ci riporta al risultato che abbiamo visto in Figura 1, ma con il contenuto in ordine colonnare invece che di riga. Non c’è in Grid un row reverse o un column reverse come c’è in flexbox, quindi dobbiamo solo preoccuparci dei pattern di flusso di righe e colonne.

Ma cosa succede se un cambiamento successivo al design porta gli item della griglia ad essere risistemati in modi diversi? Per esempio, potrebbe esserci una ragione per prendere uno o due degli item e mostrarli per ultimi nella griglia, così:

#ttt > *:nth-of-type(4), #ttt > *:nth-of-type(6) {
	order: 66;
}

Proprio come in flexbox, questo sposterà i grid item mostrati fuori dall’ordine del sorgente, mettendoli dopo i grid item che non hanno valori espliciti di order. Se questo tipo di nuova disposizione è una possibilità, non c’è un modo semplice per attivare e disattivare i bordi per creare l’illusione di linee nella griglia interna. Cosa si fa?

Attacco dei filler <b>!#section2

Se vogliamo creare degli stili standalone che seguano le grid track, ossia, gli aspetti di presentazione che non sono direttamente collegati al contenuto verosimilmente risistemato, allora abbiamo bisogno di altri elementi da sistemare e a cui assegnare degli stili. Probabilmente, non avranno alcun contenuto, rendendoli una sorta di filler strutturali per riempire i gap delle capacità di Grid.

Perciò, all’elemento <section>, possiamo aggiungere due elementi <b> con identificatori.

<section id="ttt">
	<b id="h"></b>
	<b id="v"></b>
	<div>1</div>
	<div>2</div>
	<div>3</div>

Questi “filler <b>”, come mi piace definirli, potrebbero essere messi ovunque all’interno di <section>, ma va bene all’inizio. Ci atterremo a questo. Poi aggiungeremo questi stili alla nostra griglia originale del layout di base:

b[id] {
	border: 1px solid gray;
}
b#h {
	grid-column: 1 / -1;
	grid-row: 2;
	border-width: 1px 0;
}
b#v {
	grid-column: 2;
	grid-row: 1 / -1;
	border-width: 0 1px;
}

Il 1 / -1 significa “vai dalla prima grid line all’ultima grid line dell’explicit grid”, indipendentemente da quante grid line possano esserci. È un pattern comodo da usare in ogni situazione in cui si ha un grid item che dovrebbe allungarsi da lato a lato di una griglia.

Quindi, il <b> orizzontale ha i bordi top e bottom e il <b> verticale ha i bordi left e right. Questo crea le linee del tabellone, come mostrato nella Figura 3.

Screenshot: con i filler tag b, potete vedere di nuovo il tabellone del tris. Ma solo gli angoli della griglia sono riempiti di contenuto e ci sono 5 celle sotto il tabellone perché le grid line hanno spostato il contenuto.

Figura 3: il layout base con i “filler <b>

Aspetta un attimo: abbiamo di nuovo la griglia del tris, ma adesso i numeri sono nei posti sbagliati, il che significa che i <div> che li contengono sono fuori posto. E il motivo è questo: gli elementi <div> che contengono il contenuto effettivo non fluiranno più automaticamente nelle celle della griglia, perché i filler <b> stanno già occupando cinque delle nove celle. (Si tratta delle celle nella colonna e riga centrale della griglia). L’unico modo per riportare gli elementi <div> nelle loro celle della griglia prevista è di piazzarceli esplicitamente. Ecco un modo per farlo:

div:nth-of-type(3n+1) {
	grid-column: 1;
}
div:nth-of-type(3n+2) {
	grid-column: 2;
}
div:nth-of-type(3n+3) {
	grid-column: 3;
}
div:nth-of-type(-n+3) {
	grid-row: 1;
}
div {
	grid-row: 2;
}
div:nth-of-type(n+7) {
	grid-row: 3;
}

Questo funziona se sapete che il contenuto sarà sempre disposto in ordine “prima la riga poi la colonna”. Passare a “prima la colonna poi la riga” richiede la riscrittura del CSS. Se i contenuti devono essere piazzati in un ordine confuso, allora dovrete scrivere una regola per ogni <div>.

Questo probabilmente è sufficiente per la maggior parte dei casi, ma spingiamoci oltre. Supponiamo di voler disegnare quelle linee della griglia senza interferire con il flusso automatico dei contenuti. Come possiamo farlo?

Overgridding#section3

Sarebbe comodo se ci fosse una proprietà per marcare gli elementi come non partecipanti al grid flow, ma non c’è. Quindi, al contrario, suddivideremo i contenuti e i filler nelle proprie griglie e useremo una terza griglia per mettere una di quelle griglie sopra l’altra.

Perché ciò avvenga, è necessario un po’ di cambiamento strutturale, perché funzioni, i contenuti e i filler <b> devono avere griglie identiche. Pertanto ci ritroviamo con:

<section id="ttt">
	<div id="board">
		<b id="h"></b>
		<b id="v"></b>
	</div>
	<div id="content">
		<div>1</div>
		<div>2</div>
		<div>3</div>
		<div>4</div>
		<div>5</div>
		<div>6</div>
		<div>7</div>
		<div>8</div>
		<div>9</div>
	</div>
</section>

La prima cosa da fare è dare ai <div> del tabellone e del contenuto griglie identiche. La stessa griglia che abbiamo usato prima, in effetti. Cambiamo solo un pochino il selettore della regola #ttt, per selezionare invece i figli di #ttt:

#ttt > * {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}

Adesso che queste due griglie hanno lo stesso layout, dobbiamo piazzarne una sull’altra. Potremmo posizionare relativamente il container #ttt e posizionare in maniera assoluta i suoi figli, ma c’è un altro modo: usare Grid.

#ttt { /* new rule added */
	display: grid;
}
#ttt > * {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}

Ma aspettate: dove sono le righe e le colonne per #ttt? Dove siamo diretti, non servono le righe (o le colonne). Ecco in che modo due griglie finiscono con l’occupare la stessa area con una sopra l’altra:

#ttt {
	display: grid;
}
#ttt > * {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
	grid-column: 1;  /* explicit grid placement */
	grid-row: 1;  /* explicit grid placement */
}

Quindi, a #ttt viene assegnata una griglia con una cella e i suoi due figli sono esplicitamente sistemati in quella singola cella. Pertanto, uno sta sopra l’altro, come con il posizionamento, ma a differenza del posizionamento, la dimensione della griglia più esterna è dettata dal layout dei suoi figli. Si ridimensionerà per circondarli, anche se poi cambieremo le griglie interne perché siano più grandi (o più piccole). Possiamo vedere questo in pratica nella Figura 4, dove la griglia più esterna è delineata in viola nel Grid Inspector Tool di Firefox.

Screenshot: Nel Grid Inspector di Firefox, la griglia contenitrice si espande per l'intera larghezza della pagina con un bordo viola. Occupando circa un terzo dello spazio sul lato sinistro del container ci sono due griglie figlie, una con i numeri da 1 a 9 in una griglia 3 per 3 e l'altra con le linee del tris sovrapposte le une alle altre.

Figura 4: Il layout overgridded

E questo è quanto. Potremmo spingerci oltre, magari usando lo z-index per disporre il tabellone sopra al contenuto (di default, l’elemento che viene dopo nel codice viene visualizzato sopra all’elemento che viene prima), ma questo è sufficiente per il caso che abbiamo preso in considerazione.

Il vantaggio è che il <div> del contenuto, avendo solo da preoccuparsi dei suoi contenuti, può fare uso di grid-auto-flow e order per riarrangiare le cose. Come esempio, potete fare cose come la seguente e non vi serviranno tutti i posizionamenti :nth-of-type dei grid item del nostro precedente CSS. La Figura 5 mostra il risultato.

/* added to #snippet13 code */
#ttt > #content {
	grid-auto-flow: column;
}
#ttt > #content > :nth-child(5) {
	order: 2;
}
Screenshot: la versione overgridded, in cui la grid numerata 3 per 3 è sovrapposta sul tabellone del tris, continua a funzionare bene se riordinate le celle. In questo caso, il numero 5 si è spostato dalla cella della griglia centrale a quella in fondo a destra.

Figura 5: Spostare il #5 alla fine e lasciare che gli altri item rifluiscano in colonne.

Precisazioni#section4

Qui lo svantaggio, ed è piuttosto grosso, è che le griglie del tabellone e del contenuto sono solo minimamente coscienti l’una dell’altra. La ragione per cui l’esempio precedente funziona è che le grid track sono di dimensione fissa e nessuna parte del contenuto è in overflow. Supponiamo di volere che le colonne e le righe si ridimensionino in base al contenuto, così:

#content {
	grid-template-columns: repeat(3,min-content);
	grid-template-rows: repeat(3,min-content);
}

Questo si sgretolerà presto, con le linee del tabellone non corrispondenti al layout del contenuto attuale. Per niente.

In altre parole, questa tecnica della sovrapposizione sacrifica uno dei principali punti di forza di Grid: il modo in cui le celle della griglia si relazionano alle altre celle della griglia. Si tratta di un compromesso ragionevole nei casi in cui la dimensione del contenuto è prevedibile ma l’ordinamento non lo è. In altri casi, probabilmente non è una buona idea.

Ricordatevi che questo funziona davvero solo con i layout in cui dimensioni e disposizione sono sempre noti e in cui a volte dovete disporre le griglie una sull’altra. Se il vostro filler <b> viene in contatto con un grid item implicitamente posizionato nella stessa griglia che occupa, gli verrà impedito di allungarsi. (I grid item esplicitamente posizionati, ossia quelli in cui l’autore ha dichiaro i valori sia per grid-row sia per grid-column, non bloccano i filler <b>).

Perché è utile?#section5

Ho realizzato che pochi tra noi avranno bisogno di creare un layout che assomigli a quello del tabellone del tris, quindi potreste chiedervi perché dovremmo preoccuparcene. Potremmo non volere delle strutture a forma di hashtag (#), ma ci saranno dei casi in cui vorremo dare uno stile all’intera column track o evidenziare una riga.

Dal momento che (per ora) CSS non offre un modo per assegnare direttamente uno stile alle celle, aree o track della griglia, dobbiamo allungare gli elementi lungo le parti a cui vogliamo assegnare uno stile indipendentemente dagli elementi che contengono il contenuto. C’è una discussione riguardante l’aggiunta di questa capacità direttamente a CSS nel repository GitHub del Working Group, dove voi potete aggiungere i vostri pensieri e le vostre proposte.

Ma perché i <b>? Perché?#section6

Uso i <b> per le porzioni decorative del layout perché sono elementi puramente decorativi. Non c’è contenuto da enfatizzare molto o da rendere in grassetto e semanticamente un <b> non è né meglio né peggio di uno <span>. È solo un hook a cui agganciare degli effetti visivi. Ed è più corto, quindi minimizza il page bloat (alcuni caratteri in più non farebbero tutta sta differenza).

Ancora più in tema, la completa mancanza di significato semantico di <b> lo evidenzia subito nel markup come intenzionalmente non semantico. È, in questo senso meta, auto-documentante.

È tutto quello che c’è?#section7

C’è un altro modo per ottenere questo preciso effetto: i background e i grid gap. Ha anch’esso i suoi svantaggi, ma vediamo prima come funziona. Per prima cosa, impostiamo uno sfondo nero al grid container e degli sfondi bianchi a ogni item nella griglia. Poi, usando grid-gap: 1px, facciamo apparire lo sfondo nero del container tra i grid item.

<section id="ttt">
	<div>1</div>
	<div>2</div>
	<div>3</div>
	<div>4</div>
	<div>5</div>
	<div>6</div>
	<div>7</div>
	<div>8</div>
	<div>9</div>
</section>
#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
	background: black;
	grid-gap: 1px;
}
#ttt > div {
	background: white;
}

Semplice e non c’è bisogno di alcun filler <b>. Cosa non va?

Il primo problema è che se mai rimuoveste un item, ci sarebbe un grosso blocco nero nel layout. Magari va bene, ma è più probabile che non vada bene. Il secondo problema è che i grid container, di default, non applicano “shrink-wrap” ai loro item. Al contrario, riempiranno l’elemento padre, come fanno i box block. Entrambe questi problemi sono illustrati nella Figura 6.

Screenshot: quando manca una cella della griglia con la soluzione background e grid-gap, rimane un grosso box nero al suo posto. C'è anche un box nero gigante che riepie il resto dello spazio alla destra delle celle della griglia.

Figura 6: alcuni possibili problemi di background.

Potete usare del CSS extra per restringere la larghezza del grid container ma non si può davvero evitare il background che appare dove manca un item.

D’altro canto, questi problemi potrebbero trasformarsi in vantaggi se, invece di un background nero, volessimo mostrare un’immagine di background che faccia “forare” lo spazio ai grid item, come ha fatto Jen Simmons nella sua demo “Jazz At Lincoln Center Poster”.

Un terzo problema nell’uso dei background è quando volete semplicemente delle grid line solide rispetto a un background di pagina variegato e volete che quel background di mostri attraverso i grid item. In quel caso, i grid item (in questo caso, i <div>) devono avere i background trasparenti, il che impedisce l’uso di grid-gap per rivelare un colore.

Se i <b> vi fanno davvero andare in fumo il cervelletto, potete usare al loro posto il contenuto generato. Quando generate gli pseudo-elementi before- e after-content, Grid li tratta come elementi effettivi e li fa diventare dei grid item. Quindi, usando il semplice markup usato nell’esempio precedente, potremmo scrivere questo CSS invece:

#ttt {
	display: grid;
	grid-template-columns: repeat(3,5em);
	grid-template-rows: repeat(3,5em);
}
#ttt::before {
	grid-column: 1 / -1;
	grid-row: 2;
	border-width: 1px 0;
}
#ttt::after {
	grid-column: 2;
	grid-row: 1 / -1;
	border-width: 0 1px;
}

È identico a quello con i filler <b>, tranne che qui gli elementi generati disegnano le grid line.

Questo approccio funziona bene per ogni griglia 3×3 come quella con cui ho giocato, ma andando oltre, si va sul complicato. Supponiamo di avere una griglia 5×4 invece di una 3×3. Usando i gradienti e le ripetizioni, possiamo disegnare quante linee desideriamo, al costo di rendere più complicato il CSS.

#ttt {
	display: grid;
	grid-template-columns: repeat(5,5em);
	grid-template-rows: repeat(4,5em);
}
#ttt::before {
	content: "";
	grid-column: 1 / -1;
	grid-row: 1 / -2;
	background:
		linear-gradient(to bottom,transparent 4.95em, 4.95em, black 5em)
		top left / 5em 5em;
}
#ttt::after {
	content: "";
	grid-column: 1 / -2;
	grid-row: 1 / -1;
	background:
		linear-gradient(to right,transparent 4.95em, 4.95em, black 5em)
		top left / 5em 5em;
}

Questo funziona piuttosto bene, come mostrato in Figura 7, supponendo che facciate l’esercizio di assegnare le grid cell in maniera simile a come abbiamo fatto nella #snippet9.

Screenshot: una griglia 5 per 4 con i bordi distribuiti uniformemente a dividere le celle internamente usando i gradienti di background.

Figura 7: elementi generati e gradienti di background

Questo approccio usa i gradienti linear per costruire immagini quasi interamente trasparenti che hanno solo 1/20mo di un em di nero e poi ripetono quelli o verso destra o verso il basso. Il gradiente verso il basso (che crea linee orizzontali) viene fermato una gridline prima della fine del container, dal momento che altrimenti ci sarebbe una linea orizzontale sotto l’ultima riga di item. In maniera simile, il gradiente verso destra (che crea le linee verticali) si ferma una colonna prima del bordo a destra. Questo è il motivo per cui ci sono i valori -2 per grid-column e grid-row.

Uno svantaggio di questo è lo stesso dell’approccio dei filler <b>: dal momento che gli elementi generati coprono la maggior parte del background, tutti gli item devono essere assegnati esplicitamente alle loro celle della griglia invece di lasciare che fluiscano automaticamente. L’unico modo per aggirare ciò è di usare qualcosa come la tecnica overgridding esplorata in precedenza. Potreste anche riuscire a eliminare gli elementi generati se fate l’overgridding, a seconda della specifica situazione.

Un altro svantaggio è che se mai dovesse cambiare il font size, la larghezza delle linee potrebbe cambiare. Mi aspetto che ci sia un modo per aggirare questo problema usando calc(), ma lascerò alle vostre brillanti menti trovarlo e condividerlo con il mondo.

La parte divertente per me è che se effettivamente usate questo approccio basato sul gradiente, state riempiendo di immagini il background del container, mettendoci sopra degli item… Esattamente come facevamo con le Faux Columns.

Conclusioni#section8

È divertente come alcuni concetti si ripetano negli anni. Più di un decennio fa, Dan Cederholm ci ha mostrato come creare l’effetto di colonne a tutta altezza con le immagini di background. Ora io vi mostro come simulare box con colonne e righe a tutta altezza con degli elementi vuoti e, ove necessario, con le immagini di background.

Col tempo, il trucco che sta dietro le Faux Columns è caduto in disgrazia e il web design ha lasciato perdere quel tipo di effetto visuale. Forse lo stesso destino attende le Faux Grid Tracks, ma spero di vedere il sorgere di nuove capacità CSS che permettano questo tipo di effetto senza il bisogno di questi trucchi.

Abbiamo superato così tanti dei nostri vecchi trucchi. Eccone un altro da usare finché ce n’è bisogno e che spero un giorno ci potremo lasciare alle spalle.

Nessun commento

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Altro da ALA