I now understand what is happening. The issue is the different way the browser treats the width and height of a <div>
. The default values of auto
mean that the width of the <div>
is 100%
while the height is set by the content. If the content is wider than 100%
, then on horizontal scroll the sticky element hits the end of the container <div>
and, since it cannot leave the confines of the container, begins to scroll. This doesn't happen in the same situation for vertical scrolling since the container <div>
is as tall as the content by default.
To prevent this happening, we have to ensure that the container <div>
is as wide as its content. This can be done in most browsers (not Edge or Explorer) by including width: max-content
in the container style. Alternatively, as proposed in mfluehr's answer, putting overflow: auto
creates a new block formatting context that is as wide as the content. Another option is to use display: inline-block
or inline-flex
etc. to cause the container <div>
to base its width on the content.
For example, using two of these techniques, you can create headers, sidebars and footers that stick for a page that can scroll vertically and horizontally:
body {
padding: 0;
margin: 0;
}
#app {
overflow: auto;
height: 100vh;
}
#header {
background: blue;
width: 100%;
height: 40px;
position: sticky;
top: 0;
left: 0;
z-index: 10;
color: white;
}
#sidebar {
position: sticky;
background: green;
width: 200px;
height: calc(100vh - 40px);
top: 40px;
left: 0;
color: white;
flex-grow: 0;
flex-shrink: 0;
}
#container {
display: inline-flex;
}
#content {
background: #555;
height: 200vh;
width: 200vw;
background: linear-gradient(135deg, #cc2, #a37);
flex-grow: 0;
flex-shrink: 0;
}
#footer {
background: #000;
height: 100px;
z-index: 100;
left: 0;
position: sticky;
color: white;
}
<div id="app">
<div id="header" ref="header">
Header content
</div>
<div id="container">
<div id="sidebar" ref="sidebar">
Sidebar content
</div>
<div id="content" ref="content">
Page content
</div>
</div>
<div id="footer" ref="footer">
Footer content
</div>
</div>