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

In che modo esegui il tethering tra un elemento e l'altro? Potresti provare a monitorare le loro posizioni o utilizzare un qualche tipo 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;
}

Queste soluzioni spesso non sono ideali. Richiede JavaScript oppure introducono un markup aggiuntivo. L'API di posizionamento anchor CSS mira a risolvere questo problema fornendo un'API CSS per gli elementi di tethering. Fornisce un mezzo per posizionare e ridimensionare un elemento in base alla posizione e alle dimensioni di altri.

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

Supporto del browser

Puoi provare l'API di posizionamento dell'ancoraggio CSS in Chrome Canary dietro il flag "Sperimentale Funzionalità della piattaforma web". Per attivare il flag, apri Chrome Canary e visita chrome://flags. Attiva poi il flag "Funzionalità sperimentali della piattaforma web".

C'è anche un polyfill in fase di sviluppo da parte del team di Oddbird. 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 le parti più importanti a livello generale. Inoltre, l'implementazione attuale non è completamente sincronizzata con la specifica del Gruppo di lavoro CSS.

Il problema

Perché dovresti farlo? Un caso d'uso importante potrebbe essere la creazione di descrizioni comando o esperienze simili a descrizioni comando. In questo caso, spesso vuoi eseguire il tethering della descrizione comando per i contenuti a cui fa riferimento. Spesso è necessario un modo per eseguire il tethering di un elemento con un altro. Inoltre, l'interazione con la pagina non comporta l'interruzione del tethering, ad esempio se un utente scorre o ridimensiona l'interfaccia utente.

Un'altra parte del problema è se vuoi assicurarti che l'elemento collegato tramite tethering rimanga visibile, ad esempio se apri una descrizione comando che viene tagliata dai limiti dell'area visibile. Potrebbe non essere un'esperienza ottimale per gli utenti. Vuoi che la descrizione comando si adatti.

Soluzioni attuali

Al momento, hai a disposizione diversi modi per affrontare il problema.

Il primo è l'approccio rudimentale del tipo "wrap the anchor". Prendi entrambi gli elementi e li arrotoli in un contenitore. Quindi puoi utilizzare position per posizionare la descrizione comando rispetto all'ancoraggio.

<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à nella posizione che preferisci per la maggior parte del tempo.

Un altro approccio potrebbe essere sapere se conosci la posizione dell'ancoraggio o in qualche modo puoi tracciarlo. Potresti passarlo alla descrizione comando con le 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'ancoraggio? Probabilmente dovrai intervenire con JavaScript. Potresti fare qualcosa come fa il seguente codice, ma ora significa che i tuoi stili stanno iniziando a uscire da CSS e in 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, ti vengono poste alcune domande:

  • Quando devo calcolare gli stili?
  • Come si calcolano gli stili?
  • Con quale frequenza calcoliamo gli stili?

Il problema si risolve? Potrebbe essere adatto al tuo caso d'uso, ma c'è un problema: la nostra soluzione non si adatta. Il rilevatore non risponde. Cosa succede se il mio elemento ancorato viene tagliato dall'area visibile?

Ora devi decidere se reagire a questa situazione e come. Il numero di domande e decisioni da prendere sta iniziando a crescere. Non devi fare altro che ancorare un elemento a un altro. In un mondo ideale, la tua soluzione si adatterà e reagirà all'ambiente circostante.

Per alleviare alcuni di questi problemi, potresti ricorrere a una soluzione JavaScript che ti aiuti. Ciò comporta l'aggiunta di una dipendenza al progetto e potrebbe introdurre problemi di prestazioni a seconda di come vengono utilizzati. 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 decisioni potrebbero non essere ridotte, ma cambiate. Questa è una parte della procedura per il posizionamento di ancoraggio CSS. Durante il calcolo della posizione non devi pensare a problemi di prestazioni.

Ecco come potrebbe apparire il codice per l'utilizzo di "floating-ui", un pacchetto molto utilizzato per risolvere 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'ancoraggio in questa demo che utilizza quel codice.

La "descrizione comando" potrebbe non comportarsi come previsto. Reagisce all'uscita dall'area visibile sull'asse y, ma non sull'asse x. Consulta la documentazione e troverai probabilmente 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 funziona esattamente come vuoi.

Utilizzo del posizionamento dell'ancoraggio

Inserisci l'API di posizionamento dell'ancoraggio 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 il browser ottenga la posizione migliore in base alle tue indicazioni.
  • Niente più dipendenze di terze parti
  • Nessun elemento wrapper.
  • Funziona con gli elementi che si trovano nel livello superiore.

Ripercorriamo e affrontiamo il problema che stavamo cercando di risolvere. Usa invece l'analogia con una barca con un'ancora. Rappresentano l'elemento ancorato e l'ancoraggio. L'acqua rappresenta il blocco contenitore.

Innanzitutto, devi scegliere come definire l'ancoraggio. Puoi farlo all'interno del CSS impostando la proprietà anchor-name sull'elemento di ancoraggio. Accetta un valore trattino-idente.

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

In alternativa, potrai definire un ancoraggio nel codice HTML con l'attributo anchor. Il valore dell'attributo è l'ID dell'elemento di ancoraggio. Questo crea un ancoraggio implicito.

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

Una volta definito un ancoraggio, puoi utilizzare la funzione anchor. La funzione anchor accetta tre argomenti:

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

Puoi usare la funzione anchor come valore per le proprietà inset (top, right, bottom, left o i relativi equivalenti logici) dell'elemento ancorato. Puoi anche utilizzare la funzione anchor 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à integrata di center, quindi un'opzione è utilizzare calc se conosci le dimensioni dell'elemento ancorato. Perché non utilizzare translate? Puoi utilizzare questo:

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

Tuttavia, il browser non prende in considerazione le posizioni trasformate degli elementi ancorati. Diventerà chiaro perché ciò sia importante quando si considerano le posizioni di riserva e il posizionamento automatico.

Potresti aver notato l'utilizzo della proprietà personalizzata --boat-size indicata sopra. Tuttavia, se vuoi basare la dimensione degli elementi ancorati su quella dell'ancoraggio, puoi accedere anche a queste dimensioni. Anziché calcolarla personalmente, puoi utilizzare la funzione anchor-size. Ad esempio, per rendere la nostra barca quattro volte la larghezza dell'ancoraggio:

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

Hai accesso anche all'altezza con anchor-size(--my-anchor height). Puoi utilizzarla per impostare la dimensione di entrambi gli assi o di entrambi.

E se volessi ancorare un elemento con posizionamento absolute? La regola è che gli elementi non possono essere gemelli. In tal caso, puoi includere l'ancoraggio in un contenitore con posizionamento relative. A questo punto puoi ancorare l'elemento.

<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 è possibile trascinare l'ancoraggio e la barca seguirà.

Monitoraggio della posizione di scorrimento

In alcuni casi, l'elemento di ancoraggio potrebbe trovarsi all'interno di un contenitore a scorrimento. Contemporaneamente, l'elemento ancorato potrebbe trovarsi all'esterno di quel contenitore. Poiché lo scorrimento avviene su un thread diverso dal layout, devi avere un modo per monitorarlo. La proprietà anchor-scroll può farlo. Puoi impostarlo sull'elemento ancorato e assegnargli il valore dell'ancoraggio 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.

Tuttavia, l'analogia è un po' piatta qui, come in un mondo ideale, la tua barca e l'ancora sono entrambe nell'acqua. Inoltre, funzionalità come l'API Popover promuovono la possibilità di mantenere vicini gli elementi correlati. Tuttavia, il posizionamento dell'ancoraggio funzionerà con gli elementi che si trovano nel livello superiore. Questo è uno dei principali vantaggi dell'API: la possibilità di eseguire il tethering degli elementi in flussi diversi.

Considera questa demo che ha un container a scorrimento con ancoraggi e descrizioni comando. Gli elementi della descrizione comando che sono popover potrebbero non essere posizionati insieme agli ancoraggi:

Tuttavia, noterai come i popover tracciano i rispettivi link di ancoraggio. Puoi ridimensionare il contenitore a scorrimento e le posizioni verranno aggiornate automaticamente.

Posizione di riserva e posizionamento automatico

È qui che il potere di posizionamento dell'ancoraggio sale di livello. Un elemento position-fallback può posizionare l'elemento ancorato in base a un insieme di elementi di riserva da te forniti. Tu guidi il browser con i tuoi stili e lasci che si adatti alla posizione per te.

Il caso d'uso comune qui è una descrizione comando che dovrebbe capovolgere la visualizzazione sopra o sotto un ancoraggio. Questo comportamento dipende dal fatto che la descrizione comando venga troncata o meno dal container. Di solito il container è l'area visibile.

Se avessi approfondito il codice dell'ultima demo, avresti visto che è in uso una proprietà position-fallback. Se hai fatto scorrere il contenitore, potresti aver notato che i popover ancorati sono saltati in alto. Questo accade quando i rispettivi ancoraggi si avvicinavano al limite dell'area visibile. In quel momento, i popover stanno cercando di adattarsi per rimanere nell'area visibile.

Prima di creare un position-fallback esplicito, il posizionamento dell'ancoraggio offre anche il posizionamento automatico. Puoi ottenere questa rotazione senza costi utilizzando un valore di auto sia nella funzione di ancoraggio che nella proprietà inset 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 elemento position-fallback esplicito. Ciò richiede la definizione di un insieme di posizioni di riserva. Il browser le analizzerà finché non ne troverà una che può utilizzare, quindi applicherà tale posizionamento. Se non riesce a trovare quello che funziona, per impostazione predefinita viene utilizzato il primo valore 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);
  }
}

L'applicazione di questa impostazione alle descrizioni comando ha il seguente aspetto:

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

Se utilizzi anchor-default, puoi riutilizzare position-fallback per altri elementi. Puoi anche utilizzare una proprietà personalizzata con ambito per impostare anchor-default.

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

Questa volta l'indicatore position-fallback è più dettagliato questa volta cercando di posizionare 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 principali funzionalità per il posizionamento degli ancoraggi, diamo un'occhiata ad alcuni esempi interessanti oltre alle descrizioni comando. Questi esempi hanno lo scopo di far progredire le tue idee su come utilizzare il posizionamento degli ancoraggi. Il modo migliore per approfondire le specifiche è tramite il contributo di utenti reali come te.

Menu contestuali

Iniziamo con un menu contestuale utilizzando l'API Popover. L'idea è che facendo clic sul pulsante con il gallone si aprirà un menu contestuale. Questo menu avrà un proprio menu da espandere.

Il markup non è la parte importante in questo caso. Tuttavia, ci sono tre pulsanti ciascuno che utilizza popovertarget. Hai tre elementi che utilizzano l'attributo popover. In questo modo potrai 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. Assicurati di annullare l'impostazione degli stili inset anche per i popover.

[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 ottieni un'interfaccia utente con menu contestuale nidificato adattivo. Prova a cambiare la posizione dei contenuti con il selettore. L'opzione selezionata aggiorna l'allineamento della griglia. Questo influisce sul modo in cui il posizionamento dell'ancoraggio posiziona i popover.

Concentrati e segui

Questa demo combina le primitive CSS inserendo :has(). L'idea è la transizione di un indicatore visivo per l'elemento input attivo.

Per farlo, imposta un nuovo ancoraggio in fase di runtime. In questa demo, una proprietà personalizzata con ambito viene aggiornata in base all'input.

#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 potresti andare oltre? Puoi utilizzarlo per qualche tipo di overlay didattico. Una descrizione comando potrebbe spostarsi tra i punti d'interesse e aggiornarne i contenuti. Puoi applicare la dissolvenza incrociata dei contenuti. Le animazioni discrete che consentono di animare display o visualizzare le transizioni potrebbero funzionare in questa fase.

Calcolo grafico a barre

Un'altra cosa divertente che puoi fare con il posizionamento dell'ancoraggio è combinarlo con calc. Immagina un grafico in cui sono presenti dei popover che lo annotano.

Puoi monitorare i valori più alti e più bassi utilizzando i CSS min e max. 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%;
  }

È necessario usare codice JavaScript per aggiornare i valori del grafico e codice CSS per applicare uno stile al grafico. Tuttavia, il posizionamento di ancoraggio si occupa degli aggiornamenti del layout.

Ridimensiona i punti di manipolazione

Non è necessario ancorare a un solo elemento. Per un elemento potresti utilizzare molti ancoraggi. Potresti averlo notato nell'esempio del grafico a barre. Le descrizioni comando erano ancorate al grafico e poi alla barra appropriata. Se hai approfondito questo concetto, potresti utilizzarlo per ridimensionare gli elementi.

Puoi trattare i punti di ancoraggio come punti di manipolazione di ridimensionamento personalizzati e affidarti a 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 consente di trascinare i punti di manipolazione. Tuttavia, l'elemento <img> viene ridimensionato per riempire il contenitore, che si adatta in modo da riempire lo spazio tra i punti di manipolazione.

Un SelectMenu?

Quest'ultima è un'anticipazione del futuro. Tuttavia, puoi creare un popover attivabile e ora hai a disposizione il posizionamento degli ancoraggi. Potresti creare le basi di un elemento <select> con stile.

<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 valore anchor implicito semplifica l'operazione. Tuttavia, il CSS per un punto di partenza rudimentale potrebbe presentarsi come segue:

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

Combinando le funzionalità dell'API Popover con il posizionamento di ancoraggio CSS, hai quasi finito.

È pratico quando inizi a introdurre cose come :has(). Puoi ruotare l'indicatore all'apertura:

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

Qual è il prossimo passo? Cos'altro serve per renderlo un select funzionante? Lo salveremo nel prossimo articolo. Ma non preoccuparti, sono in arrivo elementi selezionati con stile. Continua a seguirci.


È tutto!

La piattaforma web si sta evolvendo. Il posizionamento di ancoraggio CSS è una parte fondamentale per migliorare il modo in cui sviluppi i controlli dell'interfaccia utente. Ti allontanerà da alcune di queste decisioni difficili. ma ti consente anche di fare cose che non hai mai fatto prima. Ad esempio, assegna uno stile a un elemento <select>. Facci sapere cosa ne pensi.

Foto di CHUTTERSNAP su Unsplash