55

I have a custom modal component. When it's open, there is no scrolling whatsoever in the background.

I tried this code below:

componentDidMount() {
    document.body.style.overflow = 'hidden';
}

componentWillUnmount() {
    document.body.style.overflow = 'unset';
}

Which seems to work at first, but when I use the modal component, in another page, there is no scroll even when the modal is closed.

Is there a better solution for this?

My modal component:

export class Modal extends React.Component {

constructor(props) {
    super(props);
}

componentDidMount() {
    document.body.style.overflow = 'hidden';
}

componentWillUnmount() {
    document.body.style.overflow = 'unset';
}

render() {
    return (
        <React.Fragment>
            {this.props.showAt ?
                this.props.show ?
                    <div style={style} className={`${this.props.sectionName} ${modalTypeStyle ? modalTypeStyle : styles.modalWhite} ${modalTypeSize ? modalTypeSize : styles.modalSmall} ${!this.props.showAt ? styles.modalWhiteFixed : ""}`}>
                        {this.props.arrowShape ? <div className={arrowTypeStyle ? arrowTypeStyle : styles.triangleToprightWhite} /> : null}
                        {this.props.children}
                    </div>
                    : null
                :
                this.props.show ?
                    <div className={`${this.props.className} ${styles.modal}`}>
                        <div style={style} className={`${this.props.sectionName} ${modalTypeStyle ? modalTypeStyle : styles.modalWhite} ${modalTypeSize ? modalTypeSize : styles.modalSmall} ${!this.props.showAt ? styles.modalWhiteFixed : ""}`}>
                            {this.props.arrowShape ? <div className={arrowTypeStyle ? arrowTypeStyle : styles.triangleToprightWhite} /> : null}
                            {this.props.children}
                        </div>
                    </div> :
                    null}
        </React.Fragment>
    )
  }
}
RCohen
  • 1,702
  • 10
  • 24
  • 44
  • If it works on one page, what is preventing it working on the other page you mention? I did a quick Google search and found that your approach is indeed recommended by others: https://stackoverflow.com/questions/39962757/prevent-scrolling-using-css-on-react-rendered-components. – reZach Mar 04 '19 at 18:41
  • Yeah, it's a bit weird. On the other page the `overflow: hidden` was already there when the page was loaded. Maybe something related with `componentDidMount()`? – RCohen Mar 04 '19 at 18:44
  • 2
    The problem is you're using `document.body.style.overflow = 'hidden'` in `componentDidMount`. Even if you're not showing the modal, the component still gets mounted which calls the lifecycle method that hides the scroll on body. – Hemant Parashar Mar 04 '19 at 18:45
  • 1
    Yeah, that's what I thought :/. Any ideas to fix this, @HemantParashar? – RCohen Mar 04 '19 at 18:47

10 Answers10

66

Use state to track if the Modal is open and only hide scroll if it's true. Since you're using document.body.style.overflow = 'hidden' in componentDidMount, the component still gets mounted which calls the lifecycle method that hides the scroll on body.

export class Modal extends React.Component {

constructor(props) {
    super(props);
    this.state = {
      open:false
    }
}

componentDidMount() {    
  if(this.state.open){
    document.body.style.overflow = 'hidden';
  }    
}

componentWillUnmount() {
    document.body.style.overflow = 'unset';
}

render() {
    return (
        <React.Fragment>
            {this.props.showAt ?
                this.props.show ?
                    <div style={style} className={`${this.props.sectionName} ${modalTypeStyle ? modalTypeStyle : styles.modalWhite} ${modalTypeSize ? modalTypeSize : styles.modalSmall} ${!this.props.showAt ? styles.modalWhiteFixed : ""}`}>
                        {this.props.arrowShape ? <div className={arrowTypeStyle ? arrowTypeStyle : styles.triangleToprightWhite} /> : null}
                        {this.props.children}
                    </div>
                    : null
                :
                this.props.show ?
                    <div className={`${this.props.className} ${styles.modal}`}>
                        <div style={style} className={`${this.props.sectionName} ${modalTypeStyle ? modalTypeStyle : styles.modalWhite} ${modalTypeSize ? modalTypeSize : styles.modalSmall} ${!this.props.showAt ? styles.modalWhiteFixed : ""}`}>
                            {this.props.arrowShape ? <div className={arrowTypeStyle ? arrowTypeStyle : styles.triangleToprightWhite} /> : null}
                            {this.props.children}
                        </div>
                    </div> :
                    null}
        </React.Fragment>
    )
  }
}
Hemant Parashar
  • 3,684
  • 2
  • 16
  • 23
  • The component receives by props from the parent, if the modal is open or not. Even like that, it should work, right? `this.props.open` instead of `this.state.open`? – RCohen Mar 04 '19 at 18:58
  • Yeah it wil work with props too. Do accept the answer and upvote if you find it helpful.Happy coding ! – Hemant Parashar Mar 04 '19 at 18:59
  • You could create a wrapper around the Modal component to simplify & encapsulate things: `class MyModal extends React.PureComponent { componentDidMount() { this.oldValue = document.body.style.overflow; document.body.style.overflow = 'hidden'; } componentWillUnmount() { document.body.style.overflow = this.oldValue; } render() { return {this.props.children}; } }` – Kira Aug 02 '20 at 17:50
52

in functional component for when mount or unmount

  useEffect(() => {
     document.body.style.overflow = 'hidden';
     return ()=> document.body.style.overflow = 'unset';
  }, []);

or when show is change or etc

  useEffect(() => {
     show && document.body.style.overflow = 'hidden';
     !show && document.body.style.overflow = 'unset';
  }, [show ]);
Mehran Motiee
  • 3,547
  • 1
  • 13
  • 16
  • 3
    Is it a good practice to directly add styles to the dom element? Will it have any performance issues or unwanted rerenders? – Gurmeet Singh Dec 14 '19 at 18:13
  • 4
    This is a good solution using hooks, but your second example has a syntax error. `Uncaught SyntaxError: Invalid left-hand side in assignment`. Instead of `show && obj.key = val`, use `show && (obj.key = val)` – kevbost Apr 22 '20 at 15:23
  • 1
    Also setting `"unset"` on unmount is actually adding a style to the body (if overflow is not set as an inline style on the body). If no inline style is present, just say `document.body.style.overflow = ""` – kevbost Apr 22 '20 at 15:29
  • I have the same problem, when i tried this, it throw an error : useEffect hook has a messing dependency show ! – iskandar47 Oct 27 '20 at 12:58
  • 1
    @kevbost try this useEffect(() => { if(show) { document.body.style.overflow = 'hidden' } else { document.body.style.overflow = 'unset' } }, [show]); This should work – AkshayBandivadekar Jan 16 '21 at 10:48
  • Looks so beautiful & clean against clumsy Class Component code, thanks for sharing – vikramvi May 14 '21 at 06:39
  • 2
    Slightly more concise syntax that also fixes the syntax error: `useEffect(() => (document.body.style.overflow = isOpen ? "hidden" : "unset"), [isOpen]);` – michelegera Oct 26 '21 at 09:25
  • This worked perfectly for me in a `useEffect` within my modal component. Then if your modal itself needs scroll capability just add `overflow-y: scroll;` to the css for that element – James Hubert Apr 22 '23 at 15:50
34

Here is a custom hook you can use for functional components.

import { useEffect } from 'react';

export const useDisableBodyScroll = (open) => {
  useEffect(() => {
    if (open) {
      document.body.style.overflow = 'hidden';
    } else {
      document.body.style.overflow = 'unset';
    }
  }, [open]);
};
export const Modal = (props) => {
    useDisableBodyScroll(props.show);

    return (
        <React.Fragment>
            {props.showAt ?
                props.show ?
                    <div style={style} className={`${props.sectionName} ${modalTypeStyle ? modalTypeStyle : styles.modalWhite} ${modalTypeSize ? modalTypeSize : styles.modalSmall} ${!props.showAt ? styles.modalWhiteFixed : ""}`}>
                        {props.arrowShape ? <div className={arrowTypeStyle ? arrowTypeStyle : styles.triangleToprightWhite} /> : null}
                        {props.children}
                    </div>
                    : null
                :
                props.show ?
                    <div className={`${props.className} ${styles.modal}`}>
                        <div style={style} className={`${props.sectionName} ${modalTypeStyle ? modalTypeStyle : styles.modalWhite} ${modalTypeSize ? modalTypeSize : styles.modalSmall} ${!props.showAt ? styles.modalWhiteFixed : ""}`}>
                            {props.arrowShape ? <div className={arrowTypeStyle ? arrowTypeStyle : styles.triangleToprightWhite} /> : null}
                            {props.children}
                        </div>
                    </div> :
                    null}
        </React.Fragment>
    )
  }
}

John Franke
  • 1,444
  • 19
  • 23
10

In functional components if the browser move due to hiding/showing scroll.

  useEffect(() => {
    if (show) {
      document.body.style.overflow = 'hidden';
      document.body.style.paddingRight = '15px';
    }
    return () => {
      document.body.style.overflow = 'unset';
      document.body.style.paddingRight = '0px';
    };
  }, [show]);
Nervall
  • 225
  • 3
  • 7
  • I have tried this, but still my browser is jumping. https://www.loom.com/share/a76cd9d660af4a7e967277f1d6479a1f?fbclid=IwAR03yLOf5tY5wpFRERUZVSD8VXP2c9ndf1FZ-n2eGjBqI0tpLHYlJxAu0oQ – Ashik Nov 18 '20 at 05:26
  • Maybe your modal is not unmouting when you close. Maybe you could try without the return statement. So if show is true the browser is hiding the scroll and if its not true it does the opposite. The return statement fires when a component is unmouting. – Nervall Nov 19 '20 at 08:38
7

I have 2 functions to handle this, when open modal and when close modal

openModal(){
        this.setState({
            isModalOpen: true
        })
        document.body.style.overflow = 'hidden';
    }

closeModal(){
        this.setState({
            isModalOpen: false
        })
        document.body.style.overflow = 'unset';
    }
Malnav
  • 131
  • 2
  • 2
5

For me it works when I check the show of the show. Example in functional component

  useEffect(() => {
    if (show) document.body.style.overflow = 'hidden';
    else document.body.style.overflow = 'visible';
  }, [show]);
Ivan Oliveira
  • 51
  • 1
  • 2
2

Short and elegant

The same solution as previous proposed, but the most elegant:

  useEffect(() => {
    document.body.style.overflow = isOpen ? "hidden" : "unset";
  }, [isOpen]);
Aga
  • 1,019
  • 1
  • 11
  • 16
0

I tried according to Mehran Motiee's but every time I close a cookie consent, it clears the body style. So I tried adding class list instead and it works.

  React.useEffect(() => {
    if (show) document.body.classList.add('overflow-hidden');
    else document.body.classList.remove('overflow-hidden');
  }, [show]);
Hon
  • 1
0

If using react-modal, one can use these callbacks provided:

onAfterOpen
onRequestClose

the component will look like this:

function App() {
      
  const [modalIsOpen, setIsOpen] = React.useState(false);
    
  function openModal() {
    setIsOpen(true);
  }

  function afterOpenModal() {
    document.body.style.overflow = 'hidden';
  }

  function closeModal() {
    document.body.style.overflow = 'unset';
    setIsOpen(false);
  }

  return (
    <div>
      <button onClick={openModal}>Open Modal</button>
      <Modal
        isOpen={modalIsOpen}
        onAfterOpen={afterOpenModal}
        onRequestClose={closeModal}
        contentLabel="Example Modal"
      >
        <button onClick={closeModal}>close</button>
        <div>I am a modal</div>
        <form>
          <input />
          <button>tab navigation</button>
          <button>stays</button>
          <button>inside</button>
          <button>the modal</button>
        </form>
      </Modal>
    </div>
  );
}
-3

You can also use add the following class to your div containing the Modal:

.modal {
    position: fixed;
}

Considerations for styling a modal

The point of this is that a user could be scrolled down the page, trigger a modal, and have the modal be just as centered-and-visible as it would be if they weren’t scrolled.

georgekrax
  • 1,065
  • 1
  • 11
  • 22