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.

callum
fonte

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.

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

encaixável
fonte
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.

neo pós moderno
fonte
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;
    });
})
Seph Reed
fonte
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'))
Jonas Eberle
fonte