Shaggy Shaggy - 3 months ago 6
CSS Question

Accounting for Scrollbar Width When Positioning Element

I have a layout which has a maximum width and which is centred horizontally when the screen width is greater than that maximum. The layout includes a fixed header & menu; when the screen width is less than the max, the menu's

left
position is
0
and, when the screen width exceeds the max, the menu's
left
position needs to be flush with the left edge of the rest of the layout.

Here's how it should look:



*{box-sizing:border-box;margin:0;padding:0;}
header{
align-items:center;
background:#eee;
border-bottom:1px solid #999;
display:flex;
height:100px;
left:0;
justify-content:center;
padding:0 10px;
position:fixed;
right:0;
top:0;
}
h1{
height:60px;
position:relative;
width:calc(100% - 40px);
max-width:740px;
z-index:2;
}
img{
height:100%;
width:auto;
}
nav{
background:#eee;
border-right:1px solid #999;
bottom:0;
left:0;
position:fixed;
top:0;
width:300px;
z-index:1;
}
@media (min-width:801px){
nav{
border-left:1px solid #999;
left:calc((100% - 800px) / 2)
}
}
nav::before{background:#ecc;bottom:0;content:"";left:0;position:absolute;top:0;width:10px;}
nav::after{background:#999;content:"";height:1px;left:0;position:absolute;right:0;top:99px;}
main{background:#ddf;border-left:1px solid #99c;border-right:1px solid #99c;height:100vh;min-height:100%;margin:0 auto;width:100%;max-width:800px;}

<header>
<h1><img src="http://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/so-logo.png?v=9c558ec15d8a"></h1>
<nav></nav>
</header>
<main></main>





However, when a vertical scrollbar is introduced, a problem arises due to the fact that the scrollbar width is included in the width being checked for in the media query, resulting in a negative
left
position for the menu when the screen width is between 800px and 800-
x
px (where
x
is the width of the scrollbar). You can see this in the below Snippet (you'll probably need to view it full screen) by resizing your browser to slightly less than 800px - the right border of the menu gets a few pixels closer to the logo and the red edge of the menu is cropped.



*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:101%;}
header{
align-items:center;
background:#eee;
border-bottom:1px solid #999;
display:flex;
height:100px;
left:0;
justify-content:center;
padding:0 10px;
position:fixed;
right:0;
top:0;
}
h1{
height:60px;
position:relative;
width:calc(100% - 40px);
max-width:740px;
z-index:2;
}
img{
height:100%;
width:auto;
}
nav{
background:#eee;
border-right:1px solid #999;
bottom:0;
left:0;
position:fixed;
top:0;
width:300px;
z-index:1;
}
@media (min-width:801px){
nav{
border-left:1px solid #999;
left:calc((100% - 800px) / 2)
}
}
nav::before{background:#ecc;bottom:0;content:"";left:0;position:absolute;top:0;width:10px;}
nav::after{background:#999;content:"";height:1px;left:0;position:absolute;right:0;top:99px;}
main{background:#ddf;border-left:1px solid #99c;border-right:1px solid #99c;height:100vh;min-height:100%;margin:0 auto;width:100%;max-width:800px;}

<header>
<h1><img src="http://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/so-logo.png?v=9c558ec15d8a"></h1>
<nav></nav>
</header>
<main></main>





I understand what's happening and why it's happening but my question is: is there any way, using CSS alone, to prevent it from happening? I've tried using viewport units instead of percentages but that creates the problem in reverse; at certain screen widths, the logo moves a bit further over to the left, away from the menu's right border. If all browsers had identical scrollbar widths or allowed for custom styling of scrollbars, it would be easy to get around this but, unfortunately, neither is the case.

08/09/16: I've accepted my own answer for now as it was the best JavaScript solution I could come up with but I'm still on the hunt for a CSS solution.

Answer

For the benefit of anyone who comes across this question and is open to a JavaScript solution, here's a very simple one that forces the nav element's left position to 0 when the viewport's width is greater than 800px but the the body element's width is less than that.

The downsides of this solution are using JavaScript to achieve something that (in my opinion, at least) we should be able to do with CSS alone and the overhead of using an onresize listener. You can, however, remove that listener if you're only concerned about the menu being properly positioned onload (see commented JavaScript in Snippet).

((b,n,setmenu)=>{
    (setmenu=()=>n.dataset.forceleft=b.offsetWidth<800)();
    window.addEventListener("resize",setmenu,0);
})(document.body,document.querySelector("nav"));

// Use above to set menu position onload and onresize
// Or use below to only set menu position onload
//(d=>d.querySelector("nav").dataset.forceLeft=d.body.offsetWidth<800)(document);
nav{
    background:#eee;
    border-right:1px solid #999;
    bottom:0;
    left:0;
    position:fixed;
    top:0;
    width:300px;
    z-index:1;
}
@media (min-width:801px){
    nav[data-forceleft=false]{
        border-left:1px solid #999;
        left:calc((100% - 800px) / 2)
    }
}
header{
    align-items:center;
    background:#eee;
    border-bottom:1px solid #999;
    display:flex;
    height:100px;
    left:0;
    justify-content:center;
    padding:0 10px;
    position:fixed;
    right:0;
    top:0;
}
h1{
    height:60px;
    position:relative;
    width:calc(100% - 40px);
    max-width:740px;
    z-index:2;
}
img{
    height:100%;
    width:auto;
}
*{box-sizing:border-box;margin:0;padding:0;}
html,body{height:101%;}
nav::before{background:#ecc;bottom:0;content:"";left:0;position:absolute;top:0;width:10px;}
nav::after{background:#999;content:"";height:1px;left:0;position:absolute;right:0;top:99px;}
main{background:#ddf;border-left:1px solid #99c;border-right:1px solid #99c;height:100vh;min-height:100%;margin:0 auto;width:100%;max-width:800px;}
<header>
    <h1><img src="http://cdn.sstatic.net/Sites/stackoverflow/company/img/logos/so/so-logo.png?v=9c558ec15d8a"></h1>
    <nav></nav>
</header>
<main></main>