Position de ciblage: éléments collants qui sont actuellement dans un état `` bloqué ''


110

position: sticky fonctionne maintenant sur certains navigateurs mobiles, vous pouvez donc faire défiler une barre de menu avec la page, mais ensuite rester en haut de la fenêtre chaque fois que l'utilisateur la fait défiler.

Mais que se passe-t-il si vous souhaitez redéfinir légèrement votre barre de menu collante chaque fois qu'elle est actuellement bloquée? Par exemple, vous voudrez peut-être que la barre ait des coins arrondis chaque fois qu'elle défile avec la page, mais dès qu'elle colle au haut de la fenêtre, vous voulez vous débarrasser des coins arrondis supérieurs et ajouter une petite ombre portée en dessous il.

Existe-t-il une sorte de pseudosélecteur (par exemple ::stuck) pour cibler les éléments qui ont collé position: sticky et sont actuellement bloqués? Ou est-ce que les fournisseurs de navigateurs ont quelque chose de semblable dans le pipeline? Sinon, où le demanderais-je?

NB. Les solutions javascript ne sont pas bonnes pour cela car sur mobile, vous n'obtenez généralement qu'un seul scrollévénement lorsque l'utilisateur relâche son doigt, de sorte que JS ne peut pas savoir à quel moment exact le seuil de défilement a été dépassé.

Réponses:


104

Il n'y a actuellement aucun sélecteur proposé pour les éléments actuellement «bloqués». Le module de mise en page postéposition: stickyest défini ne mentionne pas non plus un tel sélecteur.

Les demandes de fonctionnalités pour CSS peuvent être publiées sur la liste de diffusion de style www . Je pense qu'une :stuckpseudo-classe a plus de sens qu'un ::stuckpseudo-élément, car vous cherchez à cibler les éléments eux-mêmes pendant qu'ils sont dans cet état. En fait, une :stuckpseudo-classe a été discutée il y a quelque temps ; la principale complication, a-t-on trouvé, est celle qui affecte à peu près n'importe quel sélecteur proposé qui tente de faire correspondre en fonction d'un style rendu ou calculé: les dépendances circulaires.

Dans le cas d'une :stuckpseudo-classe, le cas le plus simple de circularité se produirait avec le CSS suivant:

:stuck { position: static; /* Or anything other than sticky/fixed */ }
:not(:stuck) { position: sticky; /* Or fixed */ }

Et il pourrait y avoir de nombreux autres cas extrêmes qui seraient difficiles à résoudre.

Bien qu'il soit généralement admis qu'avoir des sélecteurs qui correspondent en fonction de certains états de mise en page serait bien , il existe malheureusement des limitations majeures qui rendent leur mise en œuvre non triviale. Je ne retiendrais pas mon souffle pour une solution pure CSS à ce problème de si tôt.


14
C'est une honte. Je cherchais aussi une solution à ce problème. Ne serait-il pas assez facile d'introduire simplement une règle qui stipule que les positionpropriétés d'un :stucksélecteur doivent être ignorées? (une règle pour les fournisseurs de navigateurs, je veux dire, similaire aux règles sur la leftpriorité sur rightetc.))
powerbuoy

5
Ce n'est pas seulement la position ... imaginez un :stuckqui change la topvaleur de 0à 300px, puis faites défiler vers le bas 150px... devrait-il rester ou non? Ou pensez à un élément avec position: stickyet bottom: 0où le :stuckpeut - être change font-sizeet donc à la taille des éléments (donc en changeant le moment où il devrait coller) ...
Roman

3
Voir github.com/w3c/csswg-drafts/issues/1660 où la proposition est d'avoir des événements JS pour savoir quand quelque chose se bloque / se décolle. Cela ne devrait pas avoir les problèmes qu'un pseudo-sélecteur introduit.
Ruben

27
Je crois que les mêmes problèmes circulaires peuvent être posés avec de nombreuses pseudo-classes déjà existantes (par exemple: hover changeant de largeur et: pas (: hover) changer à nouveau). J'adorerais: coincé pseudo-classe et pense que le développeur devrait être responsable de ne pas avoir les problèmes circulaires dans son code.
Marek Lisý

12
Eh bien ... je ne comprends pas vraiment cela comme une erreur - c'est comme dire que le whilecycle est mal conçu car il permet une boucle sans fin :) Cependant, merci d'avoir
clarifié

26

Dans certains cas, un simple IntersectionObserverpeut faire l'affaire, si la situation permet de s'en tenir à un pixel ou deux à l'extérieur de son conteneur racine, plutôt que de se rincer correctement. De cette façon, quand il se trouve juste au-delà du bord, l'observateur tire et nous partons en courant.

const observer = new IntersectionObserver( 
  ([e]) => e.target.toggleAttribute('stuck', e.intersectionRatio < 1),
  {threshold: [1]}
);

observer.observe(document.querySelector('nav'));

Collez l'élément juste hors de son conteneur avec top: -2px, puis ciblez via l' stuckattribut ...

nav {
  background: magenta;
  height: 80px;
  position: sticky;
  top: -2px;
}
nav[stuck] {
  box-shadow: 0 0 16px black;
}

Exemple ici: https://codepen.io/anon/pen/vqyQEK


1
Je pense qu'une stuckclasse serait meilleure qu'un attribut personnalisé ... Y a-t-il une raison spécifique à votre choix?
collimarco le

Une classe fonctionne bien aussi, mais cela semble juste un peu plus haut que cela, car il s'agit d'une propriété dérivée. Un attribut me semble plus approprié, mais dans tous les cas, c'est une question de goût.
rackable le

J'ai besoin que mon top soit 60px à cause d'un en-tête déjà fixe, donc je ne peux pas faire fonctionner votre exemple
FooBar

1
Essayez d'ajouter un rembourrage supérieur à tout ce qui est bloqué, peut-être padding-top: 60pxdans votre cas :)
Tim Willis

5

Quelqu'un sur le blog Google Developers affirme avoir trouvé une solution performante basée sur JavaScript avec un IntersectionObserver .

Bit de code pertinent ici:

/**
 * Sets up an intersection observer to notify when elements with the class
 * `.sticky_sentinel--top` become visible/invisible at the top of the container.
 * @param {!Element} container
 */
function observeHeaders(container) {
  const observer = new IntersectionObserver((records, observer) => {
    for (const record of records) {
      const targetInfo = record.boundingClientRect;
      const stickyTarget = record.target.parentElement.querySelector('.sticky');
      const rootBoundsInfo = record.rootBounds;

      // Started sticking.
      if (targetInfo.bottom < rootBoundsInfo.top) {
        fireEvent(true, stickyTarget);
      }

      // Stopped sticking.
      if (targetInfo.bottom >= rootBoundsInfo.top &&
          targetInfo.bottom < rootBoundsInfo.bottom) {
       fireEvent(false, stickyTarget);
      }
    }
  }, {threshold: [0], root: container});

  // Add the top sentinels to each section and attach an observer.
  const sentinels = addSentinels(container, 'sticky_sentinel--top');
  sentinels.forEach(el => observer.observe(el));
}

Je ne l'ai pas reproduit moi-même, mais peut-être que cela aide quelqu'un à trébucher sur cette question.


3

Je ne suis pas vraiment fan de l'utilisation des hacks js pour styliser des trucs (c'est-à-dire getBoudingClientRect, faire défiler l'écoute, redimensionner l'écoute), mais c'est ainsi que je résous actuellement le problème. Cette solution aura des problèmes avec les pages qui ont un contenu minimisable / maximisable (<détails>), ou un défilement imbriqué, ou vraiment des boules de courbe que ce soit. Cela étant dit, c'est une solution simple lorsque le problème est simple également.

let lowestKnownOffset: number = -1;
window.addEventListener("resize", () => lowestKnownOffset = -1);

const $Title = document.getElementById("Title");
let requestedFrame: number;
window.addEventListener("scroll", (event) => {
    if (requestedFrame) { return; }
    requestedFrame = requestAnimationFrame(() => {
        // if it's sticky to top, the offset will bottom out at its natural page offset
        if (lowestKnownOffset === -1) { lowestKnownOffset = $Title.offsetTop; }
        lowestKnownOffset = Math.min(lowestKnownOffset, $Title.offsetTop);
        // this condition assumes that $Title is the only sticky element and it sticks at top: 0px
        // if there are multiple elements, this can be updated to choose whichever one it furthest down on the page as the sticky one
        if (window.scrollY >= lowestKnownOffset) {
            $Title.classList.add("--stuck");
        } else {
            $Title.classList.remove("--stuck");
        }
        requestedFrame = undefined;
    });
})

Notez que l'écouteur d'événement de défilement est exécuté sur le thread principal, ce qui en fait un tueur de performances. Utilisez plutôt l'API Intersection Observer.
Skeptical Jule le

if (requestedFrame) { return; }Ce n'est pas un "tueur de performances" en raison du lot d'images d'animation. Cependant, Intersection Observer est toujours une amélioration.
Seph Reed le

0

Une manière compacte lorsque vous avez un élément au-dessus de l' position:stickyélément. Il définit l'attribut stuckque vous pouvez associer en CSS avec header[stuck]:

HTML:

<img id="logo" ...>
<div>
  <header style="position: sticky">
    ...
  </header>
  ...
</div>

JS:

if (typeof IntersectionObserver !== 'function') {
  // sorry, IE https://caniuse.com/#feat=intersectionobserver
  return
}

new IntersectionObserver(
  function (entries, observer) {
    for (var _i = 0; _i < entries.length; _i++) {
      var stickyHeader = entries[_i].target.nextSibling
      stickyHeader.toggleAttribute('stuck', !entries[_i].isIntersecting)
    }
  },
  {}
).observe(document.getElementById('logo'))
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.