Box shadow for sticky header [duplicate] - css

position: sticky works on some mobile browsers now, so you can make a menu bar scroll with the page but then stick to the top of the viewport whenever the user scrolls past it.
But what if you want to restyle your sticky menu bar slightly whenever it's currently 'sticking'? eg, you might want the bar to have rounded corners whenever it's scrolling with the page, but then as soon as it sticks to the top of the viewport, you want to get rid of the top rounded corners, and add a little drop shadow underneath it.
Is there any kind of pseudoselector (eg ::stuck) to target elements that have position: sticky and are currently sticking? Or do browser vendors have anything like this in the pipeline? If not, where would I request it?
NB. javascript solutions are not good for this because on mobile you usually only get a single scroll event when the user releases their finger, so JS can't know the exact moment that the scroll threshold was passed.

There is currently no selector that is being proposed for elements that are currently 'stuck'. The Postioned Layout module where position: sticky is defined does not mention any such selector either.
Feature requests for CSS can be posted to the www-style mailing list. I believe a :stuck pseudo-class makes more sense than a ::stuck pseudo-element, since you're looking to target the elements themselves while they are in that state. In fact, a :stuck pseudo-class was discussed some time ago; the main complication, it was found, is one that plagues just about any proposed selector that attempts to match based on a rendered or computed style: circular dependencies.
In the case of a :stuck pseudo-class, the simplest case of circularity would occur with the following CSS:
:stuck { position: static; /* Or anything other than sticky/fixed */ }
:not(:stuck) { position: sticky; /* Or fixed */ }
And there could be many more edge cases that would be difficult to address.
While it's generally agreed upon that having selectors that match based on certain layout states would be nice, unfortunately major limitations exist that make these non-trivial to implement. I wouldn't hold my breath for a pure CSS solution to this problem anytime soon.

In some cases a simple IntersectionObserver can do the trick, if the situation allows for sticking to a pixel or two outside its root container, rather than properly flush against. That way when it sits just beyond the edge, the observer fires and we're off and running.
const observer = new IntersectionObserver(
([e]) => e.target.toggleAttribute('stuck', e.intersectionRatio < 1),
{threshold: [1]}
);
observer.observe(document.querySelector('nav'));
Stick the element just out of its container with top: -2px, and then target via the stuck attribute...
nav {
background: magenta;
height: 80px;
position: sticky;
top: -2px;
}
nav[stuck] {
box-shadow: 0 0 16px black;
}
Example here: https://codepen.io/anon/pen/vqyQEK

I wanted a pure CSS solution that would allow styling a 'stuck' element, as though a ::stuck pseudo-selector exists (alas, still not in 2021).
I have created a pure CSS hack that achieves the effect with no JS and fits my needs. It works by having two copies of the element, one is sticky and the other isn't (unstuck one), and this latter one covers up the sticky element until you scroll by it.
Demo: https://codepen.io/TomAnthony/pen/qBqgErK
Alternative demo: https://codepen.io/TomAnthony/pen/mdOvJYw (this version is more what I wanted, I wanted the sticky items to only appear once they were 'stuck' - it also means no duplicate content.)
HTML:
<div class="sticky">
<div class="unstuck">
<div>
Box header. Italic when 'stuck'.
</div>
</div>
<div class="stuck">
<div>
Box header. Italic when 'stuck'.
</div>
</div>
</div>
CSS:
.sticky {
height: 20px;
display: inline;
background-color: pink;
}
.stuck {
position: -webkit-sticky;
position: sticky;
top: 0;
height: 20px;
font-style: italic;
}
.unstuck {
height: 0;
overflow-y: visible;
position: relative;
z-index: 1;
}
.unstuck > div {
position: absolute;
width: 100%;
height: 20px;
background-color: inherit;
}

Someone on the Google Developers blog claims to have found a performative JavaScript-based solution with an IntersectionObserver.
Relevant code bit here:
/**
* Sets up an intersection observer to notify when elements with the class
* `.sticky_sentinel--top` become visible/invisible at the top of the container.
* #param {!Element} container
*/
function observeHeaders(container) {
const observer = new IntersectionObserver((records, observer) => {
for (const record of records) {
const targetInfo = record.boundingClientRect;
const stickyTarget = record.target.parentElement.querySelector('.sticky');
const rootBoundsInfo = record.rootBounds;
// Started sticking.
if (targetInfo.bottom < rootBoundsInfo.top) {
fireEvent(true, stickyTarget);
}
// Stopped sticking.
if (targetInfo.bottom >= rootBoundsInfo.top &&
targetInfo.bottom < rootBoundsInfo.bottom) {
fireEvent(false, stickyTarget);
}
}
}, {threshold: [0], root: container});
// Add the top sentinels to each section and attach an observer.
const sentinels = addSentinels(container, 'sticky_sentinel--top');
sentinels.forEach(el => observer.observe(el));
}
I haven't replicated it myself, but maybe it helps someone stumbling over this question.

Not really a fan of using js hacks for styling stuff (ie getBoudingClientRect, scroll listening, resize listening), but this is how I'm currently solving the problem. This solution will have issues with pages that have minimizable/maximizable content (<details>), or nested scrolling, or really any curve balls whatsoever. That being said, it's a simple solution for when the problem is simple as well.
let lowestKnownOffset: number = -1;
window.addEventListener("resize", () => lowestKnownOffset = -1);
const $Title = document.getElementById("Title");
let requestedFrame: number;
window.addEventListener("scroll", (event) => {
if (requestedFrame) { return; }
requestedFrame = requestAnimationFrame(() => {
// if it's sticky to top, the offset will bottom out at its natural page offset
if (lowestKnownOffset === -1) { lowestKnownOffset = $Title.offsetTop; }
lowestKnownOffset = Math.min(lowestKnownOffset, $Title.offsetTop);
// this condition assumes that $Title is the only sticky element and it sticks at top: 0px
// if there are multiple elements, this can be updated to choose whichever one it furthest down on the page as the sticky one
if (window.scrollY >= lowestKnownOffset) {
$Title.classList.add("--stuck");
} else {
$Title.classList.remove("--stuck");
}
requestedFrame = undefined;
});
})

A compact way for when you have an element above the position:sticky element. It sets the attribute stuck which you can match in CSS with header[stuck]:
HTML:
<img id="logo" ...>
<div>
<header style="position: sticky">
...
</header>
...
</div>
JS:
if (typeof IntersectionObserver !== 'function') {
// sorry, IE https://caniuse.com/#feat=intersectionobserver
return
}
new IntersectionObserver(
function (entries, observer) {
for (var _i = 0; _i < entries.length; _i++) {
var stickyHeader = entries[_i].target.nextSibling
stickyHeader.toggleAttribute('stuck', !entries[_i].isIntersecting)
}
},
{}
).observe(document.getElementById('logo'))

Related

How can I change CSS pseudo-element property dynamically?

I understood:
You can't modify pseudo elements through JavaScript since they are not part of the DOM
I also knew we could add properties in pseudo-element by appending style -- more.
However, the appending solution could only add value. adding doesn't mean the ability of changing dynamically. I also need to the ability of replacing the property value.
Therefore, I tried to use attr() to change background image dynamically. However, currently attr only supports content property - more.
So what else I can try here?
In order to adding more context of the question, basically, I want to dynamically update avatar image in chat. The avatar image is setted in pseudo-element(before and after). Here is the code-pen of Chat UI -- http://codepen.io/clintioo/pen/HAkjq
Thank you very much!
As said in other answers, there is a way to inject styles that will afect your pseudo element.
A somewhat simpler workaround for your specific case could be to just inherit the background from the base element (since you are not using it)
function changebkg () {
target1 = document.getElementById('test');
target1.style.backgroundImage = "url(http://placekitten.com/1000/750)";
}
#test {
font-size: 40px;
position: relative;
display: inline-block;
color: black;
background-size: 0px;
background-color: lightblue;
}
#test:after {
content: "";
position: absolute;
left: 200px;
top: 20px;
width: 200px;
height: 200px;
background-image: inherit;
border: solid 1px;
background-size: contain;
}
<div id="test" onclick="changebkg();">click me</div>
Here is the part in the "duplicate" that shows how to get, add and change CSS pseudo-element property dynamically using Javascript.
Stack snippet
/* on page load */
window.addEventListener('load', function() {
/* get button and add click event to it */
var btn = document.querySelector('button');
btn.addEventListener("click", function(){
/* get first <p> element */
var el1 = document.querySelector('p.first');
/* get second <p> element */
var el2 = document.querySelector('p.second');
/* get first <p> pseudo's "content" property value */
var str = window.getComputedStyle(el1,':before').getPropertyValue('content');
/* get first <p> pseudo's "color" property value */
var col = window.getComputedStyle(el1,':before').getPropertyValue('color');
/* dynamically add a rule to the stylesheet so the second <p>
will get the same "content"/"color" value as the first */
document.styleSheets[0].addRule('p.second:before', 'content: ' + str + ' ; color: ' + col + ';');
/* dynamically add a rule to the stylesheet that override the
first <p> "color" to green */
document.styleSheets[0].addRule('p.first:before', 'color: green;');
});
});
p.first:before {
content:"foo";
color: red;
}
button {
display: block;
width: 220px;
margin-top: 50px;
}
<p class="first">This is a paragraph</p>
<p class="second">This is another paragraph</p>
<button>Change/Add pseudo properties</button>
As you have just learned, attr() isn't supported anywhere else but the content property at the moment. If your requirement is to set other CSS properties dynamically, then I'm afraid you can't do much of that within CSS alone. The closest you can get is to generate static CSS dynamically through a script, as shown in a number of answers to the first question you link to.
In the specific case of user avatars, I don't see why you aren't just marking those up using img elements instead, which would obviate this issue entirely.
You can create a css file and then inject it into the DOM.
var avatar_css_for_avatar = document.createElement('style');
avatar_css_for_avatar.type = 'text/css';
avatar_css_for_avatar.innerHTML = '.user-avatar-id:before { background-image: url(url_of_image); }';
document.getElementsByTagName('head')[0].appendChild(avatar_css_for_avatar);
document.getElementById('someElementId').className = 'user-avatar-id';

Why does IE freeze on Layout when rendering an empty SVG element?

I am currently hitting an issue in IE 10 and 11 where the browser tab is hanging every now and then on Layout in the UI Responsiveness tool. I am part of a team writing a fairly large knockout.js web app, so nailing down the exact condition that is creating this issue has been extremely difficult. From what I can tell, the browser tab hangs when Layout is performed when the removal of loading indicator HTML is removed from the page and some divs plus an empty SVG tag is appended to the DOM in its place.
I have been able to nail down that the empty SVG tag is the culprit, but I do not know why and I cannot remove that tag from the page is it is an important element to a D# data visualization that I am trying to create.
Here is the US Responsiveness report that IE 11 has provided me. I have zoomed in on the problematic area, and as you can see in the picture, the Layout thread spikes the CPU to 100%.
Before I get into the code samples my question is:
Why would the browser tab intermittently freeze/hang from adding an empty SVG element to the page?
The HTML gets appended to the DOM via javascript in as minimal of a way as possible from my research on reducing reflow in the browser:
var contentHTML = "";
contentHTML += '<div class="axis-title y-axis-title">' + renderString(bindingData.yAxis.title) + "</div>";
contentHTML += '<div class="' + CANVAS_CLASS + '"></div>';
contentHTML += '<svg class="x-axis"></svg>'; // The problematic element
element.innerHTML = contentHTML;
This results in the following HTML (note: all of the data-bind stuff is for knockout.js binding handlers, which triggers the JS above):
<div class="chart" data-bind="
barChart: {
data: rowData,
categoryTextKey: 'label',
valueKey: 'keyOnObject',
xAxis: {
title: 'xAxisTitle',
domain: [-1, 1]
},
yAxis: {
title: 'yAxisTitle'
},
onClick: onLabelClick,
formatValueText: formatPercentage
}
"></div>
<div class="axis-title y-axis-title">Y Title</div>
<div class="chart-canvas"></div>
<svg xmlns="http://www.w3.org/2000/svg" class="x-axis" />
<div class="axis-title x-axis-title">X Title</div>
</div>
Lastly, I also am using flexbox CSS rules to lay out my HTML. I am not sure if that is affecting this issue, but here is the CSS in case it helps:
.chart {
.flexbox();
.flex-direction(column);
height: 100%;
width: 100%;
.chart-label-click {
cursor: pointer;
}
.chart-header,
.axis-title,
.x-axis {
.flex-grow(0);
.flex-shrink(0);
}
.chart-canvas {
.flex-grow(1);
.flex-shrink(1);
overflow-x: hidden;
overflow-y: auto;
width: 100%;
}
.chart-canvas svg {
display: block;
width: 100%;
}
.axis-title {
font-weight: bold;
}
.x-axis {
.flexbox();
.flex-grow(0);
.flex-basis(20px);
margin-bottom: 5px;
overflow: visible;
width: 100%;
}
.x-axis line,
.x-axis path {
fill: none;
stroke: #d1d1d1;
stroke-width: 1px;
shape-rendering: crispEdges;
}
}
Thank you for any help you may have. I am not sure how to nail this down is it is intermittent in one section of our app and our codebase is pretty big to figure out the exact combination of code in other files that may also be contributing to this issue.
The described issue seems to be this bug:
https://connect.microsoft.com/IE/feedback/details/796745/mouse-events-are-not-delivered-at-all-anymore-when-inside-an-svg-a-use-is-removed-from-the-dom
In the comments is a workaround described which at least worked for us:
You have to set style="pointer-events: none;" on the use elements.
Or simply add this to your css:
svg use { pointer-events:none; }
But be aware that this also disables any mouse events triggered on the use element.
The way I ultimately fixed this issue was to remove the use of display:flex on the .chart element. In its place, I used a fixed height and display:block. It looks like this is ultimately a bug w/ IE when mixing SVG and flexbox together.
Make sure your code isn't setting a value in JavaScript (or other language) without even the quotes such as the following...
var a = ;//[var][space][a][space][=][space][;]
That will freeze up IE11 (not sure about 10 offhand).
After many days of searching I decided to solve the problem in addressing this:
svg use { pointer-events:none; }

AngularJS Single page app: google maps reloading weirdness

Hi I am trying to build a angular single page app for mobile that uses a map on one page. It also should include a sticky footer, and is based on bootstrap. The sticky footer css interferes with the css needed to get the map to take up all of the remaining screen space, so I add a class='map' to the html element to override certain css elements (see below).
Everything works nicely until I go to the map page, leave it and then return to the map page. In this instance the map is not working correctly at all. It is hard to describe, so please try the plnkr.
I have found CSS that works for the map reloading, but then that breaks something else in the site. It is driving me crazy trying to combine the two models, hence my appeal for help.
Update: I have now found that resizing the screen rectifies the rendering issues, until you leave and return to the map. Of course a mobile use cannot change their screen size, but this may help find a solution.
html {
position: relative;
min-height: 100%;
}
html.map {
height: 100%
}
body {
/* Margin bottom by footer height */
margin-bottom: 60px;
}
.map body {
/* For Google map */
height: 100%;
margin-bottom: 0;
padding-bottom: 60px;
padding-top: 60px
}
footer {
position: absolute;
bottom: 0;
width: 100%;
/* Set the fixed height of the footer here */
height: 60px;
background-color: #f5f5f5;
}
header {
width: 100%;
background-color: #ccc;
height: 60px;
top: 0;
}
.map header {
position: absolute;
}
UPDATE
I implemented a solution similar to yours, which I found in this blog article. Essentially, you have to trigger a resize event in order to have the map repainted correctly when it goes from hidden to visible.
But I put my code into a directive instead of a controller (doesn't bloat controller and decorates the element it affects), instead of adding a watcher it runs only after the directive/element is linked (more performant), and it doesn't require you to re-enter your coordinates in order to refresh:
.directive('autoRefresh', function($timeout){
return {
restrict: 'A',
link: function(scope, elem, attrs){
$timeout(function(){
var center = scope.map.getCenter();
google.maps.event.trigger(scope.map, "resize");
scope.map.setCenter(center);
});
}
}
})
Updated Plunker
OK, so what I was missing was to trigger the resize event. This now works perfectly in my plunker but not yet in my more complex actual code. Nearly there!
restosApp.controller('mapCtrl', function($scope) {
$scope.$watch('map', function() {
google.maps.event.trigger($scope.map, 'resize');
var ll = new google.maps.LatLng(52.374, 4.899);
$scope.map.setCenter(ll);
});
});

how can I show Loading Div with respect to the control I have clicked?

I am having hard time finding a way to display a loading image in a Div right next to a control they have clicked while waiting for a request.I am using a UpdateProgress Bar but it is supposed to be placed at a certain place on the page.I want to display it next to any control they select.Just like how it is on Facebook.
Closest thing I can think of is a absolute positioned div which is placed next to each element. However it doesn't seem too valid since if the loading thingie is to the right of every progress link, it might overlap etc.
I'm quite sure that Facebook made all these progress bars prepositioned and hidden, and not just one div for all of them.
I guess you could try something like:
<div id="progress_bar"></div>
<style type="text/css">
div#progress_bar {
background: url('the_url_for_your_gif.gif') no-repeat center;
display: none;
height: 20px; /* Adjust yourself */
position: absolute;
width: 30px; /* Adjust yourself */
}
</style>
<script type="text/javascript">
var extra = 10; /* 10px extra for space */
var prog_top = 0;
var prog_left = 0;
$(function() {
$('a').live('click', function() {
var h = $(this).offset();
prog_top = h.top;
prog_left = (h.left + $(this).width() + extra);
});
});
function progress_thingie(loading) {
if (loading)
$('div#progress_bar').css({
'top' : prog_top,
'left' : prog_left
});
$('div#progress_bar').showToggle(loading);
}
</script>
Basicly this would save positions on every <a> you press and use those positions for the progress div.
Simply call the javascript function progress_thingie() and send a TRUE or FALSE to it, depending on if it's loading or done.

Prevent body scrolling but allow overlay scrolling

I've been searching for a "lightbox" type solution that allows this but haven't found one yet (please, suggest if you know of any).
The behavior I'm trying to recreate is just like what you'd see at Pinterest when clicking on an image. The overlay is scrollable (as in the whole overlay moves up like a page on top of a page) but the body behind the overlay is fixed.
I attempted to create this with just CSS (i.e. a div overlay on top of the whole page and body with overflow: hidden), but it doesn't prevent div from being scrollable.
How to keep the body/page from scrolling but keep scrolling inside the fullscreen container?
Theory
Looking at current implementation of the pinterest site (it might change in the future), when you open the overlay, a noscroll class is applied to the body element (setting overflow: hidden) making the body no longer scrollable.
The overlay created on-the-fly or already injected in the page and made visible via display: block — it makes no difference – has position : fixed and overflow-y: scroll, with top, left, right and bottom properties set to 0: this style makes the overlay fill the whole viewport (but now we are in 2022, so you may use inset: 0 instead).
The div inside the overlay is in position: static so the vertical scrollbar is related to that element. This is resulting in a scrollable but fixed overlay.
When you close the overlay, you have to hide it (using display: none) and you could even remove the node via javascript (or just the content inside, it's up to you but also depends on the nature of the content).
The final step is to also remove the noscroll class applied to the body (so the overflow property gets back to the value it had previously)
Code
Codepen Example
(it works by changing the aria-hidden attribute of the overlay in order to show and hide it and to increase its accessibility).
Markup
(open button)
<button type="button" class="open-overlay">OPEN LAYER</button>
(overlay and close button)
<section class="overlay" aria-hidden="true" tabindex="-1">
<div>
<h2>Hello, I'm the overlayer</h2>
...
<button type="button" class="close-overlay">CLOSE LAYER</button>
</div>
</section>
CSS
.noscroll {
overflow: hidden;
}
.overlay {
position: fixed;
overflow-y: scroll;
inset: 0; }
[aria-hidden="true"] { display: none; }
[aria-hidden="false"] { display: block; }
Javascript (vanilla-JS)
var body = document.body,
overlay = document.querySelector('.overlay'),
overlayBtts = document.querySelectorAll('button[class$="overlay"]'),
openingBtt;
[].forEach.call(overlayBtts, function(btt) {
btt.addEventListener('click', function() {
/* Detect the button class name */
var overlayOpen = this.className === 'open-overlay';
/* storing a reference to the opening button */
if (overlayOpen) {
openingBtt = this;
}
/* Toggle the aria-hidden state on the overlay and the
no-scroll class on the body */
overlay.setAttribute('aria-hidden', !overlayOpen);
body.classList.toggle('noscroll', overlayOpen);
/* On some mobile browser when the overlay was previously
opened and scrolled, if you open it again it doesn't
reset its scrollTop property */
overlay.scrollTop = 0;
/* forcing focus for Assistive technologies but note:
- if your modal has just a phrase and a button move the
focus on the button
- if your modal has a long text inside (e.g. a privacy
policy) move the focus on the first heading inside
the modal
- otherwise just focus the modal.
When you close the overlay restore the focus on the
button that opened the modal.
*/
if (overlayOpen) {
overlay.focus();
}
else {
openingBtt.focus();
openingBtt = null;
}
}, false);
});
/* detect Escape key when the overlay is open */
document.body.addEventListener('keyup', (ev) => {
if (ev.key === "Escape" && overlay.getAttribute('aria-hidden') === 'false') {
overlay.setAttribute('aria-hidden', 'true');
body.classList.toggle('noscroll', false);
openingBtt.focus();
openingBtt = null;
}
})
Finally, here's another example in which the overlay opens with a fade-in effect by a CSS transition applied to the opacity property. Also a padding-right is applied to avoid a reflow on the underlying text when the scrollbar disappears.
Codepen Example (fade)
CSS
.noscroll { overflow: hidden; }
#media (min-device-width: 1025px) {
/* not strictly necessary, just an experiment for
this specific example and couldn't be necessary
at all on some browser */
.noscroll {
padding-right: 15px;
}
}
.overlay {
position: fixed;
overflow-y: scroll;
inset: 0;
}
[aria-hidden="true"] {
transition: opacity 1s, z-index 0s 1s;
width: 100vw;
z-index: -1;
opacity: 0;
}
[aria-hidden="false"] {
transition: opacity 1s;
width: 100%;
z-index: 1;
opacity: 1;
}
overscroll-behavior css property allows to override the browser's default overflow scroll behavior when reaching the top/bottom of content.
Just add the following styles to overlay:
.overlay {
overscroll-behavior: contain;
...
}
Codepen demo
Currently works in Chrome, Firefox and IE(caniuse)
For more details check google developers article.
If you want to prevent overscrolling on ios, you can add position fixed to your .noscroll class
body.noscroll{
position:fixed;
overflow:hidden;
}
Most solutions have the problem that they do not retain the scroll position, so I took a look at how Facebook does it. In addition to setting the underlaying content to position: fixed they also set the top dynamically to retain the scroll position:
scrollPosition = window.pageYOffset;
mainEl.style.top = -scrollPosition + 'px';
Then, when you remove the overlay again, you need to reset the scroll position:
window.scrollTo(0, scrollPosition);
I created a little example to demonstrate this solution
let overlayShown = false;
let scrollPosition = 0;
document.querySelector('.toggle').addEventListener('click', function() {
if (!overlayShown) {
showOverlay();
} else {
removeOverlay();
}
overlayShown = !overlayShown;
});
function showOverlay() {
scrollPosition = window.pageYOffset;
const mainEl = document.querySelector('.main-content');
mainEl.style.top = -scrollPosition + 'px';
document.body.classList.add('show-overlay');
}
function removeOverlay() {
document.body.classList.remove('show-overlay');
window.scrollTo(0, scrollPosition);
const mainEl = document.querySelector('.main-content');
mainEl.style.top = 0;
}
.main-content {
background-image: repeating-linear-gradient( lime, blue 103px);
width: 100%;
height: 200vh;
}
.show-overlay .main-content {
position: fixed;
left: 0;
right: 0;
overflow-y: scroll; /* render disabled scroll bar to keep the same width */
/* Suggestion to put: overflow-y: hidden;
Disabled scrolling still makes a mess with its width. Hiding it does the trick. */
}
.overlay {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.3);
overflow: auto;
}
.show-overlay .overlay {
display: block;
}
.overlay-content {
margin: 50px;
background-image: repeating-linear-gradient( grey, grey 20px, black 20px, black 40px);
height: 120vh;
}
.toggle {
position: fixed;
top: 5px;
left: 15px;
padding: 10px;
background: red;
}
/* reset CSS */
body {
margin: 0;
}
<main class="main-content"></main>
<div class="overlay">
<div class="overlay-content"></div>
</div>
<button class="toggle">Overlay</button>
Don't use overflow: hidden; on body. It automatically scrolls everything to the top. There's no need for JavaScript either. Make use of overflow: auto;. This solution even works with mobile Safari:
HTML Structure
<div class="overlay">
<div class="overlay-content"></div>
</div>
<div class="background-content">
lengthy content here
</div>
Styling
.overlay{
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(0, 0, 0, 0.8);
.overlay-content {
height: 100%;
overflow: scroll;
}
}
.background-content{
height: 100%;
overflow: auto;
}
See the demo here and source code here.
Update:
For people who want keyboard space bar, page up/down to work: you need to focus on the overlay, e.g., clicking on it, or manually JS focusing on it before this part of the div will respond to keyboard. Same with when the overlay is "switched off", since it's just moving the overlay to the side. Otherwise to browser, these are just two normal divs and it wouldn't know why it should focus on any one of them.
It is worth noting that sometimes adding "overflow:hidden" to the body tag doesn't do the job. In those cases, you'll have to add the property to the html tag as well.
html, body {
overflow: hidden;
}
The behaviour you want to prevent is called scroll chaining. To disable it, set
overscroll-behavior: contain;
on your overlay in CSS.
You can easily do this with some "new" css and JQuery.
Initially: body {... overflow:auto;}
With JQuery you can dynamically switch between 'overlay' and 'body'. When on 'body', use
body {
position: static;
overflow: auto;
}
When on 'overlay' use
body {
position: sticky;
overflow: hidden;
}
JQuery for the switch('body'->'overlay'):
$("body").css({"position": "sticky", "overflow": "hidden"});
JQuery for the switch('overlay'->'body'):
$("body").css({"position": "static", "overflow": "auto"});
if anyone is looking for a solution for React function components, you can put this inside the modal component:
useEffect(() => {
document.body.style.overflowY = 'hidden';
return () =>{
document.body.style.overflowY = 'auto';
}
}, [])
Generally speaking, if you want a parent (the body in this case) to prevent it from scrolling when a child (the overlay in this case) scrolls, then make the child a sibling of the parent to prevent the scroll event from bubbling up to the parent. In case of the parent being the body, this requires an additional wrapping element:
<div id="content">
</div>
<div id="overlay">
</div>
See Scroll particular DIV contents with browser's main scrollbar to see its working.
The chosen answer is correct, but has some limitations:
Super hard "flings" with your finger will still scroll <body> in the background
Opening the virtual keyboard by tapping an <input> in the modal will direct all future scrolls to <body>
I don't have a fix for the first issue, but wanted to shed some light on the second. Confusingly, Bootstrap used to have the keyboard issue documented, but they claimed it was fixed, citing http://output.jsbin.com/cacido/quiet as an example of the fix.
Indeed, that example works fine on iOS with my tests. However, upgrading it to the latest Bootstrap (v4) breaks it.
In an attempt to figure out what the difference between them was, I reduced a test case to no longer depend on Bootstrap, http://codepen.io/WestonThayer/pen/bgZxBG.
The deciding factors are bizarre. Avoiding the keyboard issue seems to require that background-color is not set on the root <div> containing the modal and the modal's content must be nested in another <div>, which can have background-color set.
To test it, uncomment the below line in the Codepen example:
.modal {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 2;
display: none;
overflow: hidden;
-webkit-overflow-scrolling: touch;
/* UNCOMMENT TO BREAK */
/* background-color: white; */
}
For touch devices, try adding a 1px wide, 101vh min-height transparent div in the wrapper of the overlay. Then add -webkit-overflow-scrolling:touch; overflow-y: auto; to the wrapper. This tricks mobile safari into thinking the overlay is scrollable, thus intercepting the touch event from the body.
Here's a sample page. Open on mobile safari: http://www.originalfunction.com/overlay.html
https://gist.github.com/YarGnawh/90e0647f21b5fa78d2f678909673507f
I found this question trying to solve issue I had with my page on Ipad and Iphone - body was scrolling when I was displaying fixed div as popup with image.
Some answers are good, however none of them solved my issue. I found following blog post by Christoffer Pettersson. Solution presented there helped issue I had with iOS devices and it helped my scrolling background problem.
Six things I learnt about iOS Safari's rubber band scrolling
As it was suggested I include major points of the blog post in case link gets outdated.
"In order to disable that the user can scroll the background page while the "menu is open", it is possible to control what elements should be allowed to be scrolled or not, by applying some JavaScript and a CSS class.
Based on this Stackoverflow answer you can control that elements with the disable-scrolling should not
perform their default scroll action when the touchmove event is triggered."
document.ontouchmove = function ( event ) {
var isTouchMoveAllowed = true, target = event.target;
while ( target !== null ) {
if ( target.classList && target.classList.contains( 'disable-scrolling' ) ) {
isTouchMoveAllowed = false;
break;
}
target = target.parentNode;
}
if ( !isTouchMoveAllowed ) {
event.preventDefault();
}
};
And then put the disable-scrolling class on the page div:
<div class="page disable-scrolling">
Simple inline styling for the body tag:
<body style="position: sticky; overflow: hidden;">
If the intent is to disable on mobile/ touch devices then the most straightforward way to do it is using touch-action: none;.
Example:
const app = document.getElementById('app');
const overlay = document.getElementById('overlay');
let body = '';
for (let index = 0; index < 500; index++) {
body += index + '<br />';
}
app.innerHTML = body;
app.scrollTop = 200;
overlay.innerHTML = body;
* {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
#app {
background: #f00;
position: absolute;
height: 100%;
width: 100%;
overflow-y: scroll;
line-height: 20px;
}
#overlay {
background: rgba(0,0,0,.5);
position: fixed;
top: 0;
left: 0;
right: 0;
height: 100%;
padding: 0 0 0 100px;
overflow: scroll;
}
<div id='app'></div>
<div id='overlay'></div>
(The example does not work in the context of Stack Overflow. You will need to recreate it in a stand-alone page.)
If you want to disable scrolling of the #app container, just add touch-action: none;.
I'd like to add to previous answers because I tried to do that, and some layout broke as soon as I switched the body to position:fixed. In order to avoid that, I had to also set body's height to 100% :
function onMouseOverOverlay(over){
document.getElementsByTagName("body")[0].style.overflowY = (over?"hidden":"scroll");
document.getElementsByTagName("html")[0].style.position = (over?"fixed":"static");
document.getElementsByTagName("html")[0].style.height = (over?"100%":"auto");
}
Use the following HTML:
<body>
<div class="page">Page content here</div>
<div class="overlay"></div>
</body>
Then JavaScript to intercept and stop scrolling:
$(".page").on("touchmove", function(event) {
event.preventDefault()
});
Then to get things back to normal:
$(".page").off("touchmove");
In my case, none of these solutions worked out on iPhone (iOS 11.0).
The only effective fix that is working on all my devices is this one - ios-10-safari-prevent-scrolling-behind-a-fixed-overlay-and-maintain-scroll-position
try this
var mywindow = $('body'), navbarCollap = $('.navbar-collapse');
navbarCollap.on('show.bs.collapse', function(x) {
mywindow.css({visibility: 'hidden'});
$('body').attr("scroll","no").attr("style", "overflow: hidden");
});
navbarCollap.on('hide.bs.collapse', function(x) {
mywindow.css({visibility: 'visible'});
$('body').attr("scroll","yes").attr("style", "");
});
One solution for a React functional component is to use the useEffect hook.
Here's the code example bellow (pay attention to the useEffect definition):
import {useEffect, useRef} from "react";
export default function PopoverMenu({className, handleClose, children}) {
const selfRef = useRef(undefined);
useEffect(() => {
const isPopoverOpenned = selfRef.current?.style.display !== "none";
const focusedElement = document?.activeElement;
const scrollPosition = {x: window.scrollX, y: window.scrollY};
if (isPopoverOpenned) {
preventDocBodyScrolling();
} else {
restoreDocBodyScrolling();
}
function preventDocBodyScrolling() {
const width = document.body.clientWidth;
const hasVerticalScrollBar = (window.innerWidth > document.documentElement.clientWidth);
document.body.style.overflowX = "hidden";
document.body.style.overflowY = hasVerticalScrollBar ? "scroll" : "";
document.body.style.width = `${width}px`;
document.body.style.position = "fixed";
}
function restoreDocBodyScrolling() {
document.body.style.overflowX = "";
document.body.style.overflowY = "";
document.body.style.width = "";
document.body.style.position = "";
focusedElement?.focus();
window.scrollTo(scrollPosition.x, scrollPosition.y);
}
return () => {
restoreDocBodyScrolling(); // cleanup on unmount
};
}, []);
return (
<>
<div
className="backdrop"
onClick={() => handleClose && handleClose()}
/>
<div
className={`pop-over-menu${className ? (` ${className}`) : ""}`}
ref={selfRef}
>
<button
className="pop-over-menu--close-button" type="button"
onClick={() => handleClose && handleClose()}
>
X
</button>
{children}
</div>
</>
);
}
Originally posted on this other related Stackoverflow question: https://stackoverflow.com/a/69016517/14131330
CSS
.noScroll {
overflow: hidden;
}
Javascript
<script>
function toggleNav() {
document.body.classList.toggle("noScroll");
}
</script>
Button
<button onclick="toggleNav()">
Toggle Nav
</button>
If you want to stop body/html scroll add as the following
CSS
html, body {
height: 100%;
}
.overlay{
position: fixed;
top: 0px;
left: 0px;
right: 0px;
bottom: 0px;
background-color: rgba(0, 0, 0, 0.8);
.overlay-content {
height: 100%;
overflow: scroll;
}
}
.background-content{
height: 100%;
overflow: auto;
}
HTML
<div class="overlay">
<div class="overlay-content"></div>
</div>
<div class="background-content">
lengthy content here
</div>
Basically, you could do it without JS.
The main idea is to add html/body with height: 100% and overflow: auto.
and inside your overlay, you could either enable/disable scroll based on your requirement.
Hope this helps!
Use below code for disabling and enabling scroll bar.
Scroll = (
function(){
var x,y;
function hndlr(){
window.scrollTo(x,y);
//return;
}
return {
disable : function(x1,y1){
x = x1;
y = y1;
if(window.addEventListener){
window.addEventListener("scroll",hndlr);
}
else{
window.attachEvent("onscroll", hndlr);
}
},
enable: function(){
if(window.removeEventListener){
window.removeEventListener("scroll",hndlr);
}
else{
window.detachEvent("onscroll", hndlr);
}
}
}
})();
//for disabled scroll bar.
Scroll.disable(0,document.body.scrollTop);
//for enabled scroll bar.
Scroll.enable();

Resources