33

I'm not sure if title is clear, so few words of explanation. I've got few little elements, let say div's (200px x 400px CONSTANT). Inside each of them, there is a paragraph with about 20 lines of text. Of course, this it to much for a poor little div. What I want to do is:

  1. Normally div has overflow: hidden property.
  2. On mouse over (:hover) this property is changed to overflow: auto;, and the scrollbar appears.

What is the problem? I want a little space (padding or margin) between the paragraph text and the scrollbar. Let's say that paragraph has a symetrical margin: 20px;. But after :hover triggers on, the scrollbar appears, and the whole right side of the paragraph is moved "scrollbar-width" px to the left. Sentences are broken in other lines, and the whole paragraph look different, which is quite annoying and not user friendly. How can I set my CSS, so the only change after hover will be the appearance of scroolbar?

In other words:

/* Normally no scrollbar */
div {display: block; width: 400px; height: 200px; overflow: hidden;}
/* On hover scrollbar appears */
div:hover {overflow: auto;}

/* Paragraph margin predicted for future scrollbar on div */
div p {margin: 20px (20px + scrollbarwidth px) 20px 50px;}
/* With scrollbar margin is symmetrical */
div:hover p {margin: 20px;} /* With scrollbar */

I have done a little snippet for it, with exaplanation of my problem in strong. I hope everything is clear :). I'm searching for a solution for almost two hours now, so I think my question is quite unique and interesting.

div.demo1 {
  width: 400px;
  height: 200px;
  overflow: hidden;
  background: #ddd;
}
div.demo1:hover {
  overflow: auto;
}
div.demo1 p {
  background: #eee;
  margin: 20px;
}
div.demo2 {
  width: 400px;
  height: 200px;
  overflow: hidden;
  background: #ddd;
}
div.demo2:hover {
  overflow: auto;
}
div.demo2 p {
  background: #eee;
  margin: 20px 40px 20px 20px;
}
div.demo2:hover p {
  margin: 20px 20px 20px 20px;
}
<div class="demo1">
  <p>
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
  </p>
</div>
<br>
<br>
<strong>As you can see, on hover right side of the paragraph "moves" left. But I want something like:</strong>
<br>
<br>
<div class="demo2">
  <p>
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
  </p>
</div>

<strong>But with a "perfect" scrollbar width (in this example I used 20px)</strong>
Jacek Kowalewski
  • 2,761
  • 2
  • 23
  • 36
  • BTW. I know that I can hardcode the paragraph width and use for example `350px`, but this is not the solution that I want - it is ugly and assumes that the max width of scrollbar is only 50px :P. Thx for your time, best regards. – Jacek Kowalewski Feb 06 '15 at 07:56
  • 1
    JS is the only option- as such possible duplicate: http://stackoverflow.com/questions/13382516/getting-scroll-bar-width-using-javascript – SW4 Feb 06 '15 at 08:33

14 Answers14

27

Scrollbar widths can vary between browsers and operating systems, and unfortunately CSS does not provide a way to detect those widths: we need to use JavaScript.

Other people have solved this problem by measuring the width of the scrollbar on an element:

We create a div .scrollbar-measure, add a scrollbar, and return its size.

// Create the div
var scrollDiv = document.createElement("div");
scrollDiv.className = "scrollbar-measure";
document.body.appendChild(scrollDiv);

// Get the scrollbar width
var scrollbarWidth = scrollDiv.offsetWidth - scrollDiv.clientWidth;
console.warn(scrollbarWidth);

// Delete the div
document.body.removeChild(scrollDiv);

This is fairly straightforward, but it is (obviously) not pure CSS.

Tanner Stern
  • 128
  • 1
  • 8
tomtomtom
  • 829
  • 1
  • 8
  • 20
  • Your answer is really great, +1 from me. However if you don't mind I will wait few hours with accept, maybe someone will come with a pure CSS solution. If not, I will of course accept it. Thanks for your time, and great reasearch. – Jacek Kowalewski Feb 06 '15 at 08:37
  • 3
    i tried this code on Linux and Chrome and console warning shows 0 (zero)... any idea what could be wrong? – mtx Jul 12 '17 at 19:00
  • 1
    @mtx Did you define the CSS for your scrollbar-measure class? It probably hadn't overflown. The jsfiddle should help. – Koh Sep 05 '17 at 18:24
  • very nice clean code... works on mac.. wonder if this works on mobile browsers as well!! – TacB0sS Sep 24 '18 at 19:23
  • 1
    This fails to account for the systems where the scrollbar width is dynamic, like on ma OS there is an option to only show the scrollbar when hovering. As there is no hover on your DIV, it thinks the scrollbar width is 0 – Ferrybig Nov 20 '18 at 14:33
19

innerWidth returns the width of the browser viewport including the scrollbar.

The read-only Window property innerWidth returns the interior width of the window in pixels. This includes the width of the vertical scroll bar, if one is present.

clientWidth returns the width of viewport, excluding the scrollbar, when used on a root element like document.

When clientWidth is used on the root element (the element), (or on if the document is in quirks mode), the viewport's width (excluding any scrollbar) is returned.

let scrollbarWidth = (window.innerWidth - document.body.clientWidth) + 'px';

Subtract the two values and voila, you get the width of the scrollbar.

This example would usually return: 15px

gummiost
  • 654
  • 7
  • 15
  • 1
    Adding an explanation would improve this answer – matt freake Jan 31 '20 at 17:53
  • Simplest and easiest method, using this to set a CSS variable on the body (or the appropriate element) and using it in the stylesheet has proven to be reliable on my end. – Raphael Parent Sep 16 '21 at 13:30
  • 1
    I believe this only works if the document.body has 100% width. It will not work if your document.body has a max-width. Dennis Icllchishin's works if your document.body has a max-width: `const scrollbarWidth = window.innerWidth - document.documentElement.offsetWidth` – Chris Jan 01 '22 at 22:23
  • just a tweak, can paste in dev tools to log: ```console.log(`scroll bar width: ${window.innerWidth - document.body.clientWidth}px`);``` – Wasit Shafi Apr 23 '23 at 12:54
14

Whilst tomtomtom's answer works fantastically, it requires an external CSS snippet that seems somewhat unnecessary, and Yurii's answer requires jQuery, which again seems sorta overkill for this. A slightly more up-to-date and self-contained answer (which can also be used in a JavaScript module setup) is:

class TempScrollBox {
  constructor() {
    this.scrollBarWidth = 0;

    this.measureScrollbarWidth();
  }

  measureScrollbarWidth() {
    // Add temporary box to wrapper
    let scrollbox = document.createElement('div');

    // Make box scrollable
    scrollbox.style.overflow = 'scroll';

    // Append box to document
    document.body.appendChild(scrollbox);

    // Measure inner width of box
    this.scrollBarWidth = scrollbox.offsetWidth - scrollbox.clientWidth;

    // Remove box
    document.body.removeChild(scrollbox);
  }

  get width() {
    return this.scrollBarWidth;
  }
}

This class can then be used like so:

let scrollbox = new TempScrollBox();

console.log(scrollbox.width) //-> 17 (Windows, Chrome)

Fiddle: https://jsfiddle.net/nu9oy1bc/

Oliver
  • 2,930
  • 1
  • 20
  • 24
8

Pure CSS solution here.
I would create an invisible div on the right of your text div with forced scrollbar inside it:

<div class="container">
  <div class="text">Your very and very long text here</div>
  <div class="scrollbar-width"></div>
</div>
<style>
.container {
  display: flex;
  width: 100px; /* to make scrollbar necessary */
  height: 30px; /* to make scrollbar necessary */
  border: 1px solid blue;
}
.text {
  flex: 1; 
  float: left;
  overflow-y: hidden; 
}
.scrollbar-width {
  float: left; 
  overflow-x: hidden; 
  overflow-y: scroll; 
  visibility: hidden;
}
.scrollbar-width::-webkit-scrollbar { /* for debugging */
  background-color: red;
}
.text:hover {
  overflow-y: scroll; 
}
.text:hover + .scrollbar-width {
  display: none;
}
</style>

When the scrollbar-width div is marked with visibility: hidden it will still take space.

Now when you finally decide to show your own scrollbar, add display: none to the invisible scrollbar-width div, so it does not take space anymore, and therefore your text div fills all available space and becomes wider.

In case you wish to verify, remove visibility: hidden; from the style of scrollbar-width div. Then you can see how this placeholder div is initially taking space (it appears red according to the "for debugging" style above) and on hover it disappears and is replaced by the text div-s scrollbar.

JS fiddle link: https://jsfiddle.net/rb1o0tkn/

Roland Pihlakas
  • 4,246
  • 2
  • 43
  • 64
8

My solution, works perfectly in latest Chrome, Mozilla, Edge, and Opera. Don't know about Safari

const scrollbarWidth = window.innerWidth - document.documentElement.offsetWidth

Update: On mobile devices browsers handle scrollbars slightly different, similar to overflow: overlay in some desktop browsers. So, this way doesn't give you actual scrollbar width, but the "shift" to element layout (html in the example above) that is caused by the scrollbar. Therefore, in this cases you might get 0, since there's no "shift" to the html element, because scrollbar is shown above the element

Denis Ilchishin
  • 171
  • 2
  • 4
  • most accurate solution – krulik Jan 14 '22 at 10:49
  • Unfortunately this is not reliable on mobile, as the browsers hide the scrollbar when not in use (i.e. you'll get `0`). – Andrei V Feb 01 '23 at 11:11
  • 1
    @AndreiV you're right. As well as recent `overflow: overlay`. It's not scrollbar width, but more like shift in pixels that is caused by scrollbar. I'll update my original answer – Denis Ilchishin Feb 02 '23 at 02:17
5

Complete solution to use within CSS

Old question and many JS solutions but noone realy shown how to use it within CSS.

Step 1.

Use any of the other answers to calculate the scrollbar width and save it as a root style property. I.e. in your JS file:

document.documentElement.style.setProperty('--scrollbar-width', (window.innerWidth - document.documentElement.offsetWidth) + 'px');

Step 2.

Use the newly created variable within your CSS. I.e. as per OP's original approach:

/* Paragraph margin predicted for future scrollbar on div */
div p {margin: 20px (20px + var(--scrollbar-width)) 20px 50px;}
/* With scrollbar margin is symmetrical */
div:hover p {margin: 20px;} /* With scrollbar */

Bonus tip: solution to common problem of expanding child inside relative DIV to full width of the screen. Paste inside the child you want to expand:

width: calc(100vw - var(--scrollbar-width));
margin-left: calc(-50vw + 50% + (var(--scrollbar-width)) / 2);
J. Wrong
  • 822
  • 11
  • 10
  • I would stick to this response, as it is in my opinion a short and a very clean solution, and in addition it adds a CSS variable to use. The scrollbar-gutter: stable; solution causes an empty space. – Ricardo Castañeda Jul 21 '23 at 06:11
4

I accidentally stumbled across this question and I've looked at the answers, they're all JS answers with the exception of 1 CSS answer. I'm pretty surprised no one has answered this correctly, so I'm assuming I've misunderstood the question but if I'm not mistaken, the OP wants a margin of scrollbar's width...

So, why not just do:

margin-right: calc(100vw - 100%);
margin-bottom: calc(100vh - 100%);

I don't think I even need to explain because it seems so obvious... the 100vw, will get the full width of the screen, whilst the 100% will get the width of the parent element (which for this to work, would need to be 100% also - the body tag is by default) and thus minus it would get the difference which should be the scrollbar. We can replace 100% with the fixed width or height of any divs (non-body tags), I'll fork your snippet, below, to show what I mean:

div.demo1 {
  width: 400px;
  height: 200px;
  overflow: hidden;
  background: #ddd;
}
div.demo1:hover {
  overflow: auto;
}
div.demo1 p {
  background: #eee;
  margin: 20px;
}
div.demo2 {
  width: 400px;
  height: 200px;
  overflow: hidden;
  background: #ddd;
}
div.demo2:hover {
  overflow: auto;
}
div.demo2 p {
  background: #eee;
  margin: 20px 40px 20px 20px;
}
div.demo2:hover p {
  margin: 20px calc(400px - 100%) 20px 20px;
}
<div class="demo1">
  <p>
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
  </p>
</div>
<br>
<br>
<strong>As you can see, on hover right side of the paragraph "moves" left. But I want something like:</strong>
<br>
<br>
<div class="demo2">
  <p>
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
    This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text. This is a long text.
  </p>
</div>

<strong>But with a "perfect" scrollbar width (in this example I used 20px)</strong>

And that's it... it looks identical to OP's 20px thing... It's been like 7 years but the answer seems so simple, I kinda want the OP to let me know if this is what they wanted or not...

Tigerrrrr
  • 526
  • 6
  • 24
  • This might not work if you have no scroll in the document. For example, content is tiny so it fits on your window height, or `overflow: 'hidden'` – Artem Matiushenko Aug 12 '22 at 08:16
2

I've tried tomtomtom's answer but it didn't works really well. So I modified the code a bit, try this if his answer doesn't works for you ;)

export const getScrollbarSize = () => {
  const scrollDiv = document.createElement("div");

  scrollDiv.className = "scrollbar-measure";
  scrollDiv.style.position = "fixed";
  scrollDiv.style.top = "0";
  scrollDiv.style.left = "0";
  scrollDiv.style.right = "0";
  scrollDiv.style.bottom = "0";

  document.body.appendChild(scrollDiv);

  // Get the scrollbar width
  const scrollbarWidth = window.innerWidth - scrollDiv.clientWidth;

  // Delete the DIV
  document.body.removeChild(scrollDiv);

  return scrollbarWidth;
};
Hoang Vu
  • 21
  • 1
  • 3
1

If someone, like me, found this question by googling "how to get browser scrollbar width", here is jQuery implementation without any additional CSS classes:

var div = $("<div style='overflow:scroll;position:absolute;top:-99999px'></div>").appendTo("body"),
    width = div.prop("offsetWidth") - div.prop("clientWidth");
div.remove();

console.log(width);

or JSFiddle, if you like.

Thanks tomtomtom's answer for the idea

Yurii Semeniuk
  • 933
  • 8
  • 13
1

All the solutions above did not work for me, because the actual scrollbar width on my machine is fractional (16.8 pixels), while the means proposed above (like window.innerWidth - document.body.clientWidth) operate integer (discrete) values only, thus returning 17 instead. This imprecision leads to quite negative visual effects in my page (I need to replace the scrollbar with a padding when necessary, and with this imprecision I have a layout shift every time I do it).

To resolve this, I had to write code like this:

export const getCurrentDocumentVerticalScrollbarWidth = () => {
  // Create a guaranteed 100% width element, and measure it's real width
  const fullWidthElement = document.createElement('div');
  fullWidthElement.style.height = '0px';
  fullWidthElement.style.width = '100%';
  fullWidthElement.style.visibility = 'hidden';

  document.body.appendChild(fullWidthElement);
  const clientWidth = Number(getComputedStyle(fullWidthElement).width.replace(/[^\d\.]/g, ''));
  document.body.removeChild(fullWidthElement);

  return document.documentElement.offsetWidth - clientWidth;
}

Here we add a temporary (invisible and fullwidth) div to DOM, measure it's real width in pixels, then remove it from DOM. Then compute the difference with window.innerWidth - and we are good to go.

With this you should be fine. Consider memoization though.

Kostiantyn Ko
  • 2,087
  • 1
  • 18
  • 16
1

There is a relatively new CSS property that's exactly supposed to solve this kind of issue:

scrollbar-gutter: stable;
Al Vedder
  • 17
  • 1
  • 1
0

It deserves to mention, that for Webkit/Blink browsers a simple one-liner is possible:

getComputedStyle(document.documentElement, '::-webkit-scrollbar').width

This works, since those browsers provide a ::-webkit-scrollbar pseudo element and getComputedStyle can retrieve the computed styling of those elements.

This does not work in Firefox, however.

CanonicEpicure
  • 444
  • 3
  • 16
0

@Al Vedder's answer is the best:

scrollbar-gutter: stable;

works for me, it does also require:

overflow: auto;

or

overflow-y: auto;

set on the element.

Documentation for this CSS: https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-gutter

I had spent on and off days to sort out column headers and column data alignments. This nails it. I have been able to get rid of around 45 lines of JavaScript code.

0

One possible solution that works in modern Chrome, Opera and Edge is:

In HTML

<div class="main-container disable-overflow-y"></div>

In CSS

.disable-overflow-y {
  overflow-y: hidden;
  height: 100%;
}

.enable-overflow-y {
  overflow-y: auto;
  height: 100%;
}

In Javascript

const el = document.querySelector('.main-container');
el.classList.remove('disable-overflow-y');
el.classList.add('enable-overflow-y');
const width = el.offsetWidth - el.scrollWidth;

It does not work in Firefox cause those two value are equal.

Nedo
  • 15
  • 5