Partager des éléments entre eux à l'aide du positionnement d'ancrage CSS

.

Comment faites-vous actuellement pour associer un élément à un autre ? Vous pouvez essayer de suivre leurs positions ou d'utiliser un élément wrapper.

<!-- index.html -->
<div class="container">
  <a href="/link" class="anchor">I’m the anchor</a>
  <div class="anchored">I’m the anchored thing</div>
</div>
/* styles.css */
.container {
  position: relative;
}
.anchored {
  position: absolute;
}

Ces solutions ne sont souvent pas idéales. Ils nécessitent JavaScript ou introduisent un balisage supplémentaire. L'API CSS Positioning Anchor vise à résoudre ce problème en fournissant une API CSS pour les éléments d'ancrage. Il permet de positionner et de dimensionner un élément en fonction de la position et de la taille d'autres éléments.

Image montrant une maquette de fenêtre de navigateur détaillant l&#39;anatomie d&#39;une info-bulle.

Prise en charge des navigateurs

Vous pouvez tester l'API de positionnement des ancres CSS dans Chrome Canary avec le commutateur "Experimental Web Platform features" (Fonctionnalités expérimentales de la plate-forme Web). Pour activer cet indicateur, ouvrez Chrome Canary et accédez à chrome://flags. Activez ensuite le commutateur "Experimental Web Platform features".

L'équipe Oddbird développe également un polyfill. N'hésitez pas à consulter le dépôt sur github.com/oddbird/css-anchor-positioning.

Vous pouvez vérifier la prise en charge de l'ancrage avec:

@supports(anchor-name: --foo) {
  /* Styles... */
}

Notez que cette API est encore en phase expérimentale et qu'elle est susceptible d'être modifiée. Cet article présente les éléments importants dans les grandes lignes. L'implémentation actuelle n'est pas non plus complètement synchronisée avec la spécification du groupe de travail CSS.

Problème

Pourquoi devez-vous le faire ? Un cas d'utilisation important est la création d'info-bulles ou d'expériences semblables. Dans ce cas, vous souhaitez souvent associer l'info-bulle au contenu qu'elle référence. Il est souvent nécessaire de lier un élément à un autre. Vous vous attendez également à ce que l'interaction avec la page ne brise pas ce lien, par exemple si un utilisateur fait défiler ou redimensionne l'interface utilisateur.

Un autre problème se pose si vous souhaitez vous assurer que l'élément associé reste visible. Par exemple, si vous ouvrez une info-bulle et qu'elle est rognée par les limites de la vue. Ce n'est peut-être pas une expérience optimale pour les utilisateurs. Vous souhaitez que l'info-bulle s'adapte.

Solutions actuelles

Actuellement, il existe plusieurs façons d'aborder le problème.

Tout d'abord, l'approche rudimentaire consistant à "encapsuler l'ancre". Vous prenez les deux éléments et les encapsulez dans un conteneur. Vous pouvez ensuite utiliser position pour positionner l'info-bulle par rapport à l'ancre.

<div class="containing-block">
  <div class="tooltip">Anchor me!</div>
  <a class="anchor">The anchor</a>
</div>
.containing-block {
  position: relative;
}

.tooltip {
  position: absolute;
  bottom: calc(100% + 10px);
  left: 50%;
  transform: translateX(-50%);
}

Vous pouvez déplacer le conteneur, et tout restera à peu près à sa place.

Vous pouvez également utiliser une autre approche si vous connaissez la position de votre ancre ou si vous pouvez la suivre d'une manière ou d'une autre. Vous pouvez le transmettre à votre info-bulle avec des propriétés personnalisées.

<div class="tooltip">Anchor me!</div>
<a class="anchor">The anchor</a>
:root {
  --anchor-width: 120px;
  --anchor-top: 40vh;
  --anchor-left: 20vmin;
}

.anchor {
  position: absolute;
  top: var(--anchor-top);
  left: var(--anchor-left);
  width: var(--anchor-width);
}

.tooltip {
  position: absolute;
  top: calc(var(--anchor-top));
  left: calc((var(--anchor-width) * 0.5) + var(--anchor-left));
  transform: translate(-50%, calc(-100% - 10px));
}

Mais que faire si vous ne connaissez pas la position de votre ancre ? Vous devrez probablement intervenir avec JavaScript. Vous pouvez utiliser une approche similaire à celle du code suivant, mais cela signifie que vos styles commencent à s'échapper du CSS et à pénétrer dans JavaScript.

const setAnchorPosition = (anchored, anchor) => {
  const bounds = anchor.getBoundingClientRect().toJSON();
  for (const [key, value] of Object.entries(bounds)) {
    anchored.style.setProperty(`--${key}`, value);
  }
};

const update = () => {
  setAnchorPosition(
    document.querySelector('.tooltip'),
    document.querySelector('.anchor')
  );
};

window.addEventListener('resize', update);
document.addEventListener('DOMContentLoaded', update);

Cela soulève quelques questions:

  • Quand dois-je calculer les styles ?
  • Comment calculer les styles ?
  • À quelle fréquence calcule-t-on les styles ?

Le problème est-il résolu ? C'est peut-être le cas pour votre cas d'utilisation, mais il y a un problème: notre solution ne s'adapte pas. Il ne répond pas. Que se passe-t-il si mon élément ancré est coupé par le viewport ?

Vous devez maintenant décider si vous devez réagir et comment. Le nombre de questions et de décisions que vous devez prendre commence à augmenter. Tout ce que vous voulez faire est d'ancrer un élément à un autre. Dans un monde idéal, votre solution s'ajustera et réagira à son environnement.

Pour vous soulager, vous pouvez envisager une solution JavaScript. Cela entraînera le coût d'ajout d'une dépendance à votre projet, et cela pourrait entraîner des problèmes de performances en fonction de la façon dont vous les utilisez. Par exemple, certains packages utilisent requestAnimationFrame pour maintenir la position correcte. Vous et votre équipe devez donc vous familiariser avec le package et ses options de configuration. Par conséquent, vos questions et décisions ne seront peut-être pas réduites, mais modifiées. C'est l'une des raisons pour lesquelles le positionnement des ancres CSS est utile. Vous n'aurez plus à vous soucier des problèmes de performances lors du calcul de la position.

Voici à quoi pourrait ressembler le code pour utiliser floating-ui, un package populaire pour ce problème:

import {computePosition, flip, offset, autoUpdate} from 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.2.1/+esm';

const anchor = document.querySelector('.anchor')
const tooltip = document.querySelector('.tooltip')

const updatePosition = () => {  
  computePosition(anchor, tooltip, {
    placement: 'top',
    middleware: [offset(10), flip()]
  })
    .then(({x, y}) => {
      Object.assign(tooltip.style, {
        left: `${x}px`,
        top: `${y}px`
      })
  })
};

const clean = autoUpdate(anchor, tooltip, updatePosition);

Essayez de repositionner l'ancre dans cette démonstration qui utilise ce code.

Il est possible que la "info-bulle" ne se comporte pas comme prévu. Il réagit à la sortie de la fenêtre d'affichage sur l'axe Y, mais pas sur l'axe X. Consultez la documentation pour trouver une solution adaptée à vos besoins.

Cependant, trouver un package adapté à votre projet peut prendre beaucoup de temps. Cela implique des décisions supplémentaires et peut être frustrant si le résultat n'est pas tout à fait conforme à vos attentes.

Utiliser le positionnement par ancrage

Saisissez l'API de positionnement des ancres CSS. L'idée est de conserver vos styles dans votre CSS et de réduire le nombre de décisions que vous devez prendre. Vous espérez obtenir le même résultat, mais l'objectif est d'améliorer l'expérience des développeurs.

  • Aucun JavaScript requis.
  • Laissez le navigateur déterminer la meilleure position en fonction de vos indications.
  • Plus de dépendances tierces
  • Aucun élément wrapper.
  • Fonctionne avec les éléments de la couche supérieure.

Recréons le problème que nous essayions de résoudre ci-dessus et essayons de le résoudre. Utilisez plutôt l'analogie d'un bateau avec une ancre. Ils représentent l'élément ancré et l'ancre. L'eau représente le bloc contenant.

Vous devez d'abord choisir comment définir l'ancrage. Pour ce faire, définissez la propriété anchor-name sur l'élément d'ancrage dans votre fichier CSS. Il accepte une valeur ident-barré.

.anchor {
  anchor-name: --my-anchor;
}

Vous pouvez également définir une ancre dans votre code HTML à l'aide de l'attribut anchor. La valeur de l'attribut correspond à l'ID de l'élément d'ancrage. Cela crée une ancre implicite.

<a id="my-anchor" class="anchor"></a>
<div anchor="my-anchor" class="boat">I’m a boat!</div>

Une fois que vous avez défini une ancre, vous pouvez utiliser la fonction anchor. La fonction anchor prend en compte trois arguments:

  • Élément d'ancrage:anchor-name de l'ancre à utiliser. Vous pouvez également omettre la valeur pour utiliser une ancre implicit. Il peut être défini via la relation HTML ou avec une propriété anchor-default avec une valeur anchor-name.
  • Côté de l'ancre:mot clé de la position que vous souhaitez utiliser. Il peut s'agir de top, right, bottom, left, center, etc. Vous pouvez également transmettre un pourcentage. Par exemple, 50% correspond à center.
  • Fallback:valeur de remplacement facultative qui accepte une longueur ou un pourcentage.

Vous utilisez la fonction anchor comme valeur pour les propriétés d'encart (top, right, bottom, left ou leurs équivalents logiques) de l'élément ancré. Vous pouvez également utiliser la fonction anchor dans calc:

.boat {
  bottom: anchor(--my-anchor top);
  left: calc(anchor(--my-anchor center) - (var(--boat-size) * 0.5));
}

 /* alternative with anchor-default */
.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: calc(anchor(center) - (var(--boat-size) * 0.5));
}

Il n'existe pas de propriété d'encart center. Vous pouvez donc utiliser calc si vous connaissez la taille de votre élément ancré. Pourquoi ne pas utiliser translate ? Vous pouvez utiliser ceci:

.boat {
  anchor-default: --my-anchor;
  bottom: anchor(top);
  left: anchor(center);
  translate: -50% 0;
}

Toutefois, le navigateur ne tient pas compte des positions transformées pour les éléments ancrés. Vous comprendrez pourquoi cela est important lorsque vous examinerez les valeurs de remplacement de position et le positionnement automatique.

Vous avez peut-être remarqué l'utilisation de la propriété personnalisée --boat-size ci-dessus. Toutefois, si vous souhaitez baser la taille de l'élément ancré sur celle de l'ancre, vous pouvez également y accéder. Au lieu de le calculer vous-même, vous pouvez utiliser la fonction anchor-size. Par exemple, pour que notre bateau soit quatre fois plus large que notre ancre:

.boat {
  width: calc(4 * anchor-size(--my-anchor width));
}

Vous avez également accès à la hauteur avec anchor-size(--my-anchor height). Vous pouvez également l'utiliser pour définir la taille de l'une ou des deux axes.

Que faire si vous souhaitez ancrer un élément avec un positionnement absolute ? La règle est que les éléments ne peuvent pas être frères et sœurs. Dans ce cas, vous pouvez encapsuler l'ancrage avec un conteneur dont le positionnement est relative. Vous pouvez ensuite l'ancrer.

<div class="anchor-wrapper">
  <a id="my-anchor" class="anchor"></a>
</div>
<div class="boat">I’m a boat!</div>

Regardez cette démonstration dans laquelle vous pouvez faire glisser l'ancre et le bateau la suit.

Suivre la position de défilement

Dans certains cas, votre élément d'ancrage peut se trouver dans un conteneur à défilement. En même temps, votre élément ancré peut se trouver en dehors de ce conteneur. Étant donné que le défilement se produit sur un thread différent de la mise en page, vous devez trouver un moyen de le suivre. La propriété anchor-scroll peut le faire. Vous le définissez sur l'élément ancré et lui attribuez la valeur de l'ancre que vous souhaitez suivre.

.boat { anchor-scroll: --my-anchor; }

Essayez cette démonstration, dans laquelle vous pouvez activer et désactiver anchor-scroll à l'aide de la case à cocher en haut à droite.

.

L'analogie est un peu faible ici, car dans un monde idéal, votre bateau et votre ancre sont tous deux dans l'eau. De plus, des fonctionnalités telles que l'API Popover permettent de garder les éléments associés à proximité. Le positionnement des ancres fonctionne toutefois avec les éléments de la couche supérieure. C'est l'un des principaux avantages de l'API: pouvoir associer des éléments dans différents flux.

Prenons l'exemple de cette démonstration qui comporte un conteneur à défilement avec des ancres associées à des info-bulles. Les éléments d'info-bulle qui sont des pop-ups ne sont pas nécessairement situés à côté des ancres:

Vous remarquerez cependant que les pop-ups suivent leurs liens d'ancrage respectifs. Vous pouvez redimensionner ce conteneur à défilement, et les positions seront mises à jour automatiquement.

Position de remplacement et positionnement automatique

C'est là que le positionnement des ancrages passe à la vitesse supérieure. Un position-fallback peut positionner votre élément ancré en fonction d'un ensemble de solutions de remplacement que vous fournissez. Vous guidez le navigateur avec vos styles et laissez-le déterminer la position pour vous.

Le cas d'utilisation courant ici est une info-bulle qui doit s'afficher au-dessus ou en dessous d'une ancre. Ce comportement dépend de la question de savoir si l'info-bulle sera rognée par son conteneur. Ce conteneur est généralement la fenêtre d'affichage.

Si vous avez examiné le code de la dernière démonstration, vous avez vu qu'une propriété position-fallback était utilisée. Si vous avez fait défiler le conteneur, vous avez peut-être remarqué que ces popovers ancrés ont sauté. Cela se produisait lorsque leurs ancrages respectifs approchaient de la limite du viewport. À ce moment-là, les popovers tentent de s'ajuster pour rester dans la fenêtre d'affichage.

Avant de créer un position-fallback explicite, le positionnement des ancres propose également un positionnement automatique. Vous pouvez obtenir ce retournement sans frais en utilisant une valeur de auto à la fois dans la fonction d'ancrage et dans la propriété d'encart opposée. Par exemple, si vous utilisez anchor pour bottom, définissez top sur auto.

.tooltip {
  position: absolute;
  bottom: anchor(--my-anchor auto);
  top: auto;
}

L'alternative au positionnement automatique consiste à utiliser une position-fallback explicite. Pour ce faire, vous devez définir un ensemble de remplacement de position. Le navigateur les examine jusqu'à ce qu'il en trouve un qu'il peut utiliser, puis applique ce positionnement. Si aucun ne fonctionne, le premier défini est utilisé par défaut.

Un position-fallback qui tente d'afficher les info-bulles au-dessus, puis en dessous, peut se présenter comme suit:

@position-fallback --top-to-bottom {
  @try {
    bottom: anchor(top);
    left: anchor(center);
  }

  @try {
    top: anchor(bottom);
    left: anchor(center);
  }
}

Voici à quoi ressemble l'application de cette règle aux info-bulles:

.tooltip {
  anchor-default: --my-anchor;
  position-fallback: --top-to-bottom;
}

L'utilisation de anchor-default vous permet de réutiliser position-fallback pour d'autres éléments. Vous pouvez également utiliser une propriété personnalisée à portée limitée pour définir anchor-default.

Revenons à la démonstration du bateau. Un position-fallback est défini. Lorsque vous modifiez la position de l'ancre, le bateau s'ajuste pour rester dans le conteneur. Essayez également de modifier la valeur de marge intérieure, qui ajuste la marge intérieure du corps. Notez que le navigateur corrige le positionnement. Les positions sont modifiées en modifiant l'alignement de la grille du conteneur.

position-fallback est plus explicite cette fois-ci et tente les positions dans le sens des aiguilles d'une montre.

.boat {
  anchor-default: --my-anchor;
  position-fallback: --compass;
}

@position-fallback --compass {
  @try {
    bottom: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    right: anchor(left);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }
}


Exemples

Maintenant que vous avez une idée des principales fonctionnalités de positionnement des ancres, examinons quelques exemples intéressants au-delà des info-bulles. Ces exemples visent à vous donner des idées sur la façon dont vous pouvez utiliser le positionnement des repères. Le meilleur moyen de faire évoluer les spécifications est de recueillir les commentaires d'utilisateurs réels comme vous.

Menus contextuels

Commençons par un menu contextuel à l'aide de l'API Popover. L'idée est que lorsque vous cliquez sur le bouton avec le chevron, un menu contextuel s'affiche. Ce menu aura son propre menu à développer.

Ce n'est pas le balisage qui est important ici. Cependant, vous avez trois boutons qui utilisent chacun popovertarget. Vous avez ensuite trois éléments utilisant l'attribut popover. Vous pouvez ainsi ouvrir les menus contextuels sans JavaScript. Cela peut se présenter comme suit:

<button popovertarget="context">
  Toggle Menu
</button>        
<div popover="auto" id="context">
  <ul>
    <li><button>Save to your Liked Songs</button></li>
    <li>
      <button popovertarget="playlist">
        Add to Playlist
      </button>
    </li>
    <li>
      <button popovertarget="share">
        Share
      </button>
    </li>
  </ul>
</div>
<div popover="auto" id="share">...</div>
<div popover="auto" id="playlist">...</div>

Vous pouvez désormais définir un position-fallback et le partager entre les menus contextuels. Nous nous assurons également de ne pas définir de styles inset pour les popovers.

[popovertarget="share"] {
  anchor-name: --share;
}

[popovertarget="playlist"] {
  anchor-name: --playlist;
}

[popovertarget="context"] {
  anchor-name: --context;
}

#share {
  anchor-default: --share;
  position-fallback: --aligned;
}

#playlist {
  anchor-default: --playlist;
  position-fallback: --aligned;
}

#context {
  anchor-default: --context;
  position-fallback: --flip;
}

@position-fallback --aligned {
  @try {
    top: anchor(top);
    left: anchor(right);
  }

  @try {
    top: anchor(bottom);
    left: anchor(right);
  }

  @try {
    top: anchor(top);
    right: anchor(left);
  }

  @try {
    bottom: anchor(bottom);
    left: anchor(right);
  }

  @try {
    right: anchor(left);
    bottom: anchor(bottom);
  }
}

@position-fallback --flip {
  @try {
    bottom: anchor(top);
    left: anchor(left);
  }

  @try {
    right: anchor(right);
    bottom: anchor(top);
  }

  @try {
    top: anchor(bottom);
    left: anchor(left);
  }

  @try {
    top: anchor(bottom);
    right: anchor(right);
  }
}

Vous obtenez ainsi une UI de menu contextuel imbriqué adaptative. Essayez de modifier la position du contenu avec la sélection. L'option que vous choisissez met à jour l'alignement de la grille. Cela affecte le positionnement des popovers.

Se concentrer et suivre

Cette démonstration combine des primitives CSS en introduisant :has(). L'idée est de faire passer un indicateur visuel pour le input sélectionné.

Pour ce faire, définissez un nouvel ancrage au moment de l'exécution. Pour cette démonstration, une propriété personnalisée de portée est mise à jour lorsque l'utilisateur se concentre sur la zone de saisie.

#email {
    anchor-name: --email;
  }
  #name {
    anchor-name: --name;
  }
  #password {
    anchor-name: --password;
  }
:root:has(#email:focus) {
    --active-anchor: --email;
  }
  :root:has(#name:focus) {
    --active-anchor: --name;
  }
  :root:has(#password:focus) {
    --active-anchor: --password;
  }

:root {
    --active-anchor: --name;
    --active-left: anchor(var(--active-anchor) right);
    --active-top: calc(
      anchor(var(--active-anchor) top) +
        (
          (
              anchor(var(--active-anchor) bottom) -
                anchor(var(--active-anchor) top)
            ) * 0.5
        )
    );
  }
.form-indicator {
    left: var(--active-left);
    top: var(--active-top);
    transition: all 0.2s;
}

Mais comment pourriez-vous aller plus loin ? Vous pouvez l'utiliser pour une superposition pédagogique. Une info-bulle peut se déplacer entre les points d'intérêt et mettre à jour son contenu. Vous pouvez utiliser un fondu croisé. Des animations discrètes vous permettant d'animer display ou les transitions de vue peuvent être utiles ici.

.

Calcul du graphique à barres

Vous pouvez également combiner le positionnement des ancrages avec calc. Imaginez un graphique avec des pop-ups qui l'animent.

Vous pouvez suivre les valeurs les plus élevées et les plus faibles à l'aide des CSS min et max. Le code CSS correspondant pourrait se présenter comme suit:

.chart__tooltip--max {
    left: anchor(--chart right);
    bottom: max(
      anchor(--anchor-1 top),
      anchor(--anchor-2 top),
      anchor(--anchor-3 top)
    );
    translate: 0 50%;
  }

Du code JavaScript est utilisé pour mettre à jour les valeurs du graphique et du CSS pour styliser le graphique. Toutefois, le positionnement des ancres s'occupe des mises à jour de la mise en page.

Poignées de redimensionnement

Vous n'êtes pas obligé d'ancrer l'élément à un seul élément. Vous pouvez utiliser plusieurs ancres pour un élément. Vous l'avez peut-être remarqué dans l'exemple de graphique à barres. Les info-bulles étaient ancrées au graphique, puis à la barre appropriée. Si vous poussiez ce concept un peu plus loin, vous pourriez l'utiliser pour redimensionner des éléments.

Vous pouvez traiter les points d'ancrage comme des poignées de redimensionnement personnalisées et utiliser une valeur inset.

.container {
   position: absolute;
   inset:
     anchor(--handle-1 top)
     anchor(--handle-2 right)
     anchor(--handle-2 bottom)
     anchor(--handle-1 left);
 }

Dans cette démonstration, GreenSock Draggable rend les poignées déplaçables. Toutefois, l'élément <img> se redimensionne pour remplir le conteneur qui s'ajuste pour combler l'espace entre les poignées.

Un SelectMenu ?

Ce dernier est un peu une mise en bouche de ce qui vous attend. Vous pouvez toutefois créer un popover pouvant être sélectionné et vous disposez désormais d'un positionnement d'ancrage. Vous pouvez créer les bases d'un élément <select> stylable.

<div class="select-menu">
<button popovertarget="listbox">
 Select option
 <svg>...</svg>
</button>
<div popover="auto" id="listbox">
   <option>A</option>
   <option>Styled</option>
   <option>Select</option>
</div>
</div>

Un anchor implicite facilite cette tâche. Toutefois, le CSS d'un point de départ rudimentaire peut se présenter comme suit:

[popovertarget] {
 anchor-name: --select-button;
}
[popover] {
  anchor-default: --select-button;
  top: anchor(bottom);
  width: anchor-size(width);
  left: anchor(left);
}

Combinez les fonctionnalités de l'API Popover avec le positionnement des ancres CSS, et vous êtes presque prêt.

C'est pratique lorsque vous commencez à utiliser des éléments comme :has(). Vous pouvez faire pivoter le repère lorsqu'il est ouvert:

.select-menu:has(:open) svg {
  rotate: 180deg;
}

Où pourriez-vous l'emmener ensuite ? Que nous faut-il d'autre pour que select fonctionne ? Nous verrons cela dans un prochain article. Mais ne vous inquiétez pas, des éléments de sélection stylables seront bientôt disponibles. Tenez-vous informé !


Et voilà !

La plate-forme Web évolue. Le positionnement des ancres CSS est essentiel pour améliorer la façon dont vous développez les commandes d'interface utilisateur. Vous n'aurez plus à prendre certaines de ces décisions difficiles. Mais cela vous permettra également de faire des choses que vous n'auriez jamais pu faire auparavant. Par exemple, pour styliser un élément <select>. Faites-nous part de vos impressions

Photo de CHUTTERSNAP, publiée sur Unsplash