Esegui il tethering degli elementi tra loro con il posizionamento di ancoraggio CSS

Al momento, come colleghi un elemento a un altro? Potresti provare a monitorare le relative posizioni o utilizzare una qualche forma di elemento 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;
}

Spesso queste soluzioni non sono ideali. Hanno bisogno di JavaScript o introducono markup aggiuntivo. L'API CSS Anchor Positioning mira a risolvere questo problema fornendo un'API CSS per il tethering degli elementi. Fornisce un mezzo per posizionare e dimensionare un elemento in base alla posizione e alle dimensioni di altri elementi.

L&#39;immagine mostra un mockup di una finestra del browser che mostra in dettaglio l&#39;anatomia di una descrizione comando.

Supporto browser

Puoi provare l'API CSS Positioning Anchor in Chrome Canary dietro il flag "Funzionalità sperimentali della piattaforma web". Per attivare questo flag, apri Chrome Canary e visita la pagina chrome://flags. Poi attiva il flag "Funzionalità sperimentali della piattaforma web".

Inoltre, il team di Oddbird sta sviluppando un polyfill. Assicurati di controllare il repository all'indirizzo github.com/oddbird/css-anchor-positioning.

Puoi verificare il supporto dell'ancoraggio con:

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

Tieni presente che questa API è ancora in fase sperimentale e potrebbe cambiare. Questo articolo illustra a grandi linee le parti importanti. Inoltre, l'implementazione attuale non è completamente in sincronizzazione con la specifica del gruppo di lavoro CSS.

Il problema

Perché eseguire questa operazione? Un caso d'uso importante è la creazione di descrizioni comando o esperienze simili. In questo caso, spesso è opportuno collegare la descrizione comando ai contenuti a cui fa riferimento. Spesso è necessario un modo per collegare un elemento a un altro. Inoltre, ti aspetti che l'interazione con la pagina non rompa il legame, ad esempio se un utente scorre o ridimensiona l'interfaccia utente.

Un altro aspetto del problema è che, se vuoi assicurarti che l'elemento collegato rimanga visibile, ad esempio se apri una descrizione comando e questa viene tagliata dai limiti dell'area visibile, Questa potrebbe non essere un'esperienza ottimale per gli utenti. Vuoi che la descrizione comando si adatti.

Soluzioni attuali

Al momento, esistono diversi modi per affrontare il problema.

Il primo è l'approccio rudimentale "Inserisci un a capo nell'ancora". Prendi entrambi gli elementi e li inserisci in un contenitore. Poi puoi utilizzare position per posizionare la descrizione comando rispetto all'ancora.

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

Puoi spostare il contenitore e tutto rimarrà dove vuoi per la maggior parte.

Un altro approccio potrebbe essere quello di conoscere la posizione della tua ancora o di poterla monitorare in qualche modo. Puoi passarlo alla descrizione comando con proprietà personalizzate.

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

Ma cosa succede se non conosci la posizione dell'ancora? È probabile che tu debba intervenire con JavaScript. Potresti fare qualcosa di simile al codice seguente, ma ora significa che i tuoi stili stanno iniziando a fuoriuscire dal CSS e a passare a 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);

A questo punto sorgono alcune domande:

  • Quando calcolo gli stili?
  • Come faccio a calcolare gli stili?
  • Con quale frequenza calcolo gli stili?

Il problema è stato risolto? Potrebbe essere adatta al tuo caso d'uso, ma c'è un problema: la nostra soluzione non si adatta. Non è reattivo. Cosa succede se l'elemento ancorato viene tagliato dal viewport?

Ora devi decidere se reagire e come. Il numero di domande e decisioni che devi prendere sta iniziando ad aumentare. Vuoi solo ancorare un elemento a un altro. In un mondo ideale, la soluzione si adeguerà e reagirà all'ambiente circostante.

Per ovviare a questo problema, puoi utilizzare una soluzione JavaScript. Ciò comporterà il costo dell'aggiunta di una dipendenza al progetto e potrebbe causare problemi di prestazioni a seconda di come li utilizzi. Ad esempio, alcuni pacchetti utilizzano requestAnimationFrame per mantenere la posizione corretta. Ciò significa che tu e il tuo team dovete acquisire familiarità con il pacchetto e le sue opzioni di configurazione. Di conseguenza, le tue domande e le tue decisioni potrebbero non essere ridotte, ma modificate. Questo è il motivo del posizionamento dell'ancora CSS. In questo modo non dovrai preoccuparti di problemi di prestazioni durante il calcolo della posizione.

Ecco un esempio di codice per l'utilizzo di "floating-ui", un pacchetto molto utilizzato per questo problema:

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

Prova a riposizionare l'ancora in questa demo che utilizza il codice.

La "mostra testo" potrebbe non comportarsi come previsto. Reagisce all'uscita dall'area visibile sull'asse Y, ma non sull'asse X. Consulta la documentazione e probabilmente troverai una soluzione adatta a te.

Tuttavia, trovare un pacchetto adatto al tuo progetto può richiedere molto tempo. Si tratta di decisioni aggiuntive e può essere frustrante se non funzionano come vuoi.

Utilizzare il posizionamento dell'ancora

Inserisci l'API di posizionamento dell'ancora CSS. L'idea è mantenere gli stili nel CSS e ridurre il numero di decisioni da prendere. Speri di ottenere lo stesso risultato, ma l'obiettivo è migliorare l'esperienza degli sviluppatori.

  • Non è richiesto JavaScript.
  • Lascia che sia il browser a trovare la posizione migliore in base alle tue indicazioni.
  • Nessuna dipendenza da terze parti
  • Nessun elemento wrapper.
  • Funziona con gli elementi nel livello superiore.

Ricreiamo e affrontiamo il problema che stavamo cercando di risolvere sopra. Utilizza invece l'analogia di una barca con un'ancora. Questi rappresentano l'elemento ancorato e l'ancora. L'acqua rappresenta il blocco contenente.

Innanzitutto, devi scegliere come definire l'ancora. Puoi farlo nel CSS impostando la proprietà anchor-name sull'elemento anchor. Accetta un valore dashed-ident.

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

In alternativa, potrai definire un'ancora nel codice HTML con l'attributo anchor. Il valore dell'attributo è l'ID dell'elemento di ancoraggio. Viene creata un'ancora implicita.

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

Una volta definita un'ancora, puoi utilizzare la funzione anchor. La funzione anchor accetta tre argomenti:

  • Elemento di ancoraggio: il anchor-name dell'ancora da utilizzare oppure puoi omettere il valore per utilizzare un'ancora implicit. Può essere definito tramite la relazione HTML o con una proprietà anchor-default con un valore anchor-name.
  • Lato di ancoraggio:una parola chiave della posizione che vuoi utilizzare. Può essere top, right, bottom, left, center e così via. In alternativa, puoi passare una percentuale. Ad esempio, il 50% corrisponde a center.
  • Valore alternativo:si tratta di un valore alternativo facoltativo che accetta una durata o una percentuale.

La funzione anchor viene utilizzata come valore per le proprietà di incasso (top, right, bottom, left o i relativi equivalenti logici) dell'elemento ancorato. Puoi utilizzare la funzione anchor anche in 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));
}

Non esiste una proprietà di rientranza center, quindi un'opzione è utilizzare calc se conosci le dimensioni dell'elemento ancorato. Perché non utilizzare translate? Potresti utilizzare:

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

Tuttavia, il browser non prende in considerazione le posizioni trasformate per gli elementi ancorati. Sarà chiaro perché questo è importante quando si prendono in considerazione le alternative alla posizione e il posizionamento automatico.

Potresti aver notato l'utilizzo della proprietà personalizzata --boat-size sopra. Tuttavia, se vuoi basare le dimensioni dell'elemento ancorato su quelle dell'ancora, puoi accedere anche a queste dimensioni. Invece di calcolarlo manualmente, puoi utilizzare la funzione anchor-size. Ad esempio, per fare in modo che la barca sia quattro volte più larga dell'ancora:

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

Con anchor-size(--my-anchor height) hai accesso anche all'altezza. Puoi utilizzarlo per impostare le dimensioni di uno o di entrambi gli assi.

Che cosa succede se vuoi ancorare un elemento con il posizionamento absolute? La regola è che gli elementi non possono essere fratelli. In questo caso, puoi racchiudere l'ancora con un contenitore con posizionamento relative. Dopodiché puoi ancorarlo.

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

Dai un'occhiata a questa demo in cui puoi trascinare l'ancora e la barca la seguirà.

Monitoraggio della posizione di scorrimento

In alcuni casi, l'elemento di ancoraggio potrebbe trovarsi all'interno di un contenitore a scorrimento. Allo stesso tempo, l'elemento ancorato potrebbe trovarsi all'esterno del contenitore. Poiché lo scorrimento avviene in un thread diverso dal layout, devi trovare un modo per monitorarlo. La proprietà anchor-scroll può eseguire questa operazione. Lo imposti sull'elemento ancorato e gli assegni il valore dell'ancora che vuoi monitorare.

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

Prova questa demo in cui puoi attivare e disattivare anchor-scroll con la casella di controllo nell'angolo.

L'analogia non è perfetta, perché in un mondo ideale la barca e l'ancora sono entrambe in acqua. Inoltre, funzionalità come l'API Popover consentono di tenere vicini gli elementi correlati. Il posizionamento dell'ancora funzionerà però con gli elementi nel livello superiore. Questo è uno dei principali vantaggi dell'API: la possibilità di collegare elementi in flussi diversi.

Considera questa demo che ha un contenitore scorrevole con ancore con descrizioni comando. Gli elementi della descrizione comando che sono popup potrebbero non essere nello stesso spazio delle ancore:

Tuttavia, noterai che i popup monitorano i rispettivi link di ancoraggio. Puoi ridimensionare il contenitore con scorrimento e le posizioni verranno aggiornate automaticamente.

Posizione di riserva e posizionamento automatico

È qui che la potenza del posizionamento dell'ancora sale di un livello. Un position-fallback può posizionare l'elemento ancorato in base a un insieme di valori di riserva che fornisci. Indirizzi il browser con i tuoi stili e lasci che calcoli la posizione per te.

Il caso d'uso comune è una descrizione comando che deve essere visualizzata sopra o sotto un'ancora. Questo comportamento si basa sul fatto che la descrizione comando venga ritagliata dal relativo contenitore. Di solito, questo contenitore è l'area visibile.

Se hai esaminato il codice dell'ultima demo, avrai notato che era in uso una proprietà position-fallback. Se hai fatto scorrere il contenitore, potresti aver notato che i popup ancorati sono saltati. Questo accadeva quando le rispettive ancore si avvicinavano al confine dell'area visibile. In quel momento, i popup cercano di adattarsi per rimanere nell'area visibile.

Prima di creare un position-fallback esplicito, il posizionamento dell'ancora offrirà anche il posizionamento automatico. Puoi ottenere questa rotazione senza costi utilizzando un valore auto sia nella funzione di ancoraggio sia nella proprietà di incavo opposta. Ad esempio, se utilizzi anchor per bottom, imposta top su auto.

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

L'alternativa al posizionamento automatico è utilizzare un position-fallback esplicito. Per farlo, devi definire un insieme di riserva per la posizione. Il browser li esaminerà finché non ne troverà uno che può utilizzare e poi applicherà quel posizionamento. Se non riesce a trovarne uno funzionante, viene utilizzato per impostazione predefinita il primo definito.

Un position-fallback che tenta di visualizzare le descrizioni comando sopra e sotto potrebbe avere il seguente aspetto:

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

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

Se lo applichi alle descrizioni comando, il risultato sarà il seguente:

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

L'utilizzo di anchor-default significa che puoi riutilizzare position-fallback per altri elementi. Puoi anche utilizzare una proprietà personalizzata basata sugli ambiti per impostare anchor-default.

Guarda di nuovo questa demo con la barca. È impostato un position-fallback. Man mano che modifichi la posizione dell'ancora, la barca si regola per rimanere all'interno del contenitore. Prova a modificare anche il valore di spaziatura, che regola la spaziatura del corpo. Nota come il browser corregge il posizionamento. Le posizioni vengono modificate cambiando l'allineamento della griglia del contenitore.

Questa volta position-fallback è più dettagliato e prova le posizioni in senso orario.

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


Esempi

Ora che hai un'idea delle funzionalità principali per il posizionamento degli elementi di ancoraggio, diamo un'occhiata ad alcuni esempi interessanti oltre alle descrizioni comando. Questi esempi hanno lo scopo di farti venire in mente dei modi in cui puoi utilizzare il posizionamento dell'ancora. Il modo migliore per migliorare le specifiche è ricevere il feedback di utenti reali come te.

Menu contestuali

Iniziamo con un menu contestuale che utilizza l'API Popover. L'idea è che facendo clic sul pulsante con il triangolo, venga visualizzato un menu contestuale. Questo menu avrà un proprio menu da espandere.

Il markup non è la parte importante. Tuttavia, hai tre pulsanti che utilizzano ciascuno popovertarget. Poi ci sono tre elementi che utilizzano l'attributo popover. In questo modo puoi aprire i menu contestuali senza JavaScript. Potrebbe avere il seguente aspetto:

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

Ora puoi definire un position-fallback e condividerlo tra i menu contestuali. Ci assicuriamo inoltre di annullare l'impostazione di eventuali stili inset per i popup.

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

In questo modo, avrai a disposizione un'interfaccia utente del menu contestuale nidificato adattabile. Prova a modificare la posizione dei contenuti con il selettore. L'opzione scelta aggiorna l'allineamento della griglia. Ciò influisce sul posizionamento dei popup in base al posizionamento dell'ancora.

Messa a fuoco e monitoraggio

Questa demo combina le primitive CSS inserendo :has(). L'idea è eseguire la transizione di un indicatore visivo per l'input che ha il focus.

Per farlo, imposta un nuovo ancoraggio in fase di esecuzione. Per questa demo, una proprietà personalizzata basata sugli ambiti viene aggiornata quando l'input è attivo.

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

Ma come puoi fare un ulteriore passo avanti? Potresti utilizzarlo per una sorta di overlay didattico. Una descrizione comando potrebbe spostarsi tra i punti d'interesse e aggiornarne i contenuti. Puoi usare la transizione sfumata tra i contenuti. In questo caso potrebbero essere utili animazioni discrete che ti consentano di animare display o le transizioni di visualizzazione.

Calcolo grafico a barre

Un'altra cosa divertente che puoi fare con il posizionamento dell'ancora è combinarlo con calc. Immagina un grafico con alcuni popup che lo annotano.

Puoi monitorare i valori più alti e più bassi utilizzando min e max CSS. Il CSS potrebbe avere il seguente aspetto:

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

Viene utilizzato del codice JavaScript per aggiornare i valori del grafico e del CSS per impostarne lo stile. Tuttavia, il posizionamento dell'ancora si occupa degli aggiornamenti del layout per noi.

Maniglie di ridimensionamento

Non è necessario ancorare solo a un elemento. Puoi utilizzare più ancore per un elemento. Probabilmente l'hai notato nell'esempio del grafico a barre. Le descrizioni comando sono state ancorate al grafico e poi alla barra appropriata. Se approfondisci questo concetto, puoi utilizzarlo per ridimensionare gli elementi.

Puoi trattare i punti di ancoraggio come maniglie di ridimensionamento personalizzate e utilizzare un valore inset.

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

In questa demo, GreenSock Draggable rende trattenibili le maniglie. Tuttavia, l'elemento <img> viene ridimensionato per riempire il contenitore che si regola per colmare lo spazio tra i manici.

Un SelectMenu?

L'ultima è un po' un'anticipazione di ciò che verrà. Tuttavia, puoi creare un popup attivabile e ora hai il posizionamento dell'ancora. Potresti creare le basi di un elemento <select> personalizzabile.

<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 implicito semplifica la procedura. Tuttavia, il CSS per un punto di partenza rudimentale potrebbe avere il seguente aspetto:

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

Combina le funzionalità dell'API Popover con il posizionamento dell'ancora CSS e hai quasi finito.

È fantastico quando inizi a introdurre elementi come :has(). Puoi ruotare l'indicatore quando è aperto:

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

Dove potresti portarlo? Cosa ci serve per rendere funzionale select? Ne parleremo nel prossimo articolo. Ma non preoccuparti, a breve saranno disponibili elementi di selezione personalizzabili. Continua a seguirci.


È tutto!

La piattaforma web è in evoluzione. Il posizionamento dell'ancora CSS è un elemento fondamentale per migliorare il modo in cui sviluppi i controlli dell'interfaccia utente. Ti consentirà di evitare alcune di queste decisioni complicate. Ma ti consentirà anche di fare cose che non avresti mai potuto fare prima. Ad esempio, puoi applicare uno stile a un elemento <select>. Facci conoscere le tue impressioni.

Foto di CHUTTERSNAP su Unsplash