4

I've been loving position: sticky. It solves most, if not all, of the issues without resorting to JavaScript. But, I've hit a wall. I need to make an element that is nested inside a couple of <div> to be sticky. We know that position: sticky works as a blend of position: relative and position: fixed, therefore it will anchor to its first parent.

From MDN:

The element is positioned according to the normal flow of the document, and then offset relative to its nearest scrolling ancestor and containing block (nearest block-level ancestor)

In this case, I want to make a header sticky relative to the window and not the container. The HTML makes it difficult for me to restructure it outside nested <div>

Is this possible without JavaScript?

Here's the code:

<div class="attendance">
<!-- Here's the header I want to make sticky to the window, and not to div.attendance-->
    <header class="text-center sticky">Monday 11/22/2019</header>
<!-- Header above -->
    <div class="date-config">
        <div class="form-group">
            <input type="checkbox" id="workable" /> No Work<br />
        </div>

        <div class="form-group">
            <label for="notes">Notes:</label>
            <textarea id="notes" class="form-control"></textarea>
        </div>
        <label for="markall">Mark all as>
        <select id="markall" class="form-control">
            <option></option>
            <option>Absent</option>
            <option>Present</option>
        </select>
    </div>

    <div class="student-attendance">
      Hello :)   
    </div>

</div>

Any ideas?

P.S: I've found this, but it uses JavaScript.

Edit: Here's an awful, but working example (Beware! It's in Spanish - Look for the dates! They won't stick to the window!).

Jose A
  • 10,053
  • 11
  • 75
  • 108
  • 2
    can you also post your existing CSS so that we can run the code and see the current result? – yqlim Nov 30 '18 at 03:14
  • @YongQuan: Sure! It's awful (Huge, and it's going to break your PC), but I added a jsfiddle with the exact code. – Jose A Nov 30 '18 at 03:38
  • `position: sticky` is not supported in IE11. So be aware of that. Ideal approach would be to use `position: fixed` and let other element flow with defined margin-top. – Dinesh Pandiyan Nov 30 '18 at 03:38
  • @DineshPandiyan: Thanks ;) This is more of an enhanced feature, so it's ok if it doesn't work with older browsers. – Jose A Nov 30 '18 at 03:40

2 Answers2

3

Ok! First I'd like to apologize as this question wasn't possible to be answered without rendering the HTML. Fortunately, I have found the solution.

TL;DR In this case, no, you need JavaScript. You will need to implement a translateY transform in the element to achieve this. I don't know if the problem is that the parent element has a transform property and it causes this bug or there's something else causing the issue.

Explanation:

I'm currently using a carousel JS library called tiny slider. I'm displaying form elements instead of images, (Building a responsive table; Had issues when I tried using CSS Grids). So far, so good. The problem started when I wanted to set sticky the date headers.

I went with the modern approach of setting position:sticky, but that didn't work. The elements would get clogged in a certain position and it wouldn't move or stick. I started researching online (which ended up asking this same question), and the HTML itself. I did find that there were many parent <div>s that were created by tiny-slider. My theory was that it was getting attached to one of those parents.

Therefore, I decided to try the old tactic of combining position:fixed with a scroll event. But, that didn't work. Going back online and Google-Fuing a bit, there seems to be an old bug [1] [2] [3] that whenever a translate is applied to one of the parents an out-of-root container is created and position:fixed doesn't work as expected.

I have a hunch that this may be one of the reasons why sticky didn't work, but according to this answer, it doesn't seem like it.

I kept thinking for a while, and resorted to use a transform CSS property with translateY. I made a small experiment in the browser, and it worked!

Hence, I ended up implementing the scroll eventListener and listening to the header's parent's position, and applying getBoundingClientRect() to get the offset. If I had applied it to the element itself, it would have given me the translated position which I applied through CSS.

I was skeptical that this could be a performance bottleneck for mobile browsers. Therefore, I checked that the transform function was called inside a requestAnimationFrame and it had applied a will-change property in the CSS stylesheet.

I ran the code with a 4x CPU Slowdown in Google Chrome, and had good results .

Here's the resulting function I have (Where elemsToFixed are all the <header> elements, and threshold is the top offset so it doesn't conflict with the navbar):

export function fixedHeaderScroll(elemsToFixed: HTMLHeadingElement[], threshold: number) {
  if (!elemsToFixed || elemsToFixed.length === 0) {
    console.error("elemsToFixed can't be null or empty");
    return;
  }
  console.log('Total elems', elemsToFixed.length);
  // We assume that all of the elements are on the same height.
  const firstEl = elemsToFixed[0];
  let propSet = false;
  window.addEventListener('scroll', (e) => {
    window.requestAnimationFrame(() => {
      const top = firstEl.parentElement!.getBoundingClientRect().top;
      if (top > threshold) {
        if (!propSet) return;
        propSet = false;
        setElemsFixed(elemsToFixed, top, threshold, false);
        return;
      }
      propSet = true;
      setElemsFixed(elemsToFixed, top, threshold);
    });
  });
}

function setElemsFixed(elemsToFixed: HTMLHeadingElement[], top: number,
                       threshold: number, setFixed = true) {
  console.log('SetElemsFixed is', setFixed);
  elemsToFixed.forEach((elem) => {
    if (!setFixed) {
      elem.removeAttribute('style');
      return;
    }

    elem.style.transform = `translateY(${(top * -1)}px)`;
  });
}

The following picture shows a 4x slowdown in the CPU and the calculation of the style (With 26 elements) is about 29.4ms (Which is great!). Using Chrome 70 on Windows and i7 4700MQ processor.

chrome profiling 4x slowdown

Mr Lister
  • 45,515
  • 15
  • 108
  • 150
Jose A
  • 10,053
  • 11
  • 75
  • 108
  • I want to make a correction. While I was driving, I realized that the translation is in fact taking too long. If it's a 60 Hz screen, each FPS is at 16.67ms. Therefore, it should be around that time. Nonetheless, I ended up applying a transition effect and it looks good. – Jose A Nov 30 '18 at 20:52
  • I also want to make an addition, even though it says a 73.75% Global Usage (adjust for your needs) in caniuse, this could also be achieved with Web Animations API. – Jose A Dec 05 '18 at 00:43
2

As you reference to the docs, the position sticky will stick to its "nearest scrollable ancestor", which generally speaking will be any ancestor with 'display: block' (the default) and also things like 'display: flex'. You can make an ancestor not scrollable by setting 'display: contents'. (Depending on the rest of your layout and CSS this may or may not be usable.)

  • Can you add a snippet explaining what exactly `display: contents` does that makes it suitable for this instance? – Allister Sep 18 '20 at 02:18