1

I'm using next JS for my application. I have a sign in modal for my application, and I would like to fix the under lying page to not scroll while it is open. I could solve the issue with document.body.style.overflow = 'hidden' when the modal is being open, but then the website jumps to occupy the space the scroll was present.

I would like to preserve the scroll bar yet disable the scroll. I'm calling the modal in a component, so CSS properties like overflow:hidden only works on the respective component. Is there a way I could achieve whatever I'm trying to perform?

2 Answers 2

5

Phew. It took many hours of digging (bloggers and A.I. [NOT StackOverflow answers per se, especially not the accepted one here] seem weirdly content with the annoyingly-widespread solution of simply making the scrollbar temporarily disappear entirely, which I think is tacky/distracting), but I finally compiled the 3 required components to disable the scrollbar while keeping it visible [only tested in Chromium(Edge) and the mobile emulator in Dev Tools]:

Block ScrollWheel Scroll

...
function App(){
  const [isModalOpen, setIsModalOpen] = useState(false)

  useEffect(() => {
    const handleWindowWheel = (event: WheelEvent) => {
      if (isModalOpen){
        event.preventDefault();
      }
    };
    
    window.addEventListener('wheel', handleWindowWheel, { passive: false });
    
    return () => {
      window.removeEventListener('wheel', handleWindowWheel);
    };
  }, [isModalOpen]);

  return (
    ...
    <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
    ...
  )
}

Block Click-and-Drag Scroll

...
function disableScroll() {
  // Store the current X & Y scroll positions.
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;

  // If any scroll event is attempted on the window, change the window's 
  // onscroll function to always scroll back to the saved X & Y positions.
  window.onscroll = function() {
    window.scrollTo(scrollLeft, scrollTop);
  };
}
function enableScroll() {
  // Reset the window's onscroll function.
  window.onscroll = function() {};
}

function App(){
  const [isModalOpen, setIsModalOpen] = useState(false)

  useEffect(() => {
    if (isModalOpen) {
      disableScroll();
    } else {
      enableScroll();
    }
  }, [isModalOpen]);

  return (
    ...
    <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
    ...
  )
}

Credit

The above snippet semi-blocks ScrollWheel scroll too, but it's ugly: It allows you to scroll the scrollbar with the wheel a whole ScrollWheel-click's distance, then visibly snaps the scrollbar back to where it originally was. (Which is why it's recommended to additionally implement the Block ScrollWheel Scroll as well.)

Block Finger Scroll (mobile)

...
function App(){
  const [isModalOpen, setIsModalOpen] = useState(false)
  ...
  return (
    {/* Sets the 'touch-action' CSS property to 'none' on 
    the outermost div when the modal is open. */}
    <div style={{ touchAction: isModalOpen ? 'none' : 'auto' }}>
      ...
      <button onClick={() => setIsModalOpen(true)}>Open Modal</button>
      ...
    </div>
  )
}

Combined (Functional Demo):

const {useState} = React;
const {useEffect} = React;

function disableScroll() {
  const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
  const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft;

  window.onscroll = function() {
    window.scrollTo(scrollLeft, scrollTop);
  };
}

function enableScroll() {
  window.onscroll = function() {};
}

const ExampleComponent = () => {
  const [isModalOpen, setIsModalOpen] = useState(false);
  
  useEffect(() => {
    if (isModalOpen) {
      disableScroll();
    } else {
      enableScroll();
    }

    const handleWindowWheel = (event: WheelEvent) => {
      if (isModalOpen){
        event.preventDefault();
      }
    };
    
    window.addEventListener('wheel', handleWindowWheel, { passive: false });
    
    return () => {
      window.removeEventListener('wheel', handleWindowWheel);
    };
  }, [isModalOpen]);  

  return (
    <div className={isModalOpen ? 'disable-touch-scroll' : ''}>
        {isModalOpen &&
          <div id="modal">
            <span>You shouldn't be able to scroll now.</span>
            <button
              onClick={() => setIsModalOpen(false)}
            >
              Close Modal
            </button>
          </div>
        }
      <div>
        {"hello ".repeat(1000)}
      </div>
      <button 
        id="modal-open-button" 
        onClick={() => setIsModalOpen(true)}
      >
        Open Modal
      </button>
    </div>
  );
};

ReactDOM.createRoot(
    document.getElementById("root")
).render(
    <ExampleComponent/>
);
#modal {
  width: 50%;
  height: 50%;
  position: fixed;
  z-index: 999;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  text-align: center;
  background: white;
  border: 1px solid black;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}

#modal-open-button {
  position: fixed;
  top: 50%;
  color: white;
  background-color: red;
}

.disable-touch-scroll {
  touch-action: none;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.development.js"></script>

<div id="root"></div>

Sign up to request clarification or add additional context in comments.

5 Comments

everybody seems weirdly content with the annoyingly-widespread solution of simply making the scrollbar temporarily disappear entirely, I assume you never looked at the accepted answer to this question?
@Keith You assume incorrectly. I took a very good look at your answer. It is not React code. I'm just trying to be helpful by leaving an answer for future Googlers who were looking for React-specific code, as the title specifies. Seeing that this has 3k views, I thought this was a good place to put it. "Everybody" was an exaggeration--I meant the majority of articles/answers seem to suggest to just hide the scrollbar.
Yeah my comment is in reference to everybody, and with my answer been the only other one here that does not do what you said, seemed a strange thing to say. But having a React specific answer makes sense.
@Keith I see. I was admittedly careless with my choice of words--I was exaggerating for dramatic effect/to sprinkle in some personality, which I need to stop doing on this website--always gets interpreted not as I hoped. I liked your repeated "hello" background idea, which I took from you (Looking again, i basically built my answer off of yours)
I enjoyed everybody seems weirdly content with the annoyingly-widespread solution of simply making the scrollbar temporarily disappear entirely, as it absolutely mirrors my thoughts - and everybody is always an exaggeration (yes, I did that on purpose). Devs are frustrated people and I don't see a reason they should not be allowed to express that. If you were to ask me, which you didn't, I'd say "Keep it going @velkoon!"
1

You could just add an event to the onscroll, and then keep a track of the current scrollTop, scrollLeft (if you need to handle horizontal too), and then when you don't want scroll just reset the scroll to these stored values.

eg. If you run the snippet below when the large checkbox is checked, scrolling is disabled.

const div = document.querySelector('div');
const cb = document.querySelector('input');

let lastY = 0;

div.innerText =  new Array(3000).fill().map(m => 'hello ').join(' ');

div.addEventListener('scroll', (e) => {
  if (cb.checked) {
    e.target.scrollTop = lastY;
  } else lastY = e.target.scrollTop;
});


cb.addEventListener('click', () => {
  console.log('click');
});
div {
  height: 150px;
  overflow: auto;
}
input {
  position: fixed;
  top: 50px;
  left: 50px;
  transform: scale(4);
}
<div>
</div>
<input type="checkbox"/>

1 Comment

It still scrolls though, it's just jittery. And, you can still scroll - perfectly fine - if you just keyboard: space bar, arrow keys, tab, etc. Why was this accepted?

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.