CSS 100% width but avoid scrollbar

Use overflow: scroll instead of overflow: auto - that'll force a scrollbar to always appear.


The answer by Mattias Ottosson to another question offers a crucial piece of information - the vw units are based on the viewport width including the scrollbar, while percentages will be based on the available width which doesn't include the space taken up by the scrollbar. In other words, for an element taking up the full width of the page, the width of the scroll bar can represented as calc(100vw - 100%)

If we have a top-level element taking up 100% of the available width, then we can use this to control what changes size when the scrollbar becomes visible. Let's say our goal layout is something like this:

.app {
  display: grid;
  grid-template-columns: 1fr 50vh 1fr;
}

Where we want the middle column to be 50% as wide as the viewport height and the rest of the width divided between the left and right column. If we used that, then the addition of a scrollbar means that the horizontal space lost to the scrollbar (about 15px on chrome) is taken out of the width of the left and right columns equally. This can cause an ugly shift when a ui change causes the scrollbar to appear while the main content in the grid remains the same or similar. See the first snippet below.

We can use the calculated width of the scrollbar to instead only shrink the right column:

.app {
  display: grid;
  grid-template-columns: calc((100vw - 50vh)/2) 50vh calc(100% - (50vh + 100vw)/2);
}

See the second snippet below. Unfortunately this means the fr units can't be used and the width of the columns must be specified a little more manually. In this case the width of the left column is half of the viewport width minus the 50vh taken up by the center column. The width of the right column is the space remaining from the available width (100% rather than 100vw) after subtracting the combined width of the left and center columns. This is clearer in the formula:

calc(100% - ((100vw - 50vh)/2) - (50vh))

which reduces to the one above

First snippet, ugly jump when scrollbar appears

$('button').click(() => {
  $('.footer').toggle()
})
body, html {
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
  overflow: auto;
  font-family: 'Archivo', sans-serif ;
}
.app {
  margin: auto;
  display: grid;
  grid-template-columns: 1fr 50vh 1fr;
  text-align: center;
  height: 100%;
  width: calc(100% - 10px);
}
.left-column, .center-column, .right-column {
  padding: 10px;
  min-height: 50vh;
  border: 1px solid black;
}
.left-column {
  border-right: none;
  background-color:#def;
}
.center-column {
  background-color:#e1ebbd;
}
.right-column {
  text-align: left;
  border-left: none;
  background: #fb1;
}
.footer {
  grid-column: 1 / span 3;
  height: 2000px;
  background: #753;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

</head>
<body>
  <div class="app">
    <div class="left-column">
      Left
    </div>
    <div class="center-column">
      Center
<script src="https://code.jquery.com/jquery-3.1.0.js"></script><br>
      <button>Toggle footer</button>
    </div>
    <div class="right-column">
      Right
    </div>
    <div class="footer">
 </div>
  </div>
</body>
</html>

second snippet, right column shrinks when scrollbar appears

$('button').click(() => {
  $('.footer').toggle()
})
body, html {
  height: 100%;
  width: 100%;
  padding: 0;
  margin: 0;
  overflow: auto;
  font-family: 'Archivo', sans-serif ;
}
.app {
  margin: auto;
  display: grid;
  grid-template-columns: calc((100vw - 50vh)/2) 50vh calc(100% - (50vh + 100vw)/2);
  text-align: center;
  height: 100%;
  width: calc(100% - 10px);
}
.left-column, .center-column, .right-column {
  padding: 10px;
  min-height: 50vh;
  border: 1px solid black;
}
.left-column {
  border-right: none;
  background-color:#def;
}
.center-column {
  background-color:#e1ebbd;
}
.right-column {
  text-align: left;
  border-left: none;
  background: #fb1;
}
.footer {
  grid-column: 1 / span 3;
  height: 2000px;
  background: #753;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>

</head>
<body>
  <div class="app">
    <div class="left-column">
      Left
    </div>
    <div class="center-column">
      Center
<script src="https://code.jquery.com/jquery-3.1.0.js"></script><br>
      <button>Toggle footer</button>
    </div>
    <div class="right-column">
      Right
    </div>
    <div class="footer">
 </div>
  </div>
</body>
</html>

The only way you can "get" and use the scrollbar-width with pure CSS is to actually have the scrollbar be there. Now, we don't want to force the scrollbar be visible all the time, so what we have to do is this: Make a container for all of the website's content that always has the scrollbar shown, and hide it. It's surprisingly simple!
I've created a Fiddle. Here it is as a snippet:

/* The trick: */

html {
  overflow-x: hidden;
}

body {
  margin: 0;
  width: 100vw;
}

body > * {
  overflow-y: scroll;
  margin-right: -100px;
  padding-right: 100px;
}

/* Other styling: */

body {
  font-family: sans-serif;
  user-select: none;
  --color: rgb(255 191 191);
}

header {
  position: sticky;
  top: 0;
  z-index: 1;
  --color: rgb(191 191 255);
}

body > * > div {
  background-color: var(--color);
  border: 3px solid;
  margin: 10px;
  padding: 20px;
  font-size: 20px;
  font-weight: bold;
}

label::before {
  position: relative;
  content: '';
  display: inline-block;
  width: 1em;
  height: 1em;
  margin: 0 10px;
  top: .2em;
  border: 1px solid;
  border-radius: .1em;
}

input:checked + label::before {
  background-color: var(--color);
  box-shadow: inset 0 0 0 1px #FFF;
}

input {
  display: none;
}

input:not(:checked) ~ div {
  display: none;
}

input ~ div {
  height: 200vh;
}
<header>
  <div>I am sticky!</div>
</header>
<main>
  <div>Hello world!</div>
  <input id="foo-2" type="checkbox" />
  <label for="foo-2">Click me</label>
  <div>Let's scroll</div>
</main>

The trick is giving the containing elements a negative margin and positive padding to the right. The offset used for these two properties can exceed the scrollbar-width, so making it 100px is plenty — I can't imagine any browser or website having scrollbars wider than 20px, let alone 100px.


By the way: The reason I'm applying these styles to every direct child of body, instead of having a single #container element, is because otherwise position: sticky wouldn't work. To have that feature work on an element, it can only have one anscestor with scrolling capabilities.

html contains #container contains sticky element -> does not work

html contains sticky container -> does work

Tags:

Css