Com desenvolupar una UI amb CSS Grid

Maquetar interfícies el 2018 serà bastant entretingut.

Adrià Fontcuberta
Calidae Blog

--

Imatge de capçalera preventiva. Sí, es pot fer servir CSS Grid. No, no passa res.

CSS Grid és complex. No és una opinió, és una realitat: són 18 propietats completament noves, i alguns conceptes que no hem sentit mai abans en CSS.

Però val la pena aprendre’l, perquè és La Manera Correcta™.

Hi ha mil posts parlant de CSS Grid. Per què he de llegir el teu?

Perquè serà pràctic. I no cobriré el 100% de la API de CSS Grid, perquè no cal. En canvi, em limitaré a les parts més útils, amb les qual podrem desenvolupar el 80% de les UI.

Dit d’una altra manera: intentarem ser eficients. I tot i així serà llarg.

Som-hi! Què construirem?

Construirem la típica UI d’una app de gestió d’alguna cosa, que serà prou adequada per ensenyar la potència de CSS Grid. En concret, aquesta (agafada de Sketch App Resources):

Xula, eh? Doncs és obra d’un tal Minh Nguyen.

Recomanació: t’anirà més bé treballar amb Firefox Quantum, perquè les DevTools s’adapten millor a CSS Grid: mostra noms, layouts, etcètera. Les de Chrome no estan malament, tampoc.

Intro súpercurta a CSS Grid: adéu als hacks per fer layouts

Taules, floats i clearfix… ñapas vàries que hem necessitat durant anys per poder convertir aquells dissenys tan preciosos fets en Photoshop (i ara Sketch) en realitat. Tot això va canviar en bona part amb l’arribada de Flexbox — però CSS Grid és encara més net, perquè està pensat precisament per dissenyar l’estructura de la interfície. La Jen Simmons ho explica bastant millor que jo.

Terminologia bàsica

Abans d’obrir el Codepen i començar a escriure CSS com si no hi hagués demà cal que ens posem d’acord en com dir les coses.

Saps que fa un parell de paràgrafs t’he dit que no explicaríem l’API sencera? Doncs és veritat, però una mica sí, que l’haurem d’explicar. Coses bàsiques que cal entendre, i que estan molt ben explicades pels internets. De fet, els robaré les imatges i tot.

  • Grid: és un conjunt de línies horitzontals i verticals que defineixen el posicionament de la resta d’elements en un layout. N’hi ha de mil tipus, i CSS Grid simplement ofereix un conjunt d’eines adequades per fer aquesta feina.

Qualsevol grid té alguns elements bàsics:

  • Grid line: cada una de les línies que delimiten espais en el grid.
  • Grid item (o grid cell): cada un dels espais que queda delimitat per 4 grid lines.
  • Grid area: conjunt rectangular de grid items. “Rectangular” vol dir que els grid items han de ser contigus i que no poden fer formes rares (ni L, ni T, ni històries. Només rectangles de 1x1, 1x2, 2x2, 2x6…).

Com faig un Grid? Com faig files? Com faig columnes?

Sí, sí, sí. Quantes preguntes, amic. Mira, fer un grid no té gaire secret:

div {
display: grid;
}

Au, aquest div ja és un grid. Un grid inútil.

Inútil perquè no té ni files ni columnes. Necessitem dir-li quantes files i columnes té, i ja que hi som, la mida de cada una. Alehop:

div {
display: grid;
grid-template-columns: 100px 100px 200px;
grid-template-rows: 40px 300px;

}

I ja està. Acabem de definir un grid amb 3 columnes (2 de 100px i una de 200) i amb dues files (de 40 i 300px respectivament). Aquí el tenim:

El nostre grid inspeccionat amb les Firefox DevTools. Codepen.

Ara sí, a picar codi

Va, que ja saps la majoria de coses que has de saber per currar-te una UI fineta fineta. Comencem a desenvolupar-la, no?

El primer que necessitem és l’HTML de la nostra aplicació:

<body>
<header></header>
<nav></nav>
<main></main>
<aside></aside>
</body>

Així en resum aquesta és l’estructura bàsica. Ja que hi som he volgut ser una mica semàntic i utilitzar elements d’HTML5. Es pot fer el mateix mb divs.

Va, ara el CSS. La primera línia ja la sabem, oi?

body {
display: grid;
}

Fins aquí tot bé. Ja ets un frontend pro. Ara hem de pensar en files i columnes.

A veure quines podem identificar en la nostra aplicació?

Les veus? Aquestes són les principals files i columnes de l’aplicació: un grid de 2x2, amb les següents característiques (que són així perquè ho acabo de decidir):

  • El header fa 80px d’alçada.
  • La sidebar fa 350px d’amplada, igual que el nav.
  • La part principal ha d’ocupar la resta d’alçada i amplada.

El primer impuls imagino que serà fer alguna cosa com:

body {
display: grid;
grid-template-rows: 80px 100%;
grid-template-columns: 350px 100%;
}

I ja ho tenim, no? Columnes amb mida fixa, columnes que ocupen el 100%.

Però no et funcionarà. 100% + 80px és més del 100% de l’alçada de la pantalla :) Per solucionar-ho podem fer-ho de dues maneres: de forma ñaposa o bé. La forma ñaposa és amb calcs: 80px + calc(100% — 80px). Però no ens agrada gaire.

La gent del CSS Grid ja sabia que ens passaria, i per això es van inventar una nova unitat (sí amic, una altra nova unitat) per solucionar-ho: et presento els fr (fractional unit). Bàsicament, el que indica és que l’element amb aquesta amplada/alçada ocuparà tot l’espai disponible. I en cas d’haver-hi més d’un element amb fr, repartirà l’espai de forma equitativa.

És a dir:

body {
display: grid;
grid-template-rows: 80px 1fr;
grid-template-columns: 350px 1fr;

}

Bam! Ja ho tenim. Si et queda algun dubte respecte les fractional units, llegeix-te això. Està explicar en sensillo i és curt.

Però potser a algú li grinyola alguna cosa.

Hem definit un markup d’HTML molt interessant, amb 4 elements situats en un grid de 2x2. Però com ho ha fet per repartir els elements del nostre layout en els items del grid?

I com ho farem per canviar la disposició quan fem l’aplicació responsive? Com ho fem per poder ressituar còmodament els elements al canviar de vista mòbil a desktop o viceversa?

Entering…

Grid Template Areas

Fins ara hem vist columns i rows. Per col·locar els elements a dins del grid cal que definim les àrees. I aquí CSS Grid s’inventa una forma completament nova de fer-ho. Una característica que farà les delícies dels més backends. Mira:

header {
grid-area: header;
}
nav {
grid-area: nav;
}
main {
grid-area: main;
}
aside {
grid-area: aside;
}

Dit d’una altra manera: El header té un nom d’àrea igual a header. El main un nom main, el nav es diu nav, i l’aside… doncs aside.

I ara, la màgia:

body {
display: grid;
grid-template-rows: 80px 1fr;
grid-template-columns: 350px 1fr;
grid-template-areas: "nav header"
"aside main";
}

hahaha, et puc sentir riure des d’aquí. Riure de desesperació, potser. O riure de “OMG això és una passada”. Perquè és una passada: veieu el que hem fet? Recordeu que teníem un grid de 2x2? Doncs acabem de dir-li de forma completament declarativa quin element va a cada grid item.

Fi

ssim.

Et deixo 1 minut perquè hi pensis una mica. Al principi t’explota el cervell, i aleshores t’adones de la potència que té declarar layouts d’aquesta manera. Veus les dues columnes? Veus les dues files? No es pot ser més declaratiu, no? “Mira, CSS Grid, vull un aside aquí i un main aquí. Fes el que hagis de fer per posar-los-hi”.

Let’s go responsive (i mobile first!)

Fins ara no hem parlat de si el nostre template serà responsive. Fem-ho ara, ni que sigui per demostrar el poder de les àrees. Imagineu que el nostre dissenyador ha decidit que en mòbil vol mostrar el header, el contingut principal i aleshores un footer amb les icones que ara tenim a l’aside.

Com ho resolem?

D’entrada, hauríem d’haver plantejat el codi mobile-first, és a dir, maquetar assumint que treballem en dispositius mòbils i anar canviant la maquetació a mida que la pantalla creix. Per tant englobem les nostres files, columnes i àrees en una media query:

@media screen and (min-width: 700px) {
body {
grid-template-rows: 80px 1fr;
grid-template-columns: 350px 1fr;
grid-template-areas: "header header"
"aside main";
}
}

I ara, fora de la media query, definim com ha de ser la nostra interfície en mòbil.

body {
grid-template-rows: 80px 1fr 80px;
grid-template-areas: "header"
"main"
"aside";

}

Són 3 columnes, amb una sola fila (implícita): el header farà 80px d’alçada, igual que l’aside. El contingut principal ocuparà la resta d’espai disponible.

Això és el que tenim fins ara:

Va, ja tenim el layout definit. Ara toca omplir-lo.

Comencem pel principi: header i nav

First things first. Al header hi tenim un logotip i una icona, i al nav dos conjunts d’icones, alineats respectivament a l’esquerra i a la dreta.

<header>
<div class="header__title">dashboard</div>
<a class="header__icon" href="#"><i class="fa fa-bars"></i></a>
</header>
<nav>
<a class="nav__item" href="#"><i class="fa fa-bell"></i></a>
<a class="nav__item" href="#"><i class="fa fa-envelope"></i></a>
<a class="nav__item" href="#"><i class="fa fa-gear"></i></a>
<a class="nav__item" href="#"><i class="fa fa-search"></i></a>
<a class="nav__item" href="#"><i class="fa fa-rocket"></i></a>
</nav>

I ara el CSS. El post va de CSS Grid, però no tot es pot fer en CSS Grid o no sempre és la millor alternativa. Combinar CSS Grid i Flexbox és una gran idea i la manera que tenim a dia d’avui de definir interfícies. En general, farem servir CSS Grid per layouts multidimensionals, i Flexbox quan treballem només en una direcció (vertical o horitzontal, és igual).

Ja que hem començat amb mobile first ho mantindrem:

nav {
grid-area: nav;
display: flex;
justify-content: space-around;
align-items: center;

}
@media screen and (min-width: 900px) {
nav {
justify-content: inherit;
}
.nav__item--last-left {
margin-right: auto;
}

}

Veus les propietats align-items i justify-items? Aquí les fem servir amb Flexbox, però sapigueu que funcionen exactament igual en un Grid.

Per alinear les icones a la dreta (en desktop) podem fer servir una de les millors eines que té Flexbox, que és combinar-lo amb marges automàtics. Bam, una línia i tenim els elements a la dreta sense floats ni clearfix.

Pel que fa a la vista mobile hem optat per fer que les icones es reparteixin proporcionalment per l’espai disponible.

Necessito alguna cosa que m’identifiqui quin és l’última icona que va a l’esquerra, per poder empènyer la resta a la dreta. Ho faig amb una classe modificadora horrorosa, però que no m’acaba de fer gens el pes. Si algú té una idea millor que me la proposi als comentaris :)

Què dius? Ah, que vols veure el resultat?

Espera, que arreglem també el header i així tindrem tota la part superior feta.

header {
grid-area: header;
display: flex;
align-items: center;

}
.header__icon {
margin-left: auto;
}

En aquest cas el header es comportarà igual en mobile que en desktop.

Ara sí, una mica de padding entre elements i ja tenim el resultat:

Té, un Codepen amb el codi fins aquest punt.

Seguim: contingut principal

Val, ja tenim el header i el navbar. Manos a la obra amb el contingut principal.

Hi distingim dues zones clares: Overview i Summary, cada zona amb el seu títol. Curiosament, però, les pastilles de dins comparteixen proporcions, ho veus?

Podríem maquetar-ho tot utilitzant un sol grid?

Whooot?

A veure:

Sembla que quadra, no? Un grid de 3 columnes iguals i 5 files variables. Va, comencem amb el markup i veiem fins on arribem:

<main>
<div class="content-title">
Overview <i class="fa fa-angle-right"></i>
<div class="filter">
<a>today</a><a>month</a><a>year</a>
</div>
<button>Logout</button>
</div>
<div class="pill-wrapper">
<div class="pill">overall sale</div>
<div class="pill">overall visited</div>
<div class="pill">overall growth</div>
</div>
<div class="content-title">
Secondary <i class="fa fa-angle-right"></i>
</div>
<div class="chart">
total sale
</div>
<div class="activity-feed">
user activity
</div>
<div class="circle-chart">
daily traffic
</div>
<div class="circle-chart">
productivity
</div>
</main>

He remarcat en negreta els elements que són fills directes del nostre grid.

El següent pas ja el sabem fer, oi? Definir les files i les columnes. Si mirem el disseny inicial, podem definir-les així (again, els números exactes me’ls he tret del barret):

main {
grid-area: main;

display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 50px 120px 50px 2fr 1fr;

}

He decidit que hi hagi dues files que creixin, les dues últimes. A més, la penúltima fila (on va el gràfic gran) sempre serà el doble que l’altra (on van els gràfics circulars).

A més, com que em fa mandra escriure 1fr 1fr 1fr, aprofito la funció repeat() perquè ho escrigui per mi. Una altra novetat de CSS Grid, sí.

Fins aquí tot bé. Però com situem cada element al grid item que li toca?

Bona pregunta. Podríem fer-ho amb àrees, però té pinta de complicar-se, no? Quin nom li donem a cada àrea? Ja no és tan automàtic com era abans.

En aquest cas ho farem a partir de les grid lines, és a dir, de les línies que delimiten cada element del grid. En CSS Grid es numeren des de dalt a la dreta i a partir del número 1. És a dir, aquest és el número implícit de cada línia:

Fixeu-vos que numerem les línies, no les àrees!

Sabent això ja tens la meitat de la equació. L’altra meitat és que existeix una manera de dir-li a cada element entre quines línies ha d’aparèixer, ja siguin files o columnes. Així:

div {
grid-col: 1 / 3;
grid-row: 2 / 3;
}

Les propietats grid-col i grid-row serveixen per indicar, respectivament, el número de la línia on ha comença un element, i el número de la línia on acaba. En el codi d’aquí sobre hem definit un element de 2 columnes i una fila.

Amb això ja podem definir on va cada element del nostre contingut. Més o menys així:

.content-title {
grid-column: 1 / 4;
}
.pill-wrapper {
grid-column: 1 / 4;
}
.chart {
grid-column: 1 / 3;
}
.activity-feed {
grid-row: 4 / 6;
grid-column: 3 / 4;
}

I el resultat:

(He fet una mica de trampa afegint un display:flex; a on diu Overview, que sinó no quedava bonic).

Repeteixo: ho podríem haver fet amb àrees. Probablement sigui més declaratiu, però imagino que a la llarga ha de ser complicat de gestionar això de tenir tantes àrees.

He afegit també una altra propietat al Grid:

main {
grid-area: main;
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: 20px 120px 20px 2fr 1fr;
grid-gap: 1em;
}

Efectivament: tenim una propietat específica per definir el gap (el gutter, l’espai entre les coses) dels elements del grid. Cool, huh?! I existeixen grid-column-gap i grid-row-gap per… bé, és evident. S’han acabat els marges negatius, els divs amb inline-block i la resta de hacks lamentables per aconseguir fer quadrícules. Welcome to 2018.

Per cert: a les grid lines també se’ls pot donar nom, i referenciar-les pel nom enlloc del número. Deixo el link i hi dones un cop d’ull si interessa.

I aquest és el resultat mentre inspecciono el Grid utilitzant les Firefox DevTools:

Si haguéssim utilitzant grid areas amb nom també el veuríem sobreimprès.

Té, un altre Codepen. No et queixaràs, eh?

Disclaimer: hauríem d’haver seguit treballant en mobile first, plantejar la UI en interfícies petites i a partir d’aquí anar creixent. Però aquest post ja és prou llarg.

Maquetem un detall: user activity

No vull fer un post infinit (ja és prou llarg), però la pastilla de User Activity em fa especial gràcia perquè és un cas d’ús. Em centraré només en la llista d’elements, no pas en el títol i el botó, que és on hi ha una característica que vull explicar-te:

<ul class="feed">
<li class="feed__item user">...</li>
<li class="feed__item user">...</li>
<li class="feed__item user">...</li>
<li class="feed__item user">...</li>
...
</ul>

L’estructura és prou evident. I l’inici del CSS també:

.feed {
display: grid;
}

Però acaba de passar una cosa genial: els ítems de la llista ens han quedat ben situats, i no hem definit ni files ni columnes! L’autoplacement del CSS Grid acaba d’actuar.

Fixa’t que en cap moment li hem dit que es reparteixin els elements. Per defecte CSS Grid situa els fills d’un grid container en una columna i vàries files, i reparteix l’espai equitativament.

Ara hem de donar estructura a l’element user, i la cosa queda així:

<ul class="feed">
<li class="feed__item user">
<div class="user__avatar"></div>
<div class="user__name">User</div>
<div class="user__message">new comment</div>
<div class="user__timestamp">4 minutes ago</div>
</li>
<li class="feed__item user">
<div class="user__avatar"></div>
<div class="user__name">User</div>
<div class="user__message">new comment</div>
<div class="user__timestamp">4 minutes ago</div>
</li>
<li class="feed__item user">
<div class="user__avatar"></div>
<div class="user__name">User</div>
<div class="user__message">new comment</div>
<div class="user__timestamp">4 minutes ago</div>
</li>
...
</ul>

Hem vingut a jugar amb CSS Grid, i per tant l’utilitzarem per estructurar aquesta informació. Tot i que és prou de calaix que el layout de l’usuari és… un media object!

.user {
display: grid;
grid-template-columns: 40px 1fr 1fr;
grid-template-rows: repeat(2, 20px);
grid-column-gap: 1em; /* o bé grid-gap: 0 1em; */
align-items: center;
grid-template-areas: "user-avatar user-name user-name"
"user-avatar user-message user-timestamp";
}
.user__avatar {
grid-area: user-avatar;
}
.user__name {
grid-area: user-name;
}
.user__message {
grid-area: user-message;
}
.user__timestamp {
grid-area: user-timestamp;
}

Res que no hàgim vist abans.

Amb això, i una mica de CSS estilitzador que no té gaire importància, aconseguim un resultat prou decent:

El resultat

He deixat un altre Codepen amb l’estructura que hem anat muntant en aquest post i amb algun detall més a nivell d’estructura i estètic. He afegit l’estructura d’alguns elements més amb CSS Grid i Flexbox. També he separat els estils que interessen per aquest post dels estils purament cosmètics, així és més senzill d’entendre. El codi no és definitiu ni és una obra d’art, és una maquetació de batalla.

Hi he afegit alguns comentaris perquè quedin clars alguns detalls. Doneu-hi un cop d’ull.

L’enganxo aquí, però segurament es veurà millor a la web original:

En total, unes 150 línies de CSS per definir l’estructura de tot el que veieu al Codepen, incloent media queries. Pas mal.

La foto finish:

I a partir d’aquí?

Els següents passos seria seguir fent aquesta espècie de drill-down d’anar cada vegada a elements més petits i anar-los configurant. En un món ideal (o simplement en un projecte més gran) aquestes peces petites ja les tindríem creades (a.k.a. components), i amb la feina que hem fet fins ara seria suficient per tenir muntada una aplicació funcional.

Recap

Hem cobert tot el que pot fer CSS Grid?

No!

Ens han quedat coses al tinter: minmax, span, anar més enllà de simple quadrícules, grid lines negatius, línies amb noms implícits, àrees amb unitats com vmin o vmax, ordre dels elements...

Hem rascat la superfície del que es pot arribar a fer amb CSS Grid.

Penso que l’article ha servit per veure que aquesta és La Manera Correcta™ de definir interfícies el 2018. Amb CSS Grid i Flexbox podem resoldre les principals necessitats que tenen les interfícies a dia d’avui, i podem fer-ho d’una forma declarativa i idiomàtica, sense hacks.

Intenta fer la interfície que hem fet amb aprox 100 línies de codi sense CSS Grid ni Flexbox. Good luck.

Fixeu-vos com pensem en la interfície com un conjunt de parcel·les que omplim amb un element o altre en funció de la mida del dispositiu. És molt més agradable (i eficient, i mantenible) treballar així que no pas a partir de grids fixos (siguin 10, 12 o 56 columnes) que hem fet servir durant els últims anys.

Per qualsevol observació, millora o correcció, feel free de fer servir els comentaris! Que per això hi són.

Update: he publicat un altre post on explico 3 secrets de CSS Grid que potser no coneixies.

--

--

Words matter – Software product development, Front-end, UX, design, lean, agile and everything in between. https://afontcu.dev