10

I'm developing a web interface that involves a lot of scrolling, utilizing sticky positioning, which works beautifully except for one little detail. Below is a minimalist example. You can try it on CodePen to see how the stickies work when scrolling horizontally and vertically.

<header>
 <nav>
  <ul id=menu class=flex><li><a href="#">Menu bar item</a></li></ul>
 </nav>
 <div class=flex>
  <div id=button>Button</div>
  <div id=bar>Bar wider than the window</div>
 </div>
</header>
<main class=flex>
 <div id=leftbar></div>
 <div id=content></div>
</main>
*           {line-height: 2em; background-position: center center}
body        {margin: 0; padding: 0}
body, a     {color: #fff}
ul,li       {margin: 0; padding: 0; list-style-type: none}
.flex       {display: flex}
.flex > *   {flex: 0 0 auto}
header      {display: block; position: sticky; top: 0; left: 0; z-index: 1;
             width: min-content; min-width: 100%; background: #000066}
#menu       {flex-wrap: wrap; position: sticky; left: 0; width: 100vw;
             background: #006600}
#button     {position: sticky; left: 0; width: 4em; background: #000066}
#bar        {width: 150vw}
main        {min-height: 100vh} /* Ensure scrollbar */
#leftbar    {position: sticky; left: 0; width: 4em; background: #444444}
#content    {width: 150vw}
/* Background grid to visualize scrolling parts */
#bar, #leftbar, #content {
 background-image: url(data:image/gif;base64,R0lGODlhEAAQAIABAO7u7v///yH5BAEKAAEA
 LAAAAAAQABAAAAIdhI9pwe0PnnRxzmphlniz7oGbmJGWeXmU2qAcyxQAOw==)}

The problem is in the topmost menu bar, which should always remain in place like a fixed header but must be sticky. 100vw width works perfectly until scrolling all the way to the right. At that point the leftmost part of the menu bar scrolls out of view, presumably due to the vertical scrollbar not being counted in 100vw.

Fixed positioning is out of the question, because the height of the menu bar will vary, so that'd need a pile of very ugly JS reading offsetHeight and moving the elements below after every DOM change – yuck!

Surely there is a way to fix this tiny problem in pure CSS or by altering the document structure?

Edit: I see JS solutions starting to pour in, so let's put a stop to that with these 3 lines of JS that fix the problem easily. I'd just prefer a CSS/HTML solution to avoid code maintenance problems in the future.

var f=function() {document.getElementById('menu').style.width=document.body.clientWidth+'px';};
window.addEventListener('DOMContentLoaded',f);
window.addEventListener('resize',f);
Martin Laakso
  • 231
  • 1
  • 9
  • that's a tough one. May be you can set overflow-y hidden on the body and set it back to overflow in the actual content area. It might look a bit ugly but it will work. – karthick Apr 25 '19 at 13:15
  • Awesome example, but yes, it's a tricky one. Basically it boils down to the fact that 100vw is screenwidth + scrollbar width. So if you'd set `#menu { width: calc(100vw - 17px) }` (17px being roughly the scrollbar width on Windows) it'd work, but it's ugly as hell and not crossbrowser. I'll try to look for a better solution – elveti Apr 25 '19 at 13:19
  • Thanks for the ideas. I need to target several different Linux desktops in addition to Windows, so relying on a fixed scrollbar width is not possible. Perhaps I could set a precise width in load/resize events using JS, as that wouldn't get too ugly. – Martin Laakso Apr 25 '19 at 14:28

3 Answers3

0

As mentioned in the comment, the problem is that 100vw includes the scrollbar-width. There is not really an elegant solution to this, but a workaround would be to reduce your #menu width to 99vw, and then fill up the empty space with a pseudo-element with the same background-color.

#menu {
  width: 99vw;
  background: #006600;
}
#menu:before {
  content: '';
  width: 5vw;
  left: 100%;
  top: 0;
  bottom: 0;
  position: absolute;
  background: inherit;
}

Beware that this works only if you have a flat background-color (no image, no transparency) on your #menu, so I'm not sure if it helps your case, but it's the best crossbrowser solution I could come up with.

See: https://codepen.io/anon/pen/WWLedm

elveti
  • 2,316
  • 4
  • 20
  • 27
  • I was afraid I'd see this suggested. Unfortunately it breaks (or needs a width even less than 99vw) if I use this interface to visualize a small dataset in an iframe, for example. Colours are not an issue in this particular case. – Martin Laakso Apr 25 '19 at 14:07
  • 1
    I see. What if you'd set the #menu width to calc(100vw - 50px) and the width of the :before to 50px? That'd be more precise. But I don't know how it'd behave in an iframe – elveti Apr 25 '19 at 17:14
0

Update

Setting the width of the scrollbar to 15px and setting the width of your green menu bar to calc(100vw - 15px) might do this.

I don't know any way to achieve this with pure CSS. However, you can achieve what you need with vanilla JS.

I used the function provided by @lostsource in this SO question to get the width of the scrollbar.

Then I created a function - resizeMenuBar() - which sets the width of the menu bar to the width of window.innerWidth minus the width of the scrollbar.

I added two event listeners - one for window.load and another one for window.resize. The event handler is the resizeMenuBar function.

* {
  line-height: 2em;
  background-position: center center;
}

body {
  margin: 0;
  padding: 0;
}

body,
a {
  color: #fff;
}

ul,
li {
  margin: 0;
  padding: 0;
  list-style-type: none;
}

.flex {
  display: flex;
}

.flex>* {
  flex: 0 0 auto;
}

header {
  display: block;
  position: sticky;
  top: 0;
  left: 0;
  width: min-content;
  min-width: 100%;
  z-index: 1;
  background: #000066;
}

#menu {
  flex-wrap: wrap;
  position: sticky;
  left: 0;
  width: calc(100vw - 15px);
  background: #006600;
}

#button {
  position: sticky;
  left: 0;
  width: 4em;
  background: #000066;
}

#bar {
  width: 150vw;
}

main {
  min-height: 100vh;
}


/* Ensure scrollbar */

#leftbar {
  position: sticky;
  left: 0;
  width: 4em;
  background: #444444;
}

#content {
  width: 150vw;
}


/* Background grid to visualize scrolling parts */

#bar,
#leftbar,
#content {
  background-image: url(data:image/gif;base64,R0lGODlhEAAQAIABAO7u7v///yH5BAEKAAEALAAAAAAQABAAAAIdhI9pwe0PnnRxzmphlniz7oGbmJGWeXmU2qAcyxQAOw==);
}

body::-webkit-scrollbar {
  width: 15px;
}

body::-webkit-scrollbar-track {
  -webkit-box-shadow: inset 0 0 6px #ccc;
}

body::-webkit-scrollbar-thumb {
  background-color: grey;
  outline: 1px solid grey;
}
<header>
  <nav>
    <ul id=menu class=flex>
      <li><a href="#">Menu bar item</a></li>
    </ul>
  </nav>
  <div class=flex>
    <div id=button>Button</div>
    <div id=bar>Bar wider than the window</div>
  </div>
</header>
<main class=flex>
  <div id=leftbar></div>
  <div id=content></div>
</main>
Csaba Farkas
  • 794
  • 7
  • 11
  • This looks rather complicated, as I've fixed the issue in 3 lines of JS, but I'd still prefer a CSS solution to avoid code maintenance problems. – Martin Laakso Apr 26 '19 at 22:04
  • 1
    I agree, it would be so much better if there was a CSS solution for this. I can't see any use-cases when 100vw or 100vh should inlcude the width of the scrollbars. Would you mind sharing the JS you used to fix this, please? – Csaba Farkas Apr 26 '19 at 22:10
  • Also, I agree that including scrollbars in viewport units is awful. I haven't managed to find out what the people behind the specification were smoking. – Martin Laakso Apr 26 '19 at 22:33
  • I've given up trying to figure out why things work in CSS the way they work a long time ago. Actually, here is an idea. Did you try to style the scrollbar itself? I.e. giving it a width of 1vw and set your menu bar to 99vw. I know it's a hack, I'm not sure if it works and I'm also not sure about cross-browser compatibility. – Csaba Farkas Apr 26 '19 at 23:25
  • Most CSS specifications have made sense in some way, at least, but this one is beyond my comprehension. Styling the scrollbars sounds like a bad idea, as my interface will be used in so many different environments, browsers and browser engines within apps that I can't rely on any hacks. – Martin Laakso Apr 27 '19 at 00:05
0

The below hack seems to work fine only on chrome

Here is a hack where the idea is to increase the width of the parent of the sticky element to have enough room for the sticky behavior then use some translation to rectify the extra width and position:

* {
  line-height: 2em;
  background-position: center center;
}
body {
  margin: 0;
  padding: 0;
}
body,
a {
  color: #fff;
}
ul,
li {
  margin: 0;
  padding: 0;
  list-style-type: none;
}
.flex {
  display: flex;
}
.flex > * {
  flex: 0 0 auto;
}
header {
  display: block;
  position: sticky;
  top: 0;
  left: 0;
  width: min-content;
  min-width: 100%;
  z-index: 1;
  background: #000066;
}
/*addedd*/
nav {
  width: calc(100% + 60px);
  transform: translateX(-60px);
}
/**/
#menu {
  flex-wrap: wrap;
  position: sticky;
  left: 0;
  width: 100vw;
  background: #006600;
  transform: translateX(60px); /*added*/
}
#button {
  position: sticky;
  left: 0;
  width: 4em;
  background: #000066;
}
#bar {
  width: 150vw;
}
main {
  min-height: 100vh;
} /* Ensure scrollbar */
#leftbar {
  position: sticky;
  left: 0;
  width: 4em;
  background: #444444;
}
#content {
  width: 150vw;
}
/* Background grid to visualize scrolling parts */
#bar,
#leftbar,
#content {
  background-image: url(data:image/gif;base64,R0lGODlhEAAQAIABAO7u7v///yH5BAEKAAEALAAAAAAQABAAAAIdhI9pwe0PnnRxzmphlniz7oGbmJGWeXmU2qAcyxQAOw==);
}
<header>
 <nav>
  <ul id=menu class=flex>
   <li><a href="#">Menu bar item</a></li>
  </ul>
 </nav>
 <div class=flex>
  <div id=button>Button</div>
  <div id=bar>Bar wider than the window</div>
 </div>
</header>
<main class=flex>
 <div id=leftbar></div>
 <div id=content></div>
</main>

60px is a random value that should simply be bigger than the scroll width.

It's not trivial to get the trick but if you remove the translate you will have the following:

* {
  line-height: 2em;
  background-position: center center;
}
body {
  margin: 0;
  padding: 0;
}
body,
a {
  color: #fff;
}
ul,
li {
  margin: 0;
  padding: 0;
  list-style-type: none;
}
.flex {
  display: flex;
}
.flex > * {
  flex: 0 0 auto;
}
header {
  display: block;
  position: sticky;
  top: 0;
  left: 0;
  width: min-content;
  min-width: 100%;
  z-index: 1;
  background: #000066;
}
/*addedd*/
nav {
  width: calc(100% + 60px);
}
/**/
#menu {
  flex-wrap: wrap;
  position: sticky;
  left: 0;
  width: 100vw;
  background: #006600;
}
#button {
  position: sticky;
  left: 0;
  width: 4em;
  background: #000066;
}
#bar {
  width: 150vw;
}
main {
  min-height: 100vh;
} /* Ensure scrollbar */
#leftbar {
  position: sticky;
  left: 0;
  width: 4em;
  background: #444444;
}
#content {
  width: 150vw;
}
/* Background grid to visualize scrolling parts */
#bar,
#leftbar,
#content {
  background-image: url(data:image/gif;base64,R0lGODlhEAAQAIABAO7u7v///yH5BAEKAAEALAAAAAAQABAAAAIdhI9pwe0PnnRxzmphlniz7oGbmJGWeXmU2qAcyxQAOw==);
}
<header>
 <nav>
  <ul id=menu class=flex>
   <li><a href="#">Menu bar item</a></li>
  </ul>
 </nav>
 <div class=flex>
  <div id=button>Button</div>
  <div id=bar>Bar wider than the window</div>
 </div>
</header>
<main class=flex>
 <div id=leftbar></div>
 <div id=content></div>
</main>

The nav is overflowing which will logically increase the scroll. Imagine now that you will only scroll without considering the extra 60px. This will work fine but of course the user will not stop scrolling before 60px, so the trick is to move the oveflow to the left side by using translate.

Temani Afif
  • 245,468
  • 26
  • 309
  • 415