card-11-corner-arrow-tile
tag-02-centered-cloud
Nuage de mots-clés centré rangée par rangée
Poser une dizaine de mots-clés de même rang au bas d'un module sans en faire une liste : chaque RANGÉE est centrée sur elle-même, si bien que les lignes de largeur inégale dessinent une silhouette de nuage — c'est le centrage par rangée, et pas le centrage du bloc, qui produit l'effet.
tagwebslide287×140px13 benchmarks
Le fragment RÉEL, dans son propre document, sur le sol de sa charte — pas une image. Ce que vous voyez est exactement ce que vous collerez.
Employer quand
Fin d'une vignette de service, pied d'une carte de rubrique, bandeau de compétences : dix à quinze termes courts qu'on veut donner à VOIR en volume plutôt qu'à lire un par un, sans hiérarchie entre eux et sans ordre significatif.
Éviter quand
L'ordre compte (un nuage le détruit visuellement), les termes sont cliquables et de longueurs très inégales (les cibles deviennent illisibles à balayer), il y en a moins de six — trois rangées sont le minimum pour qu'une silhouette existe — ou l'un d'eux doit être mis en avant : ce système n'a aucune ressource pour distinguer une pilule des autres.
Notes
L'assertion centrale est écrite sur les RANGÉES et pas sur le conteneur, parce que `justify-content: center` seul ne prouve rien : un bloc de pilules ferrées à gauche mais centré dans son module passerait ce contrôle et n'aurait aucune silhouette. La mesure regroupe donc les pilules par ordonnée et vérifie, ligne par ligne, que la marge gauche égale la marge droite. Deux assertions négatives complètent le geste. « Les rangées sont de largeurs inégales » écarte le pavé centré, qui est visuellement l'exact contraire d'un nuage tout en satisfaisant le centrage. « Aucune rangée orpheline » écarte le cas mesuré pendant le lot : dix termes courts dans un module de 287 px se rangeaient 3/3/3/1, et la pilule seule de la dernière ligne suffit à ruiner la silhouette. Le remède n'est pas dans le CSS — il est dans le choix des termes, et le benchmark est là pour l'imposer. Ce pattern ne double pas tag-01-gooey-capsule : celui-ci fabrique UN objet (un contour continu qui soude un glyphe à un mot), celui-là ne dit rien de la pilule et tout de leur ARRANGEMENT. On peut d'ailleurs servir l'un avec l'autre.
Mots-clés
Benchmarks — la géométrie, en assertions vérifiables
Rejouables par node bin/check.mjs tag-02-centered-cloud : le pattern se prouve, il ne se croit pas.
| assertion | mesure | attendu |
|---|---|---|
| les pilules passent à la ligne : c'est un nuage, pas une barre qui déborde | cs('.vl-cloud','flexWrap') === 'wrap' ? 1 : 0 | 1 |
| CHAQUE RANGÉE est centrée sur elle-même — l'écart à gauche égale l'écart à droite, ligne par ligne | (() => { const r = $('.vl-cloud').getBoundingClientRect(); const rows = {}; for (const e of document.querySelectorAll('.vl-cloud > *')) { const b = e.getBoundingClientRect(); const k = Math.round(b.top); (rows[k] = rows[k] || []).push(b); } return Math.max(...Object.values(rows).map((g) => Math.abs((Math.min(...g.map((b) => b.left)) - r.left) - (r.right - Math.max(...g.map((b) => b.right)))))) / r.width; })() | ≤ 0.012 |
| au moins TROIS rangées : en dessous, aucune silhouette n'existe | new Set([...document.querySelectorAll('.vl-cloud > *')].map((e) => Math.round(e.getBoundingClientRect().top))).size | ≥ 3 |
| les rangées sont de largeurs INÉGALES — sinon c'est un pavé centré, pas un nuage | (() => { const rows = {}; for (const e of document.querySelectorAll('.vl-cloud > *')) { const b = e.getBoundingClientRect(); const k = Math.round(b.top); (rows[k] = rows[k] || []).push(b); } const w = Object.values(rows).map((g) => Math.max(...g.map((b) => b.right)) - Math.min(...g.map((b) => b.left))); return (Math.max(...w) - Math.min(...w)) / W; })() | ≥ 0.06 |
| aucune rangée orpheline : la plus courte porte au moins deux pilules | (() => { const rows = {}; for (const e of document.querySelectorAll('.vl-cloud > *')) { const k = Math.round(e.getBoundingClientRect().top); rows[k] = (rows[k] || 0) + 1; } return Math.min(...Object.values(rows)); })() | ≥ 2 |
| une pilule EST une pilule : rayon ≥ sa demi-hauteur | num('.vl-cloud > *','borderTopLeftRadius') / box('.vl-cloud > *').h | ≥ 0.5 |
| la pilule est PLUS CLAIRE que le fond qui la porte — le rang de blanc se dépense vers le haut | lum(parseRgb(cs('.vl-cloud > *','backgroundColor'))) - lum(bgOf($('.vl-cloud'))) | ≥ 0.01 |
| pilule basse : sa hauteur reste sous 12 % de la largeur du nuage | box('.vl-cloud > *').h / W | ≤ 0.12 |
| la gouttière verticale est un peu plus large que l'horizontale : les rangées se lisent avant les colonnes | num('.vl-cloud','rowGap') - num('.vl-cloud','columnGap') | 0 → 6 |
| aucun terme ne se coupe : chaque pilule tient sur une ligne | [...document.querySelectorAll('.vl-cloud > *')].filter((e) => e.getBoundingClientRect().height > box('.vl-cloud > *').h * 1.4).length | 0 ± 0 |
| aucun liseré sur les pilules | num('.vl-cloud > *','borderTopWidth') | 0 ± 0 |
| contraste du texte de pilule ≥ 4,5 | contrast('.vl-cloud > *') | ≥ 4.5 |
| aucun débordement du nuage | overflow('.vl-cloud') | ≤ 0 |
Le code
patterns/tag-02-centered-cloud.html
Télécharger<!-- tag-02-centered-cloud — dix mots-clés en nuage. Couleurs via --vl-* uniquement.
TOUT le pattern tient dans `flex-wrap: wrap` + `justify-content: center` : chaque rangée
se centre SUR ELLE-MÊME, et ce sont les largeurs inégales qui dessinent la silhouette.
Un bloc centré dont les rangées seraient ferrées à gauche donnerait un pavé, pas un nuage.
Le choix des TERMES fait partie du pattern : dix mots trop courts dans un module étroit
se rangent 3/3/3/1, et la pilule orpheline de la dernière ligne ruine la silhouette.
Le remède est dans la longueur des libellés, pas dans le CSS — d'où le benchmark.
`.vl-cloud__field` n'est PAS le nuage : c'est le module qui l'accueillera, posé ici pour
que la règle de rang de blanc (« la pilule est plus claire que le fond qui la porte »)
soit MESURABLE sur le fragment seul. Le nuage, lui, n'a aucun fond propre — le retirer
rendrait le fragment collable partout et la règle invérifiable ; on garde donc le champ
et on le nomme pour ce qu'il est. -->
<div class="vl-cloud__field">
<div class="vl-cloud">
<span>family law</span><span>commercial disputes</span>
<span>property & housing</span><span>tax and duties</span>
<span>employment law</span><span>filings</span><span>claims</span>
<span>contracts</span><span>case support</span><span>advice</span>
</div>
</div>
<style>
.vl-cloud__field {
width: 343px; padding: 28px;
box-sizing: border-box;
background: var(--vl-module);
border-radius: var(--vl-radius-module, 24px);
}
.vl-cloud {
width: 287px;
box-sizing: border-box;
display: flex; flex-wrap: wrap;
justify-content: center;
/* La gouttière verticale est un cran plus large que l'horizontale : les rangées se lisent
alors avant les colonnes, ce qui est la lecture voulue d'un nuage. À gouttières égales,
l'œil se met à chercher des colonnes qui n'existent pas. */
gap: 8px 7px;
font-family: Inter, "Helvetica Neue", Arial, sans-serif;
}
.vl-cloud > span {
background: var(--vl-object);
color: var(--vl-ink);
border-radius: var(--vl-radius-pill, 999px);
padding: 6px 12px;
font-size: 11px; line-height: 1.2; font-weight: 500;
white-space: nowrap;
}
</style>
systems/ref-17-mono-bento-legal.json
Télécharger:root {
--vl-page: #FEFEFE;
--vl-module: #EDEDED;
--vl-object: #FFFFFF;
--vl-invert: #000000;
--vl-ink: #111111;
--vl-ink-soft: #5F5F5F;
--vl-on-invert: #FFFFFF;
--vl-on-invert-soft: rgba(255,255,255,0.62);
--vl-line: rgba(17,17,17,0.55);
--vl-radius-page: 38px;
--vl-radius-module: 24px;
--vl-radius-bar: 20px;
--vl-radius-pill: 999px;
--vl-gutter: 26px;
--vl-margin: 57px;
--vl-bar-h: 69px;
--vl-col-1: 343fr;
--vl-col-2: 466fr;
--vl-col-3: 464fr;
--vl-row-1: 302fr;
--vl-row-2: 232fr;
--vl-row-3: 243fr;
}
les métadonnées — patterns/tag-02-centered-cloud.json (télécharger)
{
"name": "Nuage de mots-clés centré rangée par rangée",
"family": "tag",
"ref": "ref-17-mono-bento-legal",
"intent": "Poser une dizaine de mots-clés de même rang au bas d'un module sans en faire une liste : chaque RANGÉE est centrée sur elle-même, si bien que les lignes de largeur inégale dessinent une silhouette de nuage — c'est le centrage par rangée, et pas le centrage du bloc, qui produit l'effet.",
"when_to_use": "Fin d'une vignette de service, pied d'une carte de rubrique, bandeau de compétences : dix à quinze termes courts qu'on veut donner à VOIR en volume plutôt qu'à lire un par un, sans hiérarchie entre eux et sans ordre significatif.",
"avoid_when": "L'ordre compte (un nuage le détruit visuellement), les termes sont cliquables et de longueurs très inégales (les cibles deviennent illisibles à balayer), il y en a moins de six — trois rangées sont le minimum pour qu'une silhouette existe — ou l'un d'eux doit être mis en avant : ce système n'a aucune ressource pour distinguer une pilule des autres.",
"media": [
"web",
"slide"
],
"tags": [
"tag",
"mot-cle",
"keyword",
"nuage",
"cloud",
"pilule",
"pill",
"chip",
"competences",
"services",
"wrap",
"monochrome"
],
"vars": [
{
"name": "--vl-object",
"role": "aplat d'une pilule — PLUS CLAIR que le module qui la porte",
"default": "#FFFFFF"
},
{
"name": "--vl-ink",
"role": "texte d'une pilule",
"default": "#111111"
},
{
"name": "--vl-radius-pill",
"role": "rayon d'une pilule",
"default": "999px"
}
],
"slots": [
"items"
],
"geometry": {
"root": ".vl-cloud",
"frame": [
287,
140
],
"ratios": {
"pill_height_over_width": 0.091,
"gap_x_over_width": 0.024,
"gap_y_over_width": 0.028,
"pad_x_over_pill_height": 0.46
},
"type_px": {
"tag": 11
},
"pad_ratio": {
"top": 0,
"x": 0,
"bottom": 0
}
},
"benchmarks": [
{
"name": "les pilules passent à la ligne : c'est un nuage, pas une barre qui déborde",
"measure": "cs('.vl-cloud','flexWrap') === 'wrap' ? 1 : 0",
"expect": 1
},
{
"name": "CHAQUE RANGÉE est centrée sur elle-même — l'écart à gauche égale l'écart à droite, ligne par ligne",
"measure": "(() => { const r = $('.vl-cloud').getBoundingClientRect(); const rows = {}; for (const e of document.querySelectorAll('.vl-cloud > *')) { const b = e.getBoundingClientRect(); const k = Math.round(b.top); (rows[k] = rows[k] || []).push(b); } return Math.max(...Object.values(rows).map((g) => Math.abs((Math.min(...g.map((b) => b.left)) - r.left) - (r.right - Math.max(...g.map((b) => b.right)))))) / r.width; })()",
"max": 0.012
},
{
"name": "au moins TROIS rangées : en dessous, aucune silhouette n'existe",
"measure": "new Set([...document.querySelectorAll('.vl-cloud > *')].map((e) => Math.round(e.getBoundingClientRect().top))).size",
"min": 3
},
{
"name": "les rangées sont de largeurs INÉGALES — sinon c'est un pavé centré, pas un nuage",
"measure": "(() => { const rows = {}; for (const e of document.querySelectorAll('.vl-cloud > *')) { const b = e.getBoundingClientRect(); const k = Math.round(b.top); (rows[k] = rows[k] || []).push(b); } const w = Object.values(rows).map((g) => Math.max(...g.map((b) => b.right)) - Math.min(...g.map((b) => b.left))); return (Math.max(...w) - Math.min(...w)) / W; })()",
"min": 0.06
},
{
"name": "aucune rangée orpheline : la plus courte porte au moins deux pilules",
"measure": "(() => { const rows = {}; for (const e of document.querySelectorAll('.vl-cloud > *')) { const k = Math.round(e.getBoundingClientRect().top); rows[k] = (rows[k] || 0) + 1; } return Math.min(...Object.values(rows)); })()",
"min": 2
},
{
"name": "une pilule EST une pilule : rayon ≥ sa demi-hauteur",
"measure": "num('.vl-cloud > *','borderTopLeftRadius') / box('.vl-cloud > *').h",
"min": 0.5
},
{
"name": "la pilule est PLUS CLAIRE que le fond qui la porte — le rang de blanc se dépense vers le haut",
"measure": "lum(parseRgb(cs('.vl-cloud > *','backgroundColor'))) - lum(bgOf($('.vl-cloud')))",
"min": 0.01
},
{
"name": "pilule basse : sa hauteur reste sous 12 % de la largeur du nuage",
"measure": "box('.vl-cloud > *').h / W",
"max": 0.12
},
{
"name": "la gouttière verticale est un peu plus large que l'horizontale : les rangées se lisent avant les colonnes",
"measure": "num('.vl-cloud','rowGap') - num('.vl-cloud','columnGap')",
"min": 0,
"max": 6
},
{
"name": "aucun terme ne se coupe : chaque pilule tient sur une ligne",
"measure": "[...document.querySelectorAll('.vl-cloud > *')].filter((e) => e.getBoundingClientRect().height > box('.vl-cloud > *').h * 1.4).length",
"expect": 0,
"tol": 0
},
{
"name": "aucun liseré sur les pilules",
"measure": "num('.vl-cloud > *','borderTopWidth')",
"expect": 0,
"tol": 0
},
{
"name": "contraste du texte de pilule ≥ 4,5",
"measure": "contrast('.vl-cloud > *')",
"min": 4.5
},
{
"name": "aucun débordement du nuage",
"measure": "overflow('.vl-cloud')",
"max": 0
}
],
"notes": "L'assertion centrale est écrite sur les RANGÉES et pas sur le conteneur, parce que `justify-content: center` seul ne prouve rien : un bloc de pilules ferrées à gauche mais centré dans son module passerait ce contrôle et n'aurait aucune silhouette. La mesure regroupe donc les pilules par ordonnée et vérifie, ligne par ligne, que la marge gauche égale la marge droite.\n\nDeux assertions négatives complètent le geste. « Les rangées sont de largeurs inégales » écarte le pavé centré, qui est visuellement l'exact contraire d'un nuage tout en satisfaisant le centrage. « Aucune rangée orpheline » écarte le cas mesuré pendant le lot : dix termes courts dans un module de 287 px se rangeaient 3/3/3/1, et la pilule seule de la dernière ligne suffit à ruiner la silhouette. Le remède n'est pas dans le CSS — il est dans le choix des termes, et le benchmark est là pour l'imposer.\n\nCe pattern ne double pas tag-01-gooey-capsule : celui-ci fabrique UN objet (un contour continu qui soude un glyphe à un mot), celui-là ne dit rien de la pilule et tout de leur ARRANGEMENT. On peut d'ailleurs servir l'un avec l'autre.",
"id": "tag-02-centered-cloud"
}
À côté
layout-05-nav-pill-bar
Barre de menu en pilule flottante
layout-06-bento-l-span
Bento 3 × 3 à deux enjambements en L
tag-01-gooey-capsule
Capsule néon soudée — mot-clé + glyphe
tag-03-action-pill-bar