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

Comment partagez-vous actuellement la connexion entre deux éléments ? Vous pouvez essayer de suivre leur position 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 CSS Anchor Positioning permet de résoudre ce problème en fournissant une API CSS pour le partage de connexion. Elle fournit un moyen 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 une fenêtre de navigateur de maquette détaillant l&#39;anatomie d&#39;une info-bulle.

Prise en charge des navigateurs

Vous pouvez essayer l'API de positionnement d'ancrage CSS dans Chrome Canary en arrière-plan des fonctionnalités expérimentales de la plate-forme Web. . Pour activer cet indicateur, ouvrez Chrome Canary et accédez à chrome://flags. Activez ensuite "Fonctionnalités expérimentales de la plate-forme Web" .

Un polyfill est en cours de développement par l'équipe d'Oddbird. N'oubliez pas de consulter le dépôt à l'adresse github.com/oddbird/css-anchor-positioning.

Vous pouvez vérifier si l'ancrage est pris en charge avec:

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

Notez que cette API est encore en phase de test et qu'elle est susceptible d'être modifiée. Cet article couvre les parties importantes dans les grandes lignes. Par ailleurs, la mise en œuvre actuelle n'est pas complètement conforme à la spécification du groupe de travail CSS.

Problème

Pourquoi procéder ainsi ? La création d'info-bulles ou d'expériences de ce type seraient un cas d'utilisation important. Dans ce cas, vous souhaiterez souvent partager la connexion de l'info-bulle avec le contenu auquel elle fait référence. Il est souvent nécessaire d'avoir un moyen de partager la connexion d'un élément vers un autre. Vous vous attendez également à ce que l'interaction avec la page n'interrompe pas ce partage de connexion, par exemple lorsqu'un utilisateur fait défiler ou redimensionne l'interface utilisateur.

Un autre problème se pose : vous voulez vous assurer que l'élément partagé reste visible (par exemple, si vous ouvrez une info-bulle et qu'il est tronqué par les limites de la fenêtre d'affichage). Cette expérience risque de ne pas être optimale pour les utilisateurs. Vous souhaitez adapter l'info-bulle.

Solutions actuelles

Actuellement, il existe différentes manières d'aborder le problème.

Commençons par le rudimentaire "Wrap the Anchor" (Encapsuler l'ancre) approche. 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%);
}

Vous pouvez déplacer le conteneur pour que tout reste à l'emplacement souhaité pour la plupart.

Une autre approche consiste à connaître la position de votre ancre ou à 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 faire quelque chose comme le code suivant, mais à présent, cela signifie que vos styles commencent à être transférés 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 ?

Le problème est-il résolu ? Cela pourrait pour votre cas d'utilisation, mais il y a un problème: notre solution ne s'adapte pas. Il n'est pas réactif. 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 à cette situation et de quelle manière. 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 atténuer une partie de ces difficultés, vous pourriez avoir recours à une solution JavaScript. Cela entraînerait le coût de l'ajout d'une dépendance à votre projet et pourrait entraîner des problèmes de performances selon la façon dont vous les utilisez. Par exemple, certains packages utilisent requestAnimationFrame pour maintenir la position correcte. Cela signifie que vous et votre équipe devez vous familiariser avec le package et ses options de configuration. Par conséquent, il se peut que vos questions et décisions ne soient pas réduites, mais qu'elles soient modifiées. Cela fait partie du « pourquoi » pour le positionnement de l'ancre CSS. Cela vous évite de penser aux problèmes de performances lors du calcul de la position.

Voici à quoi pourrait ressembler le code avec floating-ui, un package couramment utilisé pour résoudre 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.

L'info-bulle pourrait ne pas se comporter comme vous le souhaitez. Elle réagit lorsqu'on sort de la fenêtre d'affichage sur l'axe Y, mais pas sur l'axe X. Consultez la documentation pour trouver la solution adaptée à vos besoins.

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

Utiliser le positionnement de l'ancre

Saisissez l'API de positionnement d'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 le but est d'améliorer l'expérience des développeurs.

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

Recréons et attaquons le problème que nous tentions de résoudre ci-dessus. Reprenons 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 les contient.

Tout d'abord, vous devez choisir comment définir l'ancre. Pour ce faire, définissez la propriété anchor-name sur l'élément d'ancrage dans votre CSS. 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 correspond à l'identifiant 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 utilise trois arguments:

  • Élément d'ancrage:anchor-name de l'ancre à utiliser. Sinon, vous pouvez 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é de l'ancre: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% est égal à center.
  • Valeur de remplacement:il s'agit d'une valeur de remplacement facultative qui accepte une longueur ou un pourcentage.

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));
}

Comme il n'existe pas de propriété d'encart center, vous pouvez 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 des éléments ancrés. Vous comprendrez mieux pourquoi c'est important lorsque vous envisagez d'utiliser des valeurs de remplacement de position et de positionnement automatique.

Vous avez peut-être remarqué que vous avez utilisé 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 accéder à cette taille. Au lieu de le calculer vous-même, vous pouvez utiliser la fonction anchor-size. Par exemple, pour que notre bateau soit quatre fois la largeur de notre 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 des axes ou des deux.

Que se passe-t-il si vous souhaitez ancrer l'élément à 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 dont le positionnement est relative. Vous pouvez ensuite vous y ancrer.

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

Dans cette démonstration, faites glisser l'ancre et le bateau suivra.

Suivi de la position de défilement

Dans certains cas, l'élément d'ancrage peut se trouver dans un conteneur de 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 la mise en page, vous devez pouvoir le suivre. C'est possible avec la propriété anchor-scroll. Vous la définissez sur l'élément ancré et lui donnez la valeur de l'ancre que vous souhaitez suivre.

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

Dans cette démonstration, vous pouvez activer et désactiver anchor-scroll à l'aide de la case à cocher située dans l'angle.

L'analogie est cependant un peu plate, 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'ancre fonctionnera 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 la connexion des éléments dans différents flux.

Prenons cette démonstration qui comporte un conteneur à défilement avec des ancres et des info-bulles. Il est possible que les éléments d'info-bulle qui sont des pop-overs ne se trouvent pas au même endroit que les ancres:

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

Positionnement et remplacement de position

C'est là que le pouvoir de positionnement de l'ancre augmente d'un niveau. Un position-fallback peut positionner votre élément ancré en fonction d'un ensemble de valeurs 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 basculer entre l'affichage au-dessus et en dessous d'une ancre. Ce comportement est basé sur le fait que l'info-bulle serait tronquée ou non par son conteneur. Ce conteneur correspond généralement à la fenêtre d'affichage.

En explorant plus en détail le code de la dernière démonstration, vous auriez pu constater qu'une propriété position-fallback était utilisée. Si vous avez fait défiler le conteneur, vous avez peut-être remarqué que ces pop-ups ancrés ont sauté. Cela s'est produit lorsque leurs ancres respectives s'approchaient des limites de la fenêtre d'affichage. À ce stade, les pop-overs tentent de s'ajuster pour rester dans la fenêtre d'affichage.

Avant de créer une position-fallback explicite, le positionnement de l'ancre offre également un positionnement automatique. Vous pouvez effectuer cette opération 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 un position-fallback explicite. Pour cela, vous devez définir un ensemble de positions de remplacement. Le navigateur les examine jusqu'à en trouver un qu'il peut utiliser, puis applique ce positionnement. S'il n'en trouve pas, la première définie est définie par défaut.

Voici à quoi pourrait ressembler une position-fallback qui tente d'afficher les info-bulles au-dessus et en dessous:

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

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

Si vous appliquez cela aux info-bulles, procédez 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 délimitée pour définir anchor-default.

Considérons cette démonstration en utilisant à nouveau le 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 la marge intérieure pour ajuster la marge intérieure du corps. Notez la façon dont le navigateur corrige le positionnement. Les positions sont modifiées en modifiant l'alignement de la grille du conteneur.

Cette fois, le position-fallback est plus détaillé en essayant de le positionner 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 connaissez les principales fonctionnalités de positionnement des éléments d'ancrage, examinons d'autres exemples intéressants que les info-bulles. L'objectif de ces exemples est de vous aider à trouver des moyens d'utiliser le positionnement d'ancrage. Le meilleur moyen d'aller plus loin est d'obtenir des commentaires d'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 fait apparaître un menu contextuel. Et 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 qui utilisent l'attribut popover. Vous pouvez ainsi ouvrir les menus contextuels sans JavaScript. Cela pourrait 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 maintenant définir une position-fallback et la partager entre les menus contextuels. Nous veillons également à désactiver 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 adaptative de menu contextuel imbriqué. Essayez de modifier la position du contenu à l'aide de la touche "Sélectionner". L'option que vous choisissez met à jour l'alignement de la grille. Cela affecte la façon dont le positionnement de l'ancre positionne les pop-ups.

Concentrez-vous et suivez

Cette démonstration combine les primitives CSS en intégrant :has(). L'idée est de changer d'indicateur visuel pour le input ciblé.

Pour ce faire, définissez une nouvelle ancre au moment de l'exécution. Pour cette démonstration, une propriété personnalisée délimitée est mise à jour lors du ciblage des entrées.

#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 aller plus loin ? Vous pouvez l'utiliser sous forme d'instruction en superposition. Une info-bulle peut se déplacer entre les points d'intérêt et en modifier le contenu. Vous pouvez effectuer un fondu enchaîné. Les animations discrètes vous permettant d'animer display ou d'afficher des transitions peuvent fonctionner ici.

Calcul du graphique à barres

Une autre chose amusante que vous pouvez faire avec le positionnement de l'ancre consiste à le combiner avec calc. Imaginez un graphique dans lequel des pop-overs l'annotent.

Vous pouvez suivre les valeurs la plus élevée et la plus basse à l'aide des CSS min et max. Voici à quoi pourrait ressembler le code CSS correspondant:

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

Vous devez utiliser du code JavaScript pour mettre à jour les valeurs du graphique et du code CSS pour appliquer un style au graphique. Mais le positionnement de l'ancre gère automatiquement les mises à jour de la mise en page.

Redimensionner les poignées

Il n'est pas nécessaire de l'ancrer à un seul élément. Vous pouvez utiliser plusieurs 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 pouvez 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. Cependant, l'élément <img> est redimensionné pour remplir le conteneur, qui s'ajuste pour remplir l'espace entre les poignées.

Un SelectMenu?

Cette dernière vidéo sert à présenter les nouveautés à venir. Toutefois, vous pouvez créer une fenêtre pop-up sélectionnable et disposer maintenant du positionnement de l'ancre. Vous pouvez créer les bases d'un élément <select> personnalisables.

<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>

Une anchor implicite facilitera le processus. Toutefois, le CSS d'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'ancre CSS. Vous y êtes presque.

C'est une bonne idée de présenter des produits comme :has(). Vous pouvez faire pivoter le repère lorsque l'état est ouvert:

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

Que pouvez-vous faire ensuite ? De quoi d'autre avons-nous besoin pour en faire un select fonctionnel ? Nous conserverons cela pour le prochain article. Mais ne vous inquiétez pas : les éléments sélectionnés personnalisables arrivent bientôt. Tenez-vous informé !


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'avez jamais pu faire auparavant. Par exemple, vous pouvez appliquer un style à un élément <select>. Faites-nous part de vos impressions

Photo de CHUTTERSNAP sur Unsplash