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

Comment faites-vous actuellement la connexion d'un élément à un autre ? Vous pouvez essayer de suivre leurs positions ou utiliser une forme d'é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. Elles ont besoin de JavaScript ou introduisent un balisage supplémentaire. L'API de positionnement de l'ancrage CSS permet de résoudre ce problème en fournissant une API CSS pour le partage de connexion. Elle permet de positionner et de dimensionner un élément en fonction de la position et de la taille des autres éléments.

L&#39;image montre un modèle de fenêtre de navigateur détaillant la structure d&#39;une info-bulle.

Prise en charge des navigateurs

Dans Chrome Canary, vous pouvez tester l'API de positionnement de l'ancrage CSS avec l'indicateur "Fonctionnalités expérimentales de la plate-forme Web". Pour activer cet indicateur, ouvrez Chrome Canary et accédez à chrome://flags. Activez ensuite l'indicateur "Fonctionnalités expérimentales de la plate-forme Web".

L'équipe d'Oddbird a également développé un polyfill. Veillez à le consulter à l'adresse github.com/oddbird/css-anchor-positioning.

Vous pouvez vérifier la compatibilité de l'ancrage avec:

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

Notez que cette API est encore en phase de test et est susceptible d'être modifiée. Cet article aborde les parties importantes de manière générale. L'implémentation actuelle n'est pas non plus entièrement synchronisée avec les spécifications du CSS Working Group.

Problème

Pourquoi procéder ainsi ? Par exemple, vous pouvez créer des info-bulles ou des expériences semblables à des info-bulles. Dans ce cas, il est souvent souhaitable de partager l'info-bulle avec le contenu auquel elle fait référence. Il est souvent nécessaire de trouver un moyen de partager la connexion d'un élément à un autre. Vous vous attendez également à ce que l'interaction avec la page ne rompt pas ce partage de connexion, par exemple si un utilisateur fait défiler ou redimensionne l'interface utilisateur.

Vous voulez également vous assurer que l'élément partagé reste visible (par exemple, si vous ouvrez une info-bulle et qu'elle est tronquée par les limites de la fenêtre d'affichage). Cela pourrait ne pas être une bonne expérience pour les utilisateurs. Vous voulez que l'info-bulle s'adapte.

Solutions actuelles

Il existe actuellement plusieurs façons d'aborder le problème.

Commençons par l'approche rudimentaire "encapsuler l'ancre". Vous prenez les deux éléments et vous 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%);
}

Si vous déplacez le conteneur, tout restera là où vous le souhaitez, en grande partie.

Vous pouvez aussi connaître la position de l'ancre ou la suivre d'une manière ou d'une autre. Vous pouvez les 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 l'ancre ? Vous devrez probablement intervenir avec JavaScript. Vous pourriez, par exemple, faire quelque chose comme le code suivant, mais cela signifie que vos styles commencent à fuir du CSS vers 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 commence à poser quelques questions:

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

Cela résout-il le problème ? Cela pourrait convenir à votre cas d'utilisation, mais il y a un problème: notre solution ne s'adapte pas à votre situation. Elle n'est pas réactive. Que se passe-t-il si mon élément ancré est tronqué par la fenêtre d'affichage ?

Vous devez maintenant décider si vous allez 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'adaptera et réagira à son environnement.

Pour y remédier, vous pouvez envisager de recourir à une solution JavaScript. Cela entraînera le coût de l’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 conserver 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 vos décisions peuvent ne pas être réduites, mais plutôt modifiées. Cela fait partie de l'intérêt du positionnement de l'ancrage CSS. Cela vous évitera de vous préoccuper des problèmes de performances lors du calcul de la position.

Voici à quoi pourrait ressembler le code si vous utilisez 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 l'info-bulle ne se comporte pas comme prévu. Elle réagit lorsque l'utilisateur quitte la fenêtre d'affichage sur l'axe Y, mais pas sur l'axe X. Consultez la documentation. Vous y trouverez probablement une solution adaptée à vos besoins.

Mais trouver un package qui fonctionne pour votre projet peut prendre beaucoup de temps. Ce sont des décisions supplémentaires qui peuvent être frustrantes si cela ne fonctionne pas vraiment comme vous le souhaitez.

Utiliser le positionnement de l'ancrage

Accédez à l'API de positionnement de l'ancrage 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 notre objectif est d'améliorer l'expérience des développeurs.

  • JavaScript n'est pas nécessaire.
  • Laissez le navigateur déterminer la meilleure position possible en fonction de vos indications.
  • Plus de dépendances tierces
  • Aucun élément de wrapper.
  • Fonctionne avec les éléments qui se trouvent dans la couche supérieure.

Reprenons à présent et attaquons le problème que nous avons essayé de résoudre ci-dessus. Utilisez plutôt l'analogie d'un bateau avec une ancre. Ils représentent l'élément ancré et l'ancrage. L'eau représente le bloc qui le contient.

Vous devez d'abord choisir comment définir l'ancre. Vous pouvez le faire dans votre CSS en définissant la propriété anchor-name sur l'élément d'ancrage. Il accepte une valeur dashed-ident.

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

Vous pouvez également définir une ancre dans votre code HTML avec l'attribut anchor. La valeur de l'attribut est 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 comporte trois arguments:

  • Élément ancré:anchor-name de l'ancre à utiliser. Vous pouvez également omettre la valeur pour utiliser une ancre implicit. Elle peut être définie via la relation HTML ou avec une propriété anchor-default avec une valeur anchor-name.
  • Côté ancré:mot clé correspondant à 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% équivaut à center.
  • Valeur de remplacement:il s'agit d'une valeur de remplacement facultative qui accepte une durée ou un pourcentage.

Utilisez la fonction anchor en tant que 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'y a 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 pourriez utiliser ceci:

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

Toutefois, le navigateur ne prend pas en compte la transformation des positions pour les éléments ancrés. Vous comprendrez clairement pourquoi c'est important lorsque vous envisagez d'utiliser des positions de remplacement et un positionnement automatique.

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

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

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

Comment procéder 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. Dans ce cas, vous pouvez encapsuler l'ancre avec un conteneur ayant un positionnement 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 : vous pouvez faire glisser l'ancre pour que le bateau suive.

Suivi de la position de défilement

Dans certains cas, votre élément d'ancrage peut se trouver dans un conteneur à défilement. Dans le même temps, l'élément ancré peut se trouver en dehors de ce conteneur. Étant donné que le défilement s'effectue sur un thread différent de celui de la mise en page, vous avez besoin d'un moyen de le suivre. La propriété anchor-scroll permet d'effectuer cette opération. Vous le définissez sur l'élément ancré et lui donnez la valeur de l'ancre que vous souhaitez suivre.

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

Suivez cette démonstration où vous pouvez activer ou désactiver anchor-scroll à l'aide de la case à cocher située dans l'angle.

L'analogie semble un peu plate, ici, comme 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 favorisent la possibilité de garder les éléments associés à proximité. Toutefois, le positionnement de l'ancrage fonctionne avec les éléments qui se trouvent dans la couche supérieure. C'est l'un des principaux avantages de l'API: la possibilité de partager des éléments dans différents flux.

Prenons l'exemple de cette démonstration qui présente un conteneur à défilement avec des ancres avec des info-bulles. Les éléments d'info-bulle qui sont des pop-ups peuvent ne pas être colocalisés avec les ancrages:

Mais vous remarquerez que les fenêtres pop-up suivent leurs liens d'ancrage respectifs. Vous pouvez redimensionner ce conteneur de défilement. Les positions sont alors mises à jour.

Positionnement de la création de remplacement et du positionnement automatique

C'est là que la puissance de positionnement de l'ancre augmente d'un niveau. Un position-fallback peut positionner votre élément ancré en fonction d'un ensemble d'éléments de remplacement que vous fournissez. Vous guidez le navigateur avec vos styles et le laissez déterminer la position pour vous.

Le cas d'utilisation courant ici est une info-bulle qui doit s'afficher à tour de rôle au-dessus ou en dessous d'une ancre. Ce comportement est déterminé selon que l'info-bulle est tronquée par son conteneur ou non. Ce conteneur est généralement la fenêtre d'affichage.

En parcourant le code de la dernière démonstration, vous avez peut-être constaté qu'une propriété position-fallback était en cours d'utilisation. Si vous avez fait défiler le conteneur, vous avez peut-être remarqué que les pop-overs ancrés ont sauté. Cela se produisait lorsque leurs ancres respectives s'approchaient des limites de la fenêtre d'affichage. À ce stade, les fenêtres pop-up essaient de s'ajuster pour rester dans la fenêtre d'affichage.

Avant de créer un position-fallback explicite, le positionnement de l'ancrage propose également un positionnement automatique. Vous pouvez effectuer 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;
}

Plutôt que d'utiliser le positionnement automatique, vous pouvez utiliser un élément position-fallback explicite. Pour cela, vous devez définir un ensemble de position de remplacement. Le navigateur les consulte jusqu'à ce qu'il en trouve un qui peut être utilisé, puis applique ce positionnement. S'il n'en trouve pas qui fonctionne, il utilise par défaut le premier défini.

Un position-fallback qui tente d'afficher les info-bulles ci-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);
  }
}

L'application de cette fonction aux info-bulles se présente comme suit:

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

L'utilisation de anchor-default signifie que vous pouvez réutiliser position-fallback pour d'autres éléments. Vous pouvez également utiliser une propriété personnalisée dont le champ d'application est défini pour définir anchor-default.

Regardez à nouveau cette démonstration en utilisant le bateau. Il y a un ensemble position-fallback. Lorsque vous modifiez la position de l'ancre, le bateau s'ajuste pour rester dans le conteneur. Essayez également de modifier la valeur de la marge intérieure, ce 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 détaillé cette fois lorsque vous tentez d'effectuer des 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 caractéristiques du positionnement d'ancrage, examinons quelques exemples intéressants au-delà des info-bulles. Ces exemples visent à faire germer vos idées sur les façons d'utiliser le positionnement d'ancrage. Le meilleur moyen d'aller plus loin dans les spécifications est de faire appel à des utilisateurs réels comme vous.

Menus contextuels

Commençons par un menu contextuel utilisant l'API Popover. L'idée est que cliquer sur le bouton avec le chevron permet d'afficher un menu contextuel. Ce menu aura son propre menu à développer.

Le balisage n'est pas la partie importante ici. Cependant, vous avez trois boutons utilisant chacun popovertarget. Vous avez ensuite trois éléments utilisant l'attribut popover. Cela vous permet d'ouvrir les menus contextuels sans JavaScript. Cela pourrait ressembler à ceci:

<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 maintenant définir un position-fallback et le partager entre les menus contextuels. Nous veillez également à désactiver tous les styles inset pour les pop-overs.

[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 disposez ainsi d'une interface utilisateur de menu contextuel imbriqué adaptatif. Essayez de changer la position du contenu à l'aide de la sélection. L'option que vous choisissez modifie l'alignement de la grille. Cela affecte la façon dont le positionnement de l'ancrage positionne les pop-overs.

Se concentrer et suivre

Cette démonstration combine les primitives CSS en ajoutant :has(). L'idée est de faire passer un indicateur visuel au input actif.

Pour ce faire, définissez une nouvelle ancre au moment de l'exécution. Dans cette démonstration, une propriété personnalisée dont le champ d'application est défini est mise à jour sur le ciblage des entrées utilisateur.

#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 superposer des instructions. Une info-bulle peut se déplacer d'un point d'intérêt à un autre et mettre à jour son contenu. Vous pourriez effectuer un fondu enchaîné sur le contenu. Les animations discrètes vous permettant d'animer display ou d'afficher les transitions peuvent fonctionner ici.

Calcul du graphique à barres

Vous pouvez également utiliser le positionnement de l'ancre avec calc. Imaginez un graphique dans lequel vous avez des fenêtres contextuelles qui l'annotent.

Vous pouvez suivre les valeurs la plus élevée et la plus basse à l'aide des CSS min et max. Le CSS pour cela peut 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 code CSS est utilisé pour le styliser. Mais le positionnement de l'ancrage se charge des mises à jour de la mise en page.

Poignées de redimensionnement

Vous n'avez pas besoin de vous ancrer à un seul élément. Vous pouvez utiliser de nombreuses ancrages pour un élément. Vous avez peut-être remarqué cela dans l'exemple du graphique à barres. Les info-bulles étaient ancrées au graphique, puis à la barre appropriée. Si vous avez poussé 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> est redimensionné pour remplir le conteneur qui s'ajuste pour combler l'espace entre les poignées.

Un SelectMenu ?

Ce dernier n'est qu'un aperçu de ce qui vous attend. Toutefois, vous pouvez créer un pop-up sélectionnable et vous disposez désormais d'un positionnement d'ancrage. Vous pouvez créer les bases d'un élément <select> stylisé.

<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 opération. Toutefois, le CSS pour un point de départ rudimentaire pourrait 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 de l'ancrage CSS et vous y êtes presque.

C'est pratique quand vous commencez à présenter des éléments comme :has(). Vous pouvez faire pivoter le repère lorsque l'annonce est ouverte:

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

Que pourriez-vous faire ensuite ? De quoi d'autre avons-nous besoin pour en faire un select fonctionnel ? Nous garderons cela pour l'article suivant. Mais ne vous inquiétez pas : des éléments de sélection personnalisables seront bientôt disponibles. mais revenez plus tard !


Et voilà !

La plate-forme Web évolue. Le positionnement de l'ancrage CSS est essentiel pour améliorer la façon dont vous développez les commandes de l'interface utilisateur. Cela vous épargnera certaines de ces décisions délicates. Mais elle vous permettra également de faire des choses que vous n'auriez jamais pu faire auparavant. Par exemple, vous pouvez appliquer un style à un élément <select>. Donnez-nous votre avis.

Photo par CHUTTERSNAP, publiée sur Unsplash