Verbind elementen aan elkaar met CSS-ankerpositionering

Hoe koppel je momenteel het ene element aan het andere? U kunt proberen hun posities te volgen, of een vorm van wrapper-element gebruiken.

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

Deze oplossingen zijn vaak niet ideaal. Ze hebben JavaScript nodig of introduceren extra markup. De CSS-ankerpositionerings-API heeft tot doel dit op te lossen door een CSS-API te bieden voor het tetheren van elementen. Het biedt een manier om één element te positioneren en te rangschikken op basis van de positie en grootte van andere elementen.

Afbeelding toont een mockup-browservenster met details over de anatomie van een tooltip.

Browser-ondersteuning

U kunt de CSS-ankerpositionerings-API uitproberen in Chrome Canary achter de vlag 'Experimentele webplatformfuncties'. Om die vlag in te schakelen, opent u Chrome Canary en gaat u naar chrome://flags . Schakel vervolgens de vlag 'Experimentele webplatformfuncties' in.

Er is ook een polyfill in ontwikkeling door het team van Oddbird. Zorg ervoor dat je de repository op github.com/oddbird/css-anchor-positioning controleert.

U kunt controleren op verankeringsondersteuning met:

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

Houd er rekening mee dat deze API zich nog in een experimentele fase bevindt en kan veranderen. Dit artikel behandelt de belangrijke onderdelen op een hoog niveau. De huidige implementatie is ook niet volledig synchroon met de CSS Working Group-specificatie .

Het probleem

Waarom zou je dit moeten doen? Een prominente use case zou het creëren van tooltips of tooltip-achtige ervaringen zijn. In dat geval wilt u de tooltip vaak koppelen aan de inhoud waarnaar deze verwijst. Er is vaak behoefte aan een manier om een ​​element aan een ander element te koppelen. Je verwacht ook dat de interactie met de pagina de verbinding niet verbreekt, bijvoorbeeld als een gebruiker scrollt of de grootte van de gebruikersinterface wijzigt.

Een ander deel van het probleem doet zich voor als u er zeker van wilt zijn dat het gekoppelde element in beeld blijft, bijvoorbeeld als u knopinfo opent en deze wordt afgekapt door de grenzen van het venster. Dit is misschien geen geweldige ervaring voor gebruikers. U wilt dat de tooltip wordt aangepast.

Huidige oplossingen

Momenteel zijn er verschillende manieren waarop u het probleem kunt aanpakken.

Ten eerste is er de rudimentaire 'Wrap the anchor'-benadering. Je neemt beide elementen en wikkelt ze in een container. Vervolgens kunt u position gebruiken om de tooltip ten opzichte van het anker te positioneren.

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

Je kunt de container verplaatsen en alles blijft grotendeels staan ​​waar je het hebben wilt.

Een andere aanpak zou kunnen zijn als u de positie van uw anker kent of als u het op de een of andere manier kunt volgen. U kunt dit doorgeven aan uw tooltip met aangepaste eigenschappen.

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

Maar wat als u de positie van uw anker niet kent? Waarschijnlijk moet u ingrijpen met JavaScript. Je zou zoiets kunnen doen als de volgende code, maar nu betekent dit dat je stijlen uit CSS en in JavaScript beginnen te lekken.

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

Dit begint enkele vragen op te roepen:

  • Wanneer bereken ik de stijlen?
  • Hoe bereken ik de stijlen?
  • Hoe vaak bereken ik de stijlen?

Lost dat het op? Voor jouw gebruiksscenario misschien wel, maar er is één probleem: onze oplossing past zich niet aan. Het reageert niet. Wat moet ik doen als mijn verankerde element wordt afgesneden door de viewport?

Nu moet u beslissen of u hierop wilt reageren en hoe. Het aantal vragen en beslissingen dat u moet nemen begint te groeien. Het enige dat u wilt doen, is het ene element aan het andere verankeren. En in een ideale wereld zal uw oplossing zich aanpassen aan en reageren op de omgeving.

Om een ​​deel van die pijn te verzachten, kunt u een JavaScript-oplossing zoeken om u te helpen. Dat brengt de kosten met zich mee van het toevoegen van een afhankelijkheid aan uw project, en het kan prestatieproblemen veroorzaken, afhankelijk van hoe u ze gebruikt. Sommige pakketten gebruiken bijvoorbeeld requestAnimationFrame om de positie correct te houden. Dit betekent dat u en uw team vertrouwd moeten raken met het pakket en de configuratieopties ervan. Als gevolg hiervan worden uw vragen en beslissingen mogelijk niet verkleind, maar gewijzigd. Dit maakt deel uit van het ‘waarom’ voor CSS-ankerpositionering. Het zal u ervan weerhouden na te denken over prestatieproblemen bij het berekenen van uw positie.

Hier ziet u hoe de code eruit zou kunnen zien voor het gebruik van " floating-ui ", een populair pakket voor dit probleem:

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

Probeer het anker in deze demo dat die code gebruikt, opnieuw te positioneren.

De "tooltip" gedraagt ​​zich mogelijk niet zoals u verwacht. Het reageert op het buiten de viewport gaan op de y-as, maar niet op de x-as. Duik in de documentatie en u zult waarschijnlijk een oplossing vinden die voor u werkt.

Maar het kan veel tijd kosten om een ​​pakket te vinden dat voor uw project werkt. Het zijn extra beslissingen en het kan frustrerend zijn als het niet helemaal werkt zoals je wilt.

Ankerpositionering gebruiken

Voer de CSS-ankerpositionerings-API in. Het idee is om uw stijlen in uw CSS te behouden en het aantal beslissingen dat u moet nemen te verminderen. U hoopt hetzelfde resultaat te bereiken, maar het doel is om de ontwikkelaarservaring te verbeteren.

  • Geen JavaScript vereist.
  • Laat de browser op basis van uw begeleiding de beste positie bepalen.
  • Geen afhankelijkheden meer van derden
  • Geen wrapper-elementen.
  • Werkt met elementen die zich in de bovenste laag bevinden.

Laten we het probleem dat we hierboven probeerden op te lossen, opnieuw creëren en aanpakken. Maar gebruik in plaats daarvan de analogie van een boot met een anker. Deze vertegenwoordigen het verankerde element en het anker. Het water vertegenwoordigt het bevattende blok.

Eerst moet u kiezen hoe u het anker definieert. U kunt dit binnen uw CSS doen door de eigenschap anchor-name op het ankerelement in te stellen. Het accepteert een streepjes-ident- waarde.

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

Als alternatief kunt u een anker in uw HTML definiëren met het anchor . De attribuutwaarde is de ID van het ankerelement. Hierdoor ontstaat er een impliciet anker.

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

Nadat u een anker heeft gedefinieerd, kunt u de anchor gebruiken. De anchor heeft 3 argumenten:

  • Ankerelement: De anchor-name van het te gebruiken anker, of u kunt de waarde weglaten om een implicit anker te gebruiken. Het kan worden gedefinieerd via de HTML-relatie, of met een anchor-default -eigenschap met een anchor-name waarde.
  • Ankerzijde: Een trefwoord van de positie die u wilt gebruiken. Dit kan top , right , bottom , left , center , enz. zijn. Of u kunt een percentage doorgeven. 50% zou bijvoorbeeld gelijk zijn aan center .
  • Fallback: Dit is een optionele fallback-waarde die een lengte of percentage accepteert.

U gebruikt de anchor als waarde voor de inzeteigenschappen ( top , right , bottom , left of hun logische equivalenten) van het verankerde element. U kunt ook de anchor in calc gebruiken:

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

Er is geen eigenschap center , dus een optie is om calc te gebruiken als u de grootte van uw verankerde element kent. Waarom gebruik je geen translate ? Je zou dit kunnen gebruiken:

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

Maar de browser houdt geen rekening met getransformeerde posities voor verankerde elementen. Het zal duidelijk worden waarom dit belangrijk is bij het overwegen van positieterugval en automatische positionering.

Het is je wellicht opgevallen dat hierboven de aangepaste eigenschap --boat-size wordt gebruikt. Maar als u de grootte van het verankerde element wilt baseren op die van het anker, heeft u ook toegang tot die grootte. In plaats van het zelf te berekenen, kunt u de anchor-size gebruiken. Om onze boot bijvoorbeeld vier keer zo breed te maken als ons anker:

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

Je hebt ook toegang tot de hoogte met anchor-size(--my-anchor height) . En u kunt het gebruiken om de grootte van een van beide assen of beide in te stellen.

Wat als u wilt verankeren aan een element met absolute positionering? De regel is dat de elementen geen broers en zussen kunnen zijn. In dat geval kunt u het anker omwikkelen met een container met relative positionering. Dan kun je je eraan verankeren.

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

Bekijk deze demo waar je het anker rond kunt slepen en de boot zal volgen.

Scrollpositie volgen

In sommige gevallen bevindt uw ankerelement zich mogelijk in een scrollende container. Tegelijkertijd kan uw verankerde element zich buiten die container bevinden. Omdat scrollen gebeurt in een andere thread dan de lay-out, heb je een manier nodig om dit te volgen. De eigenschap anchor-scroll kan dit doen. Je stelt het in op het verankerde element en geeft het de waarde van het anker dat je wilt volgen.

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

Probeer deze demo, waarbij u anchor-scroll kunt in- en uitschakelen met het selectievakje in de hoek.

De analogie valt hier echter een beetje plat, want in een ideale wereld liggen je boot en anker allebei in het water. Functies zoals de Popover API bevorderen ook de mogelijkheid om gerelateerde elementen dichtbij te houden. Ankerpositionering werkt echter met elementen die zich in de bovenste laag bevinden. Dit is een van de belangrijkste voordelen van de API: het kunnen verbinden van elementen in verschillende stromen.

Overweeg deze demo met een scrollende container met ankers met tooltips. De tooltip-elementen die popovers zijn, bevinden zich mogelijk niet op dezelfde plek als de ankers:

Maar u zult merken hoe de popovers hun respectievelijke ankerlinks volgen. U kunt het formaat van die schuifcontainer wijzigen, waarna de posities voor u worden bijgewerkt.

Positieterugval en automatische positionering

Dit is waar het positioneringsvermogen van het anker een niveau hoger gaat. Een position-fallback kan uw verankerde element positioneren op basis van een reeks fallbacks die u verstrekt. U begeleidt de browser met uw stijlen en laat deze de positie voor u uitwerken.

Het gebruikelijke gebruiksscenario hier is een tooltip die moet schakelen tussen weergave boven of onder een anker. En dit gedrag is gebaseerd op de vraag of de tooltip door de container wordt afgekapt. Die container is meestal de viewport.

Als je je in de code van de laatste demo had verdiept, had je gezien dat er een position-fallback eigenschap in gebruik was. Als je door de container scrolde, heb je misschien gemerkt dat de verankerde popovers sprongen. Dit gebeurde toen hun respectievelijke ankers de grens van het kijkvenster naderden. Op dat moment proberen de popovers zich aan te passen om in de viewport te blijven.

Voordat een expliciete position-fallback wordt gecreëerd, biedt ankerpositionering ook automatische positionering . Je kunt die flip gratis krijgen door de waarde auto te gebruiken in zowel de ankerfunctie als de tegenovergestelde inset-eigenschap. Als u bijvoorbeeld anchor voor bottom gebruikt, stelt u top in op auto .

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

Het alternatief voor automatische positionering is het gebruik van een expliciete position-fallback . Hiervoor moet u een positie-fallback-set definiëren. De browser doorloopt deze totdat hij er een vindt die hij kan gebruiken en past die positionering vervolgens toe. Als er geen werkende wordt gevonden, wordt standaard de eerste gedefinieerde instelling gebruikt.

Een position-fallback die probeert de tooltips boven en onder weer te geven, zou er als volgt uit kunnen zien:

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

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

Het toepassen van dat op de tooltips ziet er als volgt uit:

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

Het gebruik van anchor-default betekent dat u de position-fallback voor andere elementen kunt hergebruiken. U kunt ook een aangepaste eigenschap met bereik gebruiken om anchor-default in te stellen.

Overweeg deze demo opnieuw met de boot. Er is een position-fallback set. Wanneer u de positie van het anker verandert, zal de boot zich aanpassen om binnen de container te blijven. Probeer ook de opvulwaarde te wijzigen, waardoor de lichaamsopvulling wordt aangepast. Merk op hoe de browser de positionering corrigeert. De posities worden gewijzigd door de rasteruitlijning van de container te wijzigen.

De position-fallback is deze keer uitgebreider, waarbij posities met de klok mee worden geprobeerd.

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


Voorbeelden

Nu u een idee heeft van de belangrijkste functies voor ankerpositionering, laten we eens kijken naar enkele interessante voorbeelden naast de tooltips. Deze voorbeelden zijn bedoeld om uw ideeën op gang te brengen voor manieren waarop u ankerpositionering kunt gebruiken. De beste manier om de specificaties verder te ontwikkelen is met input van echte gebruikers zoals jij.

Contextmenu's

Laten we beginnen met een contextmenu met behulp van de Popover API. Het idee is dat als je op de knop met de punthaak klikt, er een contextmenu verschijnt. En dat menu krijgt een eigen menu om uit te breiden.

De opmaak is hier niet het belangrijkste onderdeel. Maar je hebt elk drie knoppen die popovertarget gebruiken. Dan heb je drie elementen die het popover attribuut gebruiken. Dat geeft u de mogelijkheid om de contextmenu's te openen zonder JavaScript. Dat zou er zo uit kunnen zien:

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

Nu kunt u een position-fallback definiëren en deze delen tussen de contextmenu's. We zorgen ervoor dat ook eventuele inset voor de popovers worden uitgeschakeld.

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

Dit geeft u een adaptieve geneste contextmenu-UI . Probeer de inhoudspositie te wijzigen met de selectie. De optie die u kiest, werkt de rasteruitlijning bij. En dat heeft invloed op hoe de ankerpositionering de popovers positioneert.

Focus en volg

Deze demo combineert CSS-primitieven door :has() in te voeren. Het idee is om een ​​visuele indicator voor de input over te zetten die focus heeft.

Doe dit door tijdens runtime een nieuw anker in te stellen. Voor deze demo wordt een aangepaste eigenschap met bereik bijgewerkt op de invoerfocus.

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

Maar hoe zou je dit verder kunnen brengen? Je zou het kunnen gebruiken voor een of andere vorm van instructie-overlay. Een tooltip kan tussen interessante punten bewegen en de inhoud ervan bijwerken. Je zou de inhoud kunnen crossfaden. Discrete animaties waarmee u display kunt animeren of overgangen kunt bekijken, zouden hier kunnen werken.

Staafdiagramberekening

Een ander leuk ding dat je kunt doen met ankerpositionering is het combineren met calc . Stel je een diagram voor waarin je een aantal popovers hebt die het diagram annoteren.

U kunt de hoogste en laagste waarden volgen met CSS min en max . De CSS daarvoor zou er ongeveer zo uit kunnen zien:

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

Er is wat JavaScript in het spel om de grafiekwaarden bij te werken en wat CSS om de grafiek op te maken. Maar ankerpositionering zorgt voor de lay-outupdates voor ons.

Formaatgrepen wijzigen

U hoeft niet slechts aan één element te ankeren. Je kunt voor een element meerdere ankers gebruiken. Dat heb je misschien gemerkt in het voorbeeld van het staafdiagram. De tooltips werden aan het diagram en vervolgens aan de juiste balk verankerd. Als je dat concept wat verder zou doorvoeren, zou je het kunnen gebruiken om het formaat van elementen te wijzigen.

U kunt de ankerpunten behandelen als aangepaste handvatten voor formaatwijzigingen en een inset gebruiken.

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

In deze demo maakt GreenSock Draggable de handgrepen Draggable. Maar het <img> -element wordt vergroot of verkleind om de container te vullen die zich aanpast om de opening tussen de handvatten op te vullen.

Een SelectMenu?

Dit laatste is een beetje een plaag voor wat komen gaat. Maar u kunt een focusseerbare popover maken en nu heeft u ankerpositionering. U zou de basis kunnen leggen voor een stylebaar <select> -element.

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

Een impliciet anchor maakt dit gemakkelijker. Maar de CSS voor een rudimentair startpunt zou er als volgt uit kunnen zien:

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

Combineer de functies van de Popover API met CSS-ankerpositionering en je bent dichtbij.

Het is handig als je dingen als :has() begint te introduceren. Je zou de markering op open kunnen draaien:

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

Waar zou je het vervolgens naartoe kunnen brengen? Wat hebben we nog meer nodig om daar een functionerende select van te maken? Dat bewaren we voor het volgende artikel. Maar maak je geen zorgen, er komen stijlbare geselecteerde elementen aan. Blijf op de hoogte!


Dat is het!

Het webplatform evolueert. De positionering van CSS-ankers is een cruciaal onderdeel voor het verbeteren van de manier waarop u UI-besturingselementen ontwikkelt. Het zal je weghouden van enkele van die lastige beslissingen. Maar je kunt er ook dingen mee doen die je nog nooit eerder hebt gedaan. Zoals het stylen van een <select> element! Laat ons weten wat je ervan vindt.

Foto door CHUTTERSNAP op Unsplash