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

Attualmente come fai il tethering di un elemento con un altro? Puoi provare a monitorare la loro posizione o utilizzare un 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. Hanno bisogno di JavaScript o introducono un markup aggiuntivo. L'API di posizionamento dell'ancoraggio CSS ha lo scopo di risolvere questo problema fornendo un'API CSS per il tethering degli elementi. Offre un mezzo per posizionare e ridimensionare un elemento in base alla posizione e alle dimensioni degli altri elementi.

L&#39;immagine mostra una finestra del browser fittizia con i dettagli dell&#39;anatomia di una descrizione comando.

Supporto browser

Puoi provare l'API di posizionamento dell'ancoraggio CSS in Chrome Canary alla base delle "Funzionalità sperimentali della piattaforma web" flag. Per attivare il flag, apri Chrome Canary e visita chrome://flags. Poi attiva l'opzione "Funzionalità sperimentali della piattaforma web". flag.

È inoltre disponibile 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 tratta gli aspetti importanti a livello generale. Inoltre, l'implementazione attuale non è completamente sincronizzata con le specifiche del gruppo di lavoro CSS.

Il problema

A cosa serve questa operazione? Un caso d'uso in evidenza sarebbe la creazione di descrizioni comando o esperienze simili a quelle delle 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 collegare un elemento a un altro. Inoltre, ti aspetti che l'interazione con la pagina non interrompa il tethering, ad esempio se un utente scorre o ridimensiona l'UI.

Un altro problema riguarda il fatto che vuoi assicurarti che l'elemento sottoposto a tethering rimanga visibile, ad esempio se apri una descrizione comando e viene ritagliato dai limiti dell'area visibile. Potrebbe non essere un'esperienza ottimale per gli utenti. Vorresti che la descrizione comando si adatti.

Soluzioni attuali

Al momento, puoi affrontare il problema in vari modi.

Il primo è il rudimentale "Wrap the anchor" (Avvolgi l'ancora) l'importanza di un approccio umile. Devi raccogliere entrambi gli elementi e aggregarli in un container. 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 container e tutto rimarrà dove vuoi per la maggior parte.

Un altro approccio potrebbe essere se conosci la posizione dell'ancoraggio o puoi in qualche modo monitorarla. Puoi passarla 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. Puoi fare qualcosa come il seguente codice, ma ora ciò significa che i tuoi stili stanno iniziando a fuoriuscire 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);

Questo inizia a porsi alcune domande:

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

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

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

Per alleviare parte di questo problema, potresti cercare una soluzione JavaScript che ti aiuti. Ciò comporterà il costo dell'aggiunta di una dipendenza al progetto e potrebbe introdurre problemi di prestazioni, a seconda di come le 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 cambiare invece. Questo fa parte del "perché" per il posizionamento degli anchor CSS. In questo modo, non dovrai preoccuparti di problemi di prestazioni durante il calcolo della posizione.

Ecco come si potrebbe presentare il codice in caso di utilizzo di "float-ui", un pacchetto molto usato 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'ancoraggio in questa demo che utilizza questo codice.

La "descrizione comando" potrebbero 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 alle tue esigenze.

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

Utilizzare il posizionamento degli ancoraggi

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.
  • Consenti al browser di visualizzare 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.

Ricreiamo e affrontiamo il problema che stavamo cercando di risolvere sopra. Usa invece l'analogia di una barca con un'ancora. che 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 tuo CSS impostando la proprietà anchor-name sull'elemento anchor. Accetta un valore trated-ident.

.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 anchor. In questo modo viene creato un ancoraggio implicito.

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

Dopo aver definito un ancoraggio, puoi utilizzare la funzione anchor. La funzione anchor accetta tre argomenti:

  • Elemento di ancoraggio: il 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 ancorato: una parola chiave della posizione che vuoi utilizzare. Potrebbe essere top, right, bottom, left, center e così via. In alternativa, puoi trasmettere una percentuale. Ad esempio, 50% è uguale a center.
  • Di riserva: si tratta di un valore di riserva facoltativo che accetta una durata o una percentuale.

Utilizzerai la funzione anchor come valore per le proprietà del riquadro (top, right, bottom, left o gli 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 center, quindi un'opzione è utilizzare calc se conosci le dimensioni dell'elemento ancorato. Perché non usare translate? Potresti usare 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 per gli elementi ancorati. Sarà chiaro perché questo aspetto è importante quando si considerano i fallback di posizione e il posizionamento automatico.

Potresti aver notato l'utilizzo della proprietà personalizzata --boat-size di cui sopra. Tuttavia, se vuoi basare le dimensioni dell'elemento ancorato su quelle dell'ancoraggio, puoi anche accedere a quella dimensione. Anziché calcolarlo autonomamente, puoi utilizzare la funzione anchor-size. Ad esempio, per fare in modo che la nostra barca sia quattro volte la larghezza dell'ancora:

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

Puoi accedere anche all'altezza con anchor-size(--my-anchor height). e puoi utilizzarlo per impostare le dimensioni di uno o entrambi gli assi.

E se volessi ancorare un elemento con il posizionamento absolute? La regola è che gli elementi non possono essere di pari livello. In questo caso, puoi aggregare l'ancoraggio con un container con posizionamento relative. Dopodiché puoi ancorarti a questo elemento.

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

Guarda questa demo in cui puoi trascinare l'ancora per farla seguire alla barca.

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 su un thread diverso dal layout, hai bisogno di un modo per monitorarlo. La proprietà anchor-scroll può farlo. Lo imposti sull'elemento ancorato e gli assegni 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.

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

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

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

Posizione di riserva e posizionamento automatico

È in questo caso che la capacità di posizionamento degli ancoraggi aumenta di livello. Un position-fallback può posizionare l'elemento ancorato in base a un insieme di elementi di riserva che fornisci. Puoi guidare il browser con i tuoi stili e lasciare che trovi la posizione desiderata.

Il caso d'uso comune in questo caso è una descrizione comando che dovrebbe alternare la visualizzazione sopra o sotto un ancoraggio. Questo comportamento dipende dall'eventuale ritaglio della descrizione comando dal relativo container. Di solito questo contenitore è l'area visibile.

Se hai approfondito il codice dell'ultima demo, avresti notato che era in uso una proprietà position-fallback. Se hai fatto scorrere il contenitore, potresti aver notato che i popover ancorati saltano. Questo si è verificato quando i rispettivi ancoraggi si avvicinavano al confine 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 offrirà anche il posizionamento automatico. Puoi ottenere questa funzione senza costi utilizzando il valore auto sia nella funzione di ancoraggio che nella proprietà del riquadro opposto. 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 è usare un elemento position-fallback esplicito. Questo richiede la definizione di un insieme di riserva di posizione. Il browser le segue fino a quando non ne trova uno da utilizzare e poi applica quel posizionamento. Se non ne trova uno che funzioni, per impostazione predefinita viene usato 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);
  }
}

L'applicazione di questo codice 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 set di position-fallback. Quando cambi la posizione dell'ancora, la barca si regola per rimanere all'interno del container. Prova a modificare anche il valore della spaziatura interna per regolare la spaziatura interna. Osserva in che modo il browser corregge il posizionamento. Le posizioni vengono modificate modificando l'allineamento della griglia del contenitore.

position-fallback è più dettagliato questa volta quando cerca 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 ancoraggi, esaminiamo alcuni esempi interessanti oltre alle descrizioni comando. Lo scopo di questi esempi è far fluire le tue idee sui modi in cui potresti utilizzare il posizionamento degli ancoraggi. Il modo migliore per approfondire le specifiche è tramite l'input 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 gallone, verrà visualizzato un menu contestuale. che avrà un proprio menu da espandere.

Il markup non è la parte importante in questo caso. Tuttavia, ci sono tre pulsanti ciascuno che utilizza popovertarget. Poi ci sono tre elementi che utilizzano l'attributo popover. Ciò ti consente di aprire i menu contestuali senza alcuno 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 valore position-fallback e condividerlo tra i menu contestuali. Assicurati di annullare l'impostazione di qualsiasi stile 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 del menu contestuale nidificato e adattiva. Prova a modificare la posizione dei contenuti con il pulsante di selezione. L'opzione scelta aggiorna l'allineamento della griglia. Questo incide sulla posizione dei popover con il posizionamento degli ancoraggi.

Concentrati e segui

Questa demo combina le primitive CSS introducendo :has(). L'idea è eseguire la transizione di un indicatore visivo per input su cui è impostato lo stato attivo.

Per farlo, imposta un nuovo ancoraggio in fase di runtime. Per 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 una qualche forma di overlay didattico. Una descrizione comando potrebbe spostarsi tra i punti d'interesse e aggiornarne i contenuti. Potresti applicare una dissolvenza incrociata ai contenuti. Qui potrebbero funzionare animazioni discrete che ti consentono di animare display o di visualizzare transizioni.

Calcolo grafico a barre

Un'altra cosa divertente che puoi fare con il posizionamento degli ancoraggi è combinarlo con calc. Immagina un grafico in cui ci sono dei popover che annotano il grafico.

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

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

Sono attive alcune funzionalità JavaScript per aggiornare i valori del grafico e CSS per applicare uno stile al grafico. Ma il posizionamento degli ancoraggi si occupa degli aggiornamenti del layout al posto nostro.

Punti di ridimensionamento

Non è necessario ancorarsi a un solo elemento. Puoi utilizzare molti ancoraggi per un elemento. Potresti averlo notato nell'esempio del grafico a barre. Le descrizioni comando erano ancorate al grafico e quindi alla barra appropriata. Se avessi portato questo concetto un po' oltre, potresti utilizzarlo per ridimensionare gli elementi.

Puoi trattare i punti di ancoraggio come punti di manipolazione di ridimensionamento personalizzati 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 Trascinabile rende i punti di manipolazione trascinabili. Tuttavia, l'elemento <img> viene ridimensionato per riempire il contenitore, regolando così lo spazio tra i punti di manipolazione.

Un menu di selezione?

Quest'ultima è un'anticipazione su cosa ci aspetta in futuro. Tuttavia, puoi creare un popover attivabile per avere il posizionamento dell'ancoraggio. 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 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 degli ancoraggi CSS e avrai quasi finito.

È piacevole iniziare a introdurre elementi come :has(). Puoi ruotare l'indicatore quando apri:

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

Quale potrebbe essere il prossimo passo? Cos'altro dobbiamo fare per renderlo un select funzionante? Lo salveremo per il prossimo articolo. Ma non preoccuparti, saranno in arrivo elementi selezionati allo stile. Continua a seguirci.


È tutto!

La piattaforma web si sta evolvendo. Il posizionamento degli ancoraggi CSS è fondamentale per migliorare il modo in cui sviluppi i controlli dell'interfaccia utente. Ti astrarrà da alcune di queste decisioni difficili. ma ti consentirà anche di fare cose che non hai mai potuto fare prima. Ad esempio, applica lo stile di un elemento <select>. Facci conoscere le tue impressioni.

Foto di CHUTTERSNAP su Unsplash