Aller au contenu
Bibliotheque UIJABS

CSS moderne 12 variantes

Les fonctions CSS qui suppriment du JavaScript. Chacune remplace un schema qui demandait, il y a deux ans, une bibliotheque ou plusieurs dizaines de lignes. Chaque fiche indique son etat de disponibilite, et la regle tenue partout est la meme : une fonction recente est employee en amelioration, jamais en fondation. Le repli n'est pas une degradation, c'est le comportement normal du site pour qui n'a pas la derniere version de son navigateur.

A. Elements flottants sans JavaScript

Popover natif, ancrage de position avec repli automatique, info-bulle complete sans une ligne de code.

Panneau natif

Fermeture au clic exterieur et sur Echap, sans une ligne de code. La pile d'empilement est geree par le navigateur.

Popover natif

L'attribut popover donne le comportement complet d'un panneau flottant : ouverture, fermeture au clic exterieur, fermeture sur Echap, gestion du focus et de la pile. Zero ligne de JavaScript.

Voir le codeMasquer le code
<div class="cm-demo">
  <button class="btn btn--secondaire" popovertarget="pop-demo">Ouvrir le panneau <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
  <div id="pop-demo" popover class="cm-pop">
    <strong>Panneau natif</strong>
    <p>Fermeture au clic exterieur et sur Echap, sans une ligne de code. La pile d'empilement est geree par le navigateur.</p>
    <button class="btn btn--plein btn--sm" popovertarget="pop-demo" popovertargetaction="hide">Fermer</button>
  </div>
</div>
.cm-pop {
  width: 300px;
  padding: var(--sp-4);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  box-shadow: var(--sh-4);
  color: var(--c-text);
}
.cm-pop strong { display: block; font-size: var(--fs-md); font-weight: var(--fw-bold); margin-bottom: 5px; }
.cm-pop p { margin: 0 0 var(--sp-3); font-size: var(--fs-sm); color: var(--c-text-soft); line-height: 1.55; }
/* Le voile derriere le popover, stylable directement. */
.cm-pop::backdrop { background: rgba(10,12,18,.18); }
.cm-demo { display: flex; flex-wrap: wrap; gap: var(--sp-3); align-items: center; min-height: 60px; }
&lt;button popovertarget="x"&gt; + &lt;div id="x" popover&gt; &mdash; disponible partout depuis 2024
Ancre automatique

Positionne sous le bouton, bascule au-dessus s'il n'y a pas la place en bas.

position-area: block-end span-inline-end
position-try-fallbacks: flip-block, flip-inline
Ancrage de position

Le panneau se positionne par rapport a son declencheur, et bascule tout seul quand il manque de place. C'est ce qui remplace les bibliotheques de positionnement flottant et leurs calculs au defilement.

Voir le codeMasquer le code
<div class="cm-demo">
  <button class="btn btn--secondaire cm-ancre" popovertarget="pop-ancre" style="anchor-name:--a1">En bas <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
  <div id="pop-ancre" popover class="cm-pop cm-pop--ancre" style="position-anchor:--a1">
    <strong>Ancre automatique</strong>
    <p>Positionne sous le bouton, bascule au-dessus s'il n'y a pas la place en bas.</p>
  </div>
  <span class="cm-note">position-area: block-end span-inline-end<br>position-try-fallbacks: flip-block, flip-inline</span>
</div>
.cm-pop--ancre {
  margin: 6px 0 0;
  /* Se place sous l'ancre, aligne a gauche. */
  position-area: block-end span-inline-end;
  /* Bascule si la place manque, plutot que de sortir de l'ecran. */
  position-try-fallbacks: flip-block, flip-inline;
}
.cm-note {
  font-family: var(--font-mono);
  font-size: 11px;
  line-height: 1.6;
  color: var(--c-text-muted);
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  background: var(--c-surface-2);
}
/* Repli : sans ancrage, le panneau reste centre a l'ecran, ce qui est le
   comportement natif d'un popover. Rien ne casse. */
@supports not (position-area: block-end) {
  .cm-pop--ancre { margin: 0; }
}
anchor-name sur le declencheur, position-anchor sur le panneau
Le prix indique comprend la pose et la mise en service.
Genere un fichier Excel avec la mise en page de la societe.
Info-bulle sans JavaScript

Popover et ancrage combines : la bulle suit son declencheur, se ferme seule, et se replace quand elle depasse. Le tout en CSS, avec le comportement clavier inclus.

Voir le codeMasquer le code
<div class="cm-demo">
  <button class="btn btn--icone btn--secondaire" popovertarget="bulle1" style="anchor-name:--b1" aria-label="Aide"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 9v4M12 17h.01M10.3 3.9 1.8 18a2 2 0 0 0 1.7 3h17a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z"/></svg></button>
  <div id="bulle1" popover class="cm-bulle" style="position-anchor:--b1">Le prix indique comprend la pose et la mise en service.</div>
  <button class="btn btn--secondaire" popovertarget="bulle2" style="anchor-name:--b2">Exporter <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg></button>
  <div id="bulle2" popover class="cm-bulle" style="position-anchor:--b2">Genere un fichier Excel avec la mise en page de la societe.</div>
</div>
.cm-bulle {
  max-width: 240px;
  margin: 0 0 8px;
  padding: 7px 11px;
  border: none;
  border-radius: var(--r-sm);
  background: var(--c-text);
  color: var(--c-surface);
  font-size: var(--fs-xs);
  line-height: 1.45;
  position-area: block-start;
  position-try-fallbacks: flip-block;
}
.cm-bulle::backdrop { background: transparent; }
popover=hint pour une bulle qui ne ferme pas les autres panneaux ouverts

B. Selection et portee

:has() le selecteur de parent, @scope et sa limite haute, requetes de conteneur.

Total des options : 340,00 EUR
.carte:has(:checked) { border-color: var(--c-primary) }
form:has(:invalid) button[type=submit] { opacity: .5 }
Le selecteur de parent

:has() permet enfin de styler un element selon ce qu'il contient. C'est ce qui supprime la plupart des classes ajoutees en JavaScript pour refleter un etat.

Voir le codeMasquer le code
<div class="demo-col demo-col--large">
  <label class="cm-carte-choix"><input type="checkbox"><span class="coche__boite"></span><span><strong>Pose comprise</strong><small>Intervention sous 3 semaines</small></span><em>+ 1 200 EUR</em></label>
  <label class="cm-carte-choix"><input type="checkbox" checked><span class="coche__boite"></span><span><strong>Extension de garantie</strong><small>5 ans pieces et main d'oeuvre</small></span><em>+ 340 EUR</em></label>
  <div class="cm-total">Total des options : <strong>340,00 EUR</strong></div>
  <span class="cm-note">.carte:has(:checked) { border-color: var(--c-primary) }<br>form:has(:invalid) button[type=submit] { opacity: .5 }</span>
</div>
.cm-carte-choix {
  display: flex; align-items: center; gap: var(--sp-3);
  padding: var(--sp-4);
  border: 1.5px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
  cursor: pointer;
  transition: border-color var(--t-fast), background-color var(--t-fast);
}
.cm-carte-choix input { position: absolute; opacity: 0; width: 0; height: 0; }
.cm-carte-choix > span:last-of-type { display: flex; flex-direction: column; flex: 1; }
.cm-carte-choix strong { font-size: var(--fs-md); font-weight: var(--fw-semi); }
.cm-carte-choix small { font-size: var(--fs-xs); color: var(--c-text-muted); }
.cm-carte-choix em { font-style: normal; font-weight: var(--fw-bold); }
/* La carte reagit a l'etat de la case qu'elle contient. */
.cm-carte-choix:has(input:checked) {
  border-color: var(--c-primary);
  background: color-mix(in srgb, var(--c-primary-soft) 50%, var(--c-surface));
}
.cm-total { padding: var(--sp-3); border-radius: var(--r-md); background: var(--c-surface-2); font-size: var(--fs-sm); }
:has(:checked), :has(> img), form:has(:invalid) &mdash; disponible partout depuis 2023
/* Les regles ne s'appliquent qu'a l'interieur de .carte,
   et s'arretent avant toute .carte imbriquee */
@scope (.carte) to (.carte .carte) {
  h3 { font-size: 18px; font-weight: 700; }
  p  { color: var(--c-text-soft); }
  a  { color: var(--c-primary); }
}

/* :scope designe la racine, et la proximite l'emporte sur
   la specificite : plus besoin de surenchere de selecteurs */
@scope (.theme-sombre) {
  :scope { background: #14181f; color: #e8eef6; }
  a { color: #7fb3ff; }
}

Sans @scope, isoler un composant demandait soit un prefixe sur chaque classe, soit une surenchere de specificite. La limite haute (to) est ce qui manquait le plus : elle permet a un composant de ne pas styler ses propres copies imbriquees.

Portee de style

@scope limite des regles a un sous-arbre, et coupe la cascade a une frontiere. Il remplace les prefixes de classe defensifs et les guerres de specificite.

Voir le codeMasquer le code
<div class="mv-code">
  <pre><code>/* Les regles ne s'appliquent qu'a l'interieur de .carte,
   et s'arretent avant toute .carte imbriquee */
@scope (.carte) to (.carte .carte) {
  h3 { font-size: 18px; font-weight: 700; }
  p  { color: var(--c-text-soft); }
  a  { color: var(--c-primary); }
}

/* :scope designe la racine, et la proximite l'emporte sur
   la specificite : plus besoin de surenchere de selecteurs */
@scope (.theme-sombre) {
  :scope { background: #14181f; color: #e8eef6; }
  a { color: #7fb3ff; }
}</code></pre>
  <p class="thm-note">Sans <code>@scope</code>, isoler un composant demandait soit un prefixe sur chaque classe,
  soit une surenchere de specificite. La limite haute (<code>to</code>) est ce qui manquait le plus :
  elle permet a un composant de ne pas styler ses propres copies imbriquees.</p>
</div>
/* aucun style specifique : le socle .btn suffit */
@scope (.racine) to (.limite) { ... } &mdash; disponible dans tous les moteurs depuis 2025
Dossier 2026-103

Neuilly-sur-Marne — PAC air/eau 11 kW

Signe
Dossier 2026-103

Neuilly-sur-Marne — PAC air/eau 11 kW

Signe
Dossier 2026-103

Neuilly-sur-Marne

Signe
Requetes de conteneur

Un composant qui s'adapte a la largeur de SON conteneur, pas a celle de la fenetre. C'est la seule facon d'avoir un composant vraiment reutilisable dans une colonne etroite comme en pleine page.

Voir le codeMasquer le code
<div class="cm-conteneurs">
  <div class="cm-boite" style="width:100%"><div class="cm-adapt"><div class="cm-adapt__img"></div><div><strong>Dossier 2026-103</strong><p>Neuilly-sur-Marne &mdash; PAC air/eau 11 kW</p><span class="doc-etiq doc-etiq--ok">Signe</span></div></div></div>
  <div class="cm-boite" style="width:340px"><div class="cm-adapt"><div class="cm-adapt__img"></div><div><strong>Dossier 2026-103</strong><p>Neuilly-sur-Marne &mdash; PAC air/eau 11 kW</p><span class="doc-etiq doc-etiq--ok">Signe</span></div></div></div>
  <div class="cm-boite" style="width:210px"><div class="cm-adapt"><div class="cm-adapt__img"></div><div><strong>Dossier 2026-103</strong><p>Neuilly-sur-Marne</p><span class="doc-etiq doc-etiq--ok">Signe</span></div></div></div>
</div>
.cm-conteneurs { display: flex; flex-direction: column; gap: var(--sp-3); align-items: flex-start; width: 100%; }
.cm-boite { container-type: inline-size; max-width: 100%; }
.cm-adapt {
  display: flex; flex-direction: column; gap: var(--sp-3);
  padding: var(--sp-3);
  border: 1px solid var(--c-border);
  border-radius: var(--r-lg);
  background: var(--c-surface);
}
.cm-adapt__img { aspect-ratio: 16 / 9; border-radius: var(--r-md); background: linear-gradient(140deg, #4a6fa5, #8a6a4a); flex: none; }
.cm-adapt strong { display: block; font-size: var(--fs-md); font-weight: var(--fw-bold); }
.cm-adapt p { margin: 3px 0 var(--sp-2); font-size: var(--fs-sm); color: var(--c-text-muted); }
/* Le meme composant, trois formes, selon la largeur du conteneur. */
@container (min-width: 300px) {
  .cm-adapt { flex-direction: row; align-items: center; }
  .cm-adapt__img { width: 90px; aspect-ratio: 1; }
}
@container (min-width: 520px) {
  .cm-adapt { padding: var(--sp-4); gap: var(--sp-4); }
  .cm-adapt__img { width: 130px; aspect-ratio: 16 / 10; }
  .cm-adapt strong { font-size: var(--fs-xl); }
}
container-type: inline-size sur le parent, @container (min-width: X) pour l'enfant

C. Mise en page et texte

Champ qui grandit, ajustement optique, grille en lanes, couleurs derivees, logique conditionnelle, transitions entre vues.

field-sizing: content; max-height: 200px;
Champ qui grandit

field-sizing: content fait grandir un champ ou une zone de texte avec son contenu. Il remplace le calcul de hauteur en JavaScript qu'on trouvait dans toutes les zones de saisie.

Voir le codeMasquer le code
<div class="demo-col demo-col--large">
  <div class="champ"><label class="champ__label">Zone qui s'adapte</label><textarea class="saisie cm-grandit" rows="1">Cette zone grandit avec son contenu, sans une ligne de JavaScript. Ajoutez du texte pour la voir s'etendre, jusqu'a la limite fixee par max-height.</textarea></div>
  <div class="champ"><label class="champ__label">Champ qui s'ajuste a la valeur</label><input class="saisie cm-grandit cm-grandit--min" value="4 280,00"></div>
  <span class="cm-note">field-sizing: content; max-height: 200px;</span>
</div>
.cm-grandit { field-sizing: content; max-height: 200px; min-height: 44px; }
.cm-grandit--min { width: auto; min-width: 120px; }
field-sizing: content &mdash; Chrome et Edge depuis 2024, Safari 26, Firefox en cours
Sans ajustement

Titre de section

Avec text-box-trim

Titre de section

Ajustement optique du texte

text-box-trim retire l'espace au-dessus des majuscules et sous la ligne de base. C'est ce qui permet d'aligner vraiment un titre sur une icone ou un bord, sans marge negative bricolee.

Voir le codeMasquer le code
<div class="cm-trim">
  <div><span class="cm-trim__etiq">Sans ajustement</span><div class="cm-trim__boite"><h4>Titre de section</h4></div></div>
  <div><span class="cm-trim__etiq">Avec text-box-trim</span><div class="cm-trim__boite"><h4 class="cm-coupe">Titre de section</h4></div></div>
</div>
.cm-trim { display: grid; grid-template-columns: 1fr 1fr; gap: var(--sp-4); }
.cm-trim__etiq { display: block; margin-bottom: 6px; font-size: 10px; font-weight: var(--fw-bold); text-transform: uppercase; letter-spacing: .07em; color: var(--c-text-muted); }
.cm-trim__boite { padding: var(--sp-3); border: 1px dashed var(--c-primary); border-radius: var(--r-sm); background: var(--c-surface); }
.cm-trim h4 { margin: 0; font-size: 26px; font-weight: 800; letter-spacing: -.02em; }
/* Le cadre epouse la hauteur des lettres, pas celle de la police. */
.cm-coupe { text-box: trim-both cap alphabetic; }
@media (max-width: 560px) { .cm-trim { grid-template-columns: 1fr; } }
text-box: trim-both cap alphabetic &mdash; Chrome et Safari, non encore dans Firefox
Facade
Combles
Local technique
Unite exterieure
Tableau
Toiture
Grille en lanes

La mise en page en briques decalees, native. Elle remplace les bibliotheques de masonry et leurs recalculs a chaque redimensionnement. Encore recente : le repli est une grille reguliere, ce qui reste parfaitement correct.

Voir le codeMasquer le code
<div class="cm-lanes">
  <figure style="--h:150px"><div style="--t:linear-gradient(140deg,#4a6fa5,#c9d9e8)"></div><figcaption>Facade</figcaption></figure>
  <figure style="--h:210px"><div style="--t:linear-gradient(140deg,#a8643c,#f0d9b5)"></div><figcaption>Combles</figcaption></figure>
  <figure style="--h:120px"><div style="--t:linear-gradient(140deg,#3d6b4f,#c3ddc9)"></div><figcaption>Local technique</figcaption></figure>
  <figure style="--h:180px"><div style="--t:linear-gradient(140deg,#6b4a72,#ddc9dc)"></div><figcaption>Unite exterieure</figcaption></figure>
  <figure style="--h:135px"><div style="--t:linear-gradient(140deg,#8a3b3b,#e8c4c4)"></div><figcaption>Tableau</figcaption></figure>
  <figure style="--h:195px"><div style="--t:linear-gradient(140deg,#35485c,#b9c6cd)"></div><figcaption>Toiture</figcaption></figure>
</div>
/* Repli universel : colonnes CSS. Le rendu est proche, sans l'ordre exact. */
.cm-lanes { columns: 3; column-gap: var(--sp-3); width: 100%; }
.cm-lanes figure { break-inside: avoid; margin: 0 0 var(--sp-3); }
.cm-lanes div { height: var(--h); border-radius: var(--r-md); background: var(--t); }
.cm-lanes figcaption { padding-top: 6px; font-size: var(--fs-xs); color: var(--c-text-muted); }
/* Quand la grille en lanes est disponible, on la prefere : l'ordre de lecture
   suit alors les lignes, ce que les colonnes CSS ne savent pas faire. */
@supports (grid-template-rows: masonry) {
  .cm-lanes {
    columns: auto;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    grid-template-rows: masonry;
    gap: var(--sp-3);
  }
  .cm-lanes figure { margin: 0; }
}
@media (max-width: 640px) { .cm-lanes { columns: 2; } }
grid-template-rows: masonry (Safari 26) &mdash; repli en colonnes CSS partout ailleurs
--marque
survol — 18 % de noir
fond doux — 12 %
bordure — 30 %
eclairci en oklch
complementaire
Couleurs derivees

color-mix() et les couleurs relatives permettent de deduire une palette entiere d'une seule teinte. Un client fournit sa couleur, les survols, fonds doux et bordures s'en deduisent.

Voir le codeMasquer le code
<div class="cm-couleurs" style="--marque:#b8002e">
  <div style="background:var(--marque)"><span>--marque</span></div>
  <div style="background:color-mix(in srgb, var(--marque) 82%, black)"><span>survol &mdash; 18 % de noir</span></div>
  <div style="background:color-mix(in srgb, var(--marque) 12%, white);color:var(--marque)"><span>fond doux &mdash; 12 %</span></div>
  <div style="background:color-mix(in srgb, var(--marque) 30%, white);color:var(--c-text)"><span>bordure &mdash; 30 %</span></div>
  <div style="background:oklch(from #b8002e calc(l + .28) c h)"><span>eclairci en oklch</span></div>
  <div style="background:oklch(from #b8002e l c calc(h + 180))"><span>complementaire</span></div>
</div>
.cm-couleurs { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: var(--sp-2); width: 100%; }
.cm-couleurs > div {
  display: grid; place-items: center;
  min-height: 74px;
  padding: var(--sp-2);
  border-radius: var(--r-md);
  color: #fff;
  text-align: center;
}
.cm-couleurs span { font-family: var(--font-mono); font-size: 10.5px; font-weight: var(--fw-bold); }
color-mix(in srgb, X 12%, white) et oklch(from X calc(l + .28) c h)
/* Une seule regle, trois resultats selon un attribut */
.etiquette {
  background: if(
    style(--ton: succes): var(--c-success-soft);
    style(--ton: alerte): var(--c-warn-soft);
    else: var(--c-surface-3)
  );
}

/* Ce qu'on ecrivait avant : trois regles et trois classes */
.etiquette             { background: var(--c-surface-3); }
.etiquette--succes     { background: var(--c-success-soft); }
.etiquette--alerte     { background: var(--c-warn-soft); }

Encore experimental — Chrome 137 et suivants seulement, au moment ou ces lignes sont ecrites. La forme a trois classes reste la bonne fondation ; if() se pose par-dessus quand il permet d'eviter une combinatoire de classes qui deviendrait ingerable.

Logique conditionnelle

if() introduit une condition dans une valeur, sans classe ni media query. Encore tres recent : a n'employer que la ou le repli est acceptable.

Voir le codeMasquer le code
<div class="mv-code">
  <pre><code>/* Une seule regle, trois resultats selon un attribut */
.etiquette {
  background: if(
    style(--ton: succes): var(--c-success-soft);
    style(--ton: alerte): var(--c-warn-soft);
    else: var(--c-surface-3)
  );
}

/* Ce qu'on ecrivait avant : trois regles et trois classes */
.etiquette             { background: var(--c-surface-3); }
.etiquette--succes     { background: var(--c-success-soft); }
.etiquette--alerte     { background: var(--c-warn-soft); }</code></pre>
  <p class="thm-note"><strong>Encore experimental</strong> &mdash; Chrome 137 et suivants seulement, au moment ou ces lignes
  sont ecrites. La forme a trois classes reste la bonne fondation ; <code>if()</code> se pose par-dessus quand
  il permet d'eviter une combinatoire de classes qui deviendrait ingerable.</p>
</div>
/* aucun style specifique : le socle .btn suffit */
if(style(--x: valeur): A; else: B) &mdash; experimental, prevoir le repli
// Une seule ligne enveloppe le changement de contenu
document.startViewTransition(() => {
  liste.innerHTML = nouvellesLignes;
});

/* Chaque element qui doit etre suivi porte un nom unique */
.ligne[data-id="103"] { view-transition-name: ligne-103; }

/* La duree et la courbe se reglent en CSS */
::view-transition-group(*) {
  animation-duration: 280ms;
  animation-timing-function: cubic-bezier(.2,0,0,1);
}

/* Entre deux PAGES, une seule declaration suffit */
@view-transition { navigation: auto; }

C'est ce qui remplace FLIP dans la plupart des cas : le navigateur mesure les positions, applique la difference et anime, sans qu'on ait a le faire soi-meme. Les navigateurs qui ne le connaissent pas executent simplement le changement sans animation, ce qui reste correct.

Transitions entre vues

Les View Transitions animent le passage d'un etat a l'autre dans la meme page : le filtre change, les lignes qui restent glissent a leur nouvelle place au lieu de clignoter.

Voir le codeMasquer le code
<div class="mv-code">
  <pre><code>// Une seule ligne enveloppe le changement de contenu
document.startViewTransition(() =&gt; {
  liste.innerHTML = nouvellesLignes;
});

/* Chaque element qui doit etre suivi porte un nom unique */
.ligne[data-id="103"] { view-transition-name: ligne-103; }

/* La duree et la courbe se reglent en CSS */
::view-transition-group(*) {
  animation-duration: 280ms;
  animation-timing-function: cubic-bezier(.2,0,0,1);
}

/* Entre deux PAGES, une seule declaration suffit */
@view-transition { navigation: auto; }</code></pre>
  <p class="thm-note">C'est ce qui remplace FLIP dans la plupart des cas : le navigateur mesure les positions,
  applique la difference et anime, sans qu'on ait a le faire soi-meme. Les navigateurs qui ne le connaissent
  pas executent simplement le changement sans animation, ce qui reste correct.</p>
</div>
/* aucun style specifique : le socle .btn suffit */
document.startViewTransition() + view-transition-name