Big Trouble With Scrollbar Grip Minimum Size

Scrollbars are one of those things you usually do not pay much attention until you need to implement one. In a very basic form, there is just some straightforward math to learn, and you are good to go. To maintain good UX you usually want to have a minimum size for the scroll grip (the thing a user can drag), which causes math to be a bit more tricky, but nothing crazy.

Real trouble begins though when you want to add marks on the scrollbar that correspond to certain positions in the document - like the ones your favorite editor shows for errors / warnings. So let's take a journey down the rabbit hole...

The Basics

The main job of a scrollbar is to represent the position of the user's viewport into a document, and to allow the user to adjust said viewport by either dragging the grip or clicking somewhere on the track. Some scrollbars also provide arrow buttons at the end for small adjustment, but I will ignore this type of interaction for this article as it does not affect the topic discussed.

Scrollbars can be vertical or horizontal, and since the math does not really change, except for using x vs y and width vs height, I will use the horizontal version for the demo purposes.

Here's a typical scrollbar with the components named for clarity and document contents indicated by the vertical bars:


With the component names clarified we can start with the math required to calculate the size of the grip:

let documentSize = 1000;
let containerSize = 200;
let trackSize = containerSize;
let gripSize =
  containerSize / documentSize * trackSize;

Very first gotcha with the math above is that if your document is smaller than the container your grip size will end up being larger than the track. If that happens you need to either hide the scrollbar altogether or limit the grip size with something like this:

gripSize = gripSize > trackSize
  ? trackSize
  : gripSize;

Next, let's calculate offset of the grip given the document scroll offset. In the majority of UI systems (except for Apple iOS / macOS) it is not allowed to scroll past the end of the content, so the maximum scroll of is difference between the document size and container size. Similarly, we do not want for grip to go outside of the track, so max grip offset is reduced by its size. With this in place, calculation of a grip offset based on document scroll is matter of applying the document scroll over size proportion to the grip and track:

let documentScroll = 400;
let maxDocumentScroll =
  documentSize - containerSize;
let maxGripOffset = containerSize - gripSize;
let documentScrollProportion =
  documentScroll / maxDocumentScroll;
let gripOffset =
  documentScrollProportion * maxGripOffset;

If the user is allowed to drag the grip to change the scroll position of the document, then we also need to solve the proportion in the opposite direction:

let newGripOffset = 20;
let gripOffsetProportion =
  newGripOffset / maxGripOffset;
let newDocumentScroll =
  gripOffsetProportion * maxDocumentScroll;

Putting all of this together, we get the scrollbar behavior that also has the calculations visualized for clarity:

drag me
  • documentSize: 1000
  • containerSize: 100
  • trackSize: 200
  • gripSize: 20
  • documentScroll: 200
  • maxDocumentScroll: 800
  • maxGripOffset: 180
  • gripOffset: 20

Grip Minimum Size

Astute reader might note that with the current setup if we increase the size of the document and / or decrease container size, then at some point the size of the grip becomes too small to comfortable interact with or look at:

The good news is that we can ensure minimum size of the grip in the same way that we ensured max size, and the math still works out... mostly.

let minGripSize = 40;
gripSize = gripSize < minGripSize
  ? minGripSize
  : gripSize;

If your drag around the grip in the example below then everything seems to work out fine, and indeed this is the math and setup that majority, if not all, scrollbars use.

So, what's wrong with it? Well, with this small change our scrollbar is no longer a birds-eye view of the document with a viewport visualized, but rather a rough indicator of the viewport's position.

This mismatch becomes a real problem if we need to mark certain positions in the document and display them in the scrollbar. The example below has some content bars colorized, and their positions shown in the scrollbar with the same color. The grip is also semi-transparent so that user can see the marks below it. And the calculation of the mark position in the scrollbar is quite straightforward:

let markOffsetInDocument = 300;
let markProportion =
  markOffsetInDocument / documentSize;
let markPositionOnScrollbar =
    markProportion * trackSize;

As you move the grip above the marks that are visible in the document, and the marks that are under the grip in the scrollbar often do not match.

Now that we understand the problem, let's consider our options for handling it.

How Do Others Do It?

Most often the functionality of marks is used inside IDEs and text editors. Here's the summary of the ones that do have scroll bar marks:

Visual Studio (Win)no minimum size
Visual Studio Code
Atomno minimum size
Xcode (Mac)
Nova (Mac)
Qt Creatorsplit projection (#3 below)

Solution 1: Small or No Minimum Size

Probably the easiest way to mitigate the issue is to reduce the minimum size of the grip as this increases the size of the document necessary for the issue to appear and reduces the effect when it does happen. Of course, you should still follow the UI and accessibility guidelines for your target platform regarding minimum area for interactive elements. You can adjust minimum size of the grip in the example below and see the effect.

Solution 2: Separate Grip Drag Area From Visual Area

The idea behind this solution is to draw the grip without the minimum size, and then overlay larger grip, either permanently or on hover, so that user can comfortably interact with the grip while being able to accurately map scroll position to the document area.

Depending on the UI design and requirements you might also choose to allow an overhang of the "drag grip" like shown below:

Solution 3: Split Projection

I do not know if the title of the section adequately represents the idea, but I could not come up with something better so here we go. Also as mentioned above, this is the solution used in Qt Creator.

Our target state is to have grip and marks below (on) it be an accurate, scaled representation of the visible part of the document. The solution would then to draw the grip and marks scaled up and then proportionally scale down the remaining free track space before and after the grip. The implementation of such an approach is not too complicated. Instead of projecting the whole document to the whole track we split the document into three pieces and map accordingly with their own separate scaling: before viewport → before grip, viewport → grip, and after viewport → after grip.

// grip position / size same as in basic code above
let beforeProportion =
  gripOffset / documentScroll;
let gripProportion =
  gripSize / containerSize;
let documentAfterStart =
  documentScroll + containerSize;
let gripAfterStart = gripOffset + gripSize;
let afterProportion =
  (trackSize - gripAfterStart) /
  (documentSize - documentAfterStart);
let marks = [100, 180, 300/* , ... */];
for (let offsetInDocument of marks) { 
  if (offsetInDocument < documentScroll) {
    offsetInDocument *= beforeProportion;
  } else if (offsetInDocument > documentAfterStart) {
    offsetInDocument -= documentAfterStart;
    offsetInDocument *= afterProportion;
    offsetInDocument += gripAfterStart;
  } else { // Inside viewport
    offsetInDocument -= documentScroll;
    offsetInDocument *= gripProportion;
    offsetInDocument += gripOffset;

  // ... your painting code

The result, as seen below looks a bit a like a lens effect inside the grip when we move it around, but it does maintain the correct mapping of the document content to scrollbar at all times:


I think all three solutions have their place depending on the project requirements, and I believe all of them provide better trade-offs than just ignoring the problem which seems to be what is happening with the majority of code editors at least.