Anchoring an element to the browser’s vertical scrollbar

Posted on

Problem

The problem


I’m trying to position a chosen element so that it’s always horizontally aligned against the right-hand side of the page, and vertically aligned with the center of the web browser’s vertical inner scrollbar, i.e. the part of the scrollbar that moves as you scroll up and down the page. The “handlebar”, if you will.

I’ve been tweaking this code for so long that it’s starting to do my head in, so would like a sanity check, i.e. am I going about this the right way?

The key part of this function is the line beginning offset = scroll_position ...

The code

barFollower = function (e) {
  var target = $(e.data.target),
    scrollbar_button_height = 20,
    window_height = $(window).height(),
    window_height_adj = window_height - (scrollbar_button_height * 2),
    scroll_position = $(window).scrollTop(),
    body_height = $('body').height(),

  offset = scroll_position // The top of window
    + ((scroll_position / body_height) * window_height_adj) // The position of the top of scrollbar
    + (((window_height_adj / body_height) * window_height_adj) / 2) // Half the height of the scrollbar
    + scrollbar_button_height // The scrollbar button height (depends on browser chrome, unfortunately) - see notes below
    - element_offset; // offset for chosen 'position' based on element height - see notes below

  /* Some catches for when element height might mean it would get positioned outside the window */
  if (body_height < window_height) { // If there's no scrollbar.
    offset = (window_height / 2) + (target.height() / 2); // Position halfway down the window
  } else if (offset < scroll_position) { // Top bounds
    offset = scroll_position; // Fix it to the top of the window
  } else if (offset > scroll_position + window_height - target.height()) { // Bottom bounds
    offset = scroll_position + window_height - target.height(); // Fix it to the bottom of the window
  }
  target.css('top', offset);
};

Notes

Triggering

This function is triggered once on page load, and subsequently on every window scroll event.

Scrollbar button size

The tricky part here is that I can’t find a way to determine the height of the scrollbar ‘up’ and ‘down’ buttons, so I have made an assumption (20px), although this will potentially vary from browser to browser, and depends the browser chrome.
If anyone knows a way to accurately determine scrollbar button sizes, please let me know.

The element_offset variable

The element_offset is set outside this function, and can have one of three values to offset the element so that either the top, middle, or bottom of the element align with the middle of the vertical scrollbar. How it is set is not relevant at this point, so I’ve left it out to keep things simple.

Solution

I got mixed results from researching whether “scroll bar” properly refers to the movable part or the larger range. I had originally thought the latter, and had called the movable part a “thumb” — that may be a java-ism. I opted to use the OP’s original definition (movable part) for “scroll bar” — for the “handlebar”, if you prefer. And I used “scroll bar area” for the larger range — the “scrollbar”, if you prefer. Sorry for any confusion.

Refactoring common expressions exposed some quirks in the OP.
The original formula for the no-scrollbar case used + target.height()
in place of the - target.height() used everywhere else,
but taller targets should always get smaller offsets, yes?

Also, that case strangely ignored element_offset
— wouldn’t that cause target to suddenly jump when the body or
window is resized to make the scroll-bar appear/disappear?
I brought element_offset and (largely as a result) bounds testing into this case.

The window_height_adj value was an odd name for the scroll area height.
It was being mistaken for the simpler window_height in one calculation.
I factored it into the more useful scroll_scale_factor to avoid such confusion.

Scroll position plays two roles:

  • a baseline for the mid-window position
    calculation that gets scaled down to produce the relative
    mid-scrollbar position, and

  • a baseline for the final offset.

Calculating only relative offsets until the very end more clearly
separates out this second role and simplifies the bounds testing.

barFollower = function (e) {
    var target = $(e.data.target),
    scrollbar_button_height = 20, // (depends on browser chrome, unfortunately)
    window_height = $(window).height(),
    max_target_offset = window_height - target.height(),
    scroll_position = $(window).scrollTop(),
    body_height = $('body').height(),
    // ratio of full body height to full height of scroll area 
    // which does not include the buttons.
    scroll_scale_factor = body_height / (window_height - (scrollbar_button_height * 2)),
    offset = 0;

    if (body_height <= window_height) { // If there's no scrollbar.
        offset = max_target_offset / 2 - element_offset; // Position halfway down the window
    } else {
        offset = scrollbar_button_height 
            + (scroll_position + window_height/2) / scroll_scale_factor; // mid-window position reduced to scroll area scale
            - element_offset; // offset for chosen 'position' based on element height - see notes below
    }

    /* Some catches for when element height might mean it would get positioned outside the window */
    if (offset < 0) { // Top bounds
        offset = 0; // Fix it to the top of the window
    } else if (offset > max_target_offset) { // Bottom bounds
        offset = max_target_offset; // Fix it to the bottom of the window
    }
    target.css('top', scroll_position + offset);
};

What is wrong with:

<body>
    ...
    <div id="myfloatingElement">This is the element</div>
</body>

css:

#myfloatingElement
{
    position: fixed;
    right: 0;
    top: 50%;
}

Leave a Reply

Your email address will not be published. Required fields are marked *