Posição de segmentação: elementos fixos que estão atualmente em um estado 'travado'


110

position: sticky funciona em alguns navegadores móveis agora, então você pode fazer uma barra de menu rolar com a página, mas depois manter a parte superior da janela de visualização sempre que o usuário rolar por ela.

Mas e se você quiser dar um novo estilo à sua barra de menu fixa sempre que ela estiver 'travada' no momento? por exemplo, você pode querer que a barra tenha cantos arredondados sempre que rolar com a página, mas assim que ela aderir ao topo da janela de visualização, você deseja se livrar dos cantos arredondados superiores e adicionar uma pequena sombra embaixo isto.

Existe algum tipo de pseudo-seletor (por exemplo ::stuck) para direcionar os elementos que estão position: sticky e estão aderindo? Ou os fornecedores de navegadores têm algo parecido em desenvolvimento? Se não, onde eu o solicitaria?

NB. As soluções javascript não são boas para isso porque no celular você geralmente obtém apenas um único scrollevento quando o usuário libera o dedo, então JS não pode saber o momento exato em que o limite de rolagem foi ultrapassado.

Respostas:


104

Atualmente, não há um seletor proposto para os elementos que estão 'travados' no momento. O módulo Postioned Layout, onde position: stickyé definido, também não menciona nenhum seletor.

Solicitações de recursos para CSS podem ser postadas na lista de discussão no estilo www . Acredito que uma :stuckpseudoclasse faz mais sentido do que um ::stuckpseudoelemento, uma vez que você está procurando atingir os próprios elementos enquanto eles estão nesse estado. Na verdade, uma :stuckpseudo-aula foi discutida algum tempo atrás ; a principal complicação, descobriu-se, é aquela que prejudica praticamente qualquer seletor proposto que tente fazer a correspondência com base em um estilo renderizado ou calculado: dependências circulares.

No caso de uma :stuckpseudoclasse, o caso mais simples de circularidade ocorreria com o seguinte CSS:

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

E pode haver muitos outros casos extremos que seriam difíceis de resolver.

Embora seja geralmente aceito que seria bom ter seletores que correspondam com base em determinados estados de layout , infelizmente existem limitações importantes que tornam a implementação desses seletores não triviais. Eu não prenderia minha respiração por uma solução CSS pura para este problema tão cedo.


14
Isso é uma vergonha. Eu também estava procurando uma solução para esse problema. Não seria bastante fácil simplesmente introduzir uma regra que diz que as positionpropriedades em um :stuckseletor devem ser ignoradas? (uma regra para fornecedores de navegador, quero dizer, semelhante a regras sobre como lefttem precedência sobre rightetc))
— powerbuoy

5
Não é apenas a posição ... imagine a :stuckque muda o topvalor de 0para 300px, então role para baixo 150px... deve ficar ou não? Ou pense em um elemento com position: stickye bottom: 0onde o :stucktalvez muda font-sizee, portanto, o tamanho dos elementos (mudando, portanto, o momento em que ele deve ficar) ...
— Romano

3
Consulte github.com/w3c/csswg-drafts/issues/1660, onde a proposta é ter eventos JS para saber quando algo fica travado / desencravado. Isso não deve ter os problemas que um pseudo-seletor apresenta.
— Ruben

27
Acredito que os mesmos problemas circulares podem ser feitos com muitas pseudo classes já existentes (por exemplo: hover mudando a largura e: não (: hover) mudando de volta). Eu adoraria: prendo pseudo classe e acho que o desenvolvedor deve ser responsável por não ter os problemas circulares em seu código.
— Marek Lisý de

12
Bem ... Eu realmente não entendo isso como um erro - é como dizer que o whileciclo está mal projetado porque permite um loop infinito :) No entanto, obrigado por esclarecer isso;)
— Marek Lisý

25

Em alguns casos, um simples IntersectionObserverpode resolver o problema, se a situação permitir aderir a um ou dois pixels fora de seu contêiner raiz, em vez de alinhá-lo adequadamente. Dessa forma, quando fica logo além da borda, o observador dispara e estamos prontos e correndo.

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

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

Cole o elemento fora de seu contêiner com top: -2pxe, em seguida, direcione por meio do stuckatributo ...

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

Exemplo aqui: https://codepen.io/anon/pen/vqyQEK


1
Acho que uma stuckclasse seria melhor do que um atributo personalizado ... Existe algum motivo específico para sua escolha?
— collimarco

Uma classe também funciona bem, mas isso parece um nível um pouco mais alto do que isso, já que é uma propriedade derivada. Um atributo parece mais apropriado para mim, mas de qualquer forma é uma questão de gosto.
— rackable

Preciso que meu topo tenha 60px por causa de um cabeçalho já corrigido, então não consigo fazer seu exemplo funcionar
— FooBar

1
Tente adicionar um pouco de preenchimento superior ao que estiver preso, talvez padding-top: 60pxno seu caso :)
— Tim Willis

5

Alguém no blog do Google Developers afirma ter encontrado uma solução baseada em JavaScript performativa com um IntersectionObserver .

Bit de código relevante aqui:

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

Eu mesmo não repliquei, mas talvez ajude alguém a tropeçar nessa questão.


3

Não sou realmente um fã de usar js hacks para estilizar coisas (ou seja, getBoudingClientRect, ouvir rolagem, ouvir redimensionamento), mas é assim que estou resolvendo o problema. Esta solução terá problemas com páginas que possuem conteúdo minimizável / maximizável (<detalhes>), ou rolagem aninhada, ou realmente quaisquer bolas curvas. Dito isso, é uma solução simples para quando o problema também é simples.

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

Observe que o ouvinte de evento de rolagem é executado no thread principal, o que o torna um assassino de desempenho. Em vez disso, use a API Intersection Observer.
— Cético Jule

if (requestedFrame) { return; }Não é um "assassino de desempenho" devido ao lote de quadros de animação. O Intersection Observer ainda é uma melhoria.
— Seph Reed

0

Uma forma compacta de quando você tem um elemento acima do position:stickyelemento. Ele define o atributo stuckque você pode combinar em CSS com 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'))
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.