Waypoints: Some Points

jQuery.Waypoints (GitHub) is just so cool! But, as always, some traps and pitfalls… Here’s a demo jsFiddle.

Setup

  1. DOM: for the usual “historical reasons”, this is the markup I started with:
    h1
    ol
        li
            header
                h2[id=...] {display: inline}
            p
        ...

    header needed because my h2’s are often inlined with other stuff.

  2. JS: beautifully short, simple:
    /* Make headers sticky when scrolled off. */
    $("body > ol > li").waypoint(function(event, direction) {
        $(this).toggleClass("sticky", direction === "down");
    });

    (Obviously, run after DOM ready.)

  3. CSS:
    .sticky > header {position: fixed; top: 0; background: white; border-bottom: thin solid black}

“Fixed” won’t scroll

  1. First pitfall: too easy to bind waypoints to elements whose position is switched to fixed, eg:
    $("header").waypoint(... $(this).toggleClass("sticky"...
    .sticky {position: fixed...

    which is broken, of course: once the header becomes position:fixed you can’t scroll by it!

    Must instead bind listeners to a parent element, and apply styling accordingly, eg:

    $("li").waypoint(...)
    li.sticky > header {...}
  2. Internal links: similarly, once an element is position:fixed, fragment links fail to trigger scrolling, since the element is always already “in view”. Here, I had to move the IDs from h2’s to their parent list items:
    <li id="foo"><header><h2>...

Height

  1. Repositioning an existing DOM element from relative to fixed, and vice versa, has another annoying ramification: it’s taken out of the flow, causing a reflow — layout changes. Apparently, Waypoints doesn’t recalculate the coordinates (height) of set waypoints, so they’re now out of sync… Visibly broken.
  2. Slightly less noticeable here is the skip caused by the removal of headers, though obviously in a different scenario the breakage would be apparent.
  3. Forcing a recalculation of all heights “fixes” this behavior:
    ...waypoint(function(event, direction) {
        $(this).toggleClass("sticky", direction === "down");
        $.waypoints("refresh");
    });

Width

  1. Pulling headers out of their containers also breaks their own layout: width is now relative to the viewport, not containing element. Using “absolute” units (even ems) will work aroundthis, but can’t use percentages:
    body > ol > li {width: 15em; ...}
    .sticky > header {width: 15em; ...}

Z-index

  1. All those fixed headers stack up over each other till you scroll by them in the opposite direction. And they’re not the same size, so don’t cover the others completely. Now that’s a harder problem to solve…


Comments are closed.