104

I'm trying to detect when my document height changes. Once it does, I need to run a few functions to help organize my page layout.

I'm not looking for window.onresize. I need the entire document, which is larger than the window.

How do I observe this change?

Steve Robbins
  • 13,672
  • 12
  • 76
  • 124

8 Answers8

154

Update (Oct 2020):

resizeObserver is a wonderful API (support table)

// create an Observer instance
const resizeObserver = new ResizeObserver(entries => 
  console.log('Body height changed:', entries[0].target.clientHeight)
)

// start observing a DOM node
resizeObserver.observe(document.body)

// click anywhere to rnadomize height
window.addEventListener('click', () =>
  document.body.style.height = Math.floor((Math.random() * 5000) + 1) + 'px'
)
click anywhere to change the height

Old answer:

Although a "hack", this simple function continuously "listens" (through setTimeout) to changes in an element's height and fire a callback when a change was detected.

It's important to take into account an element's height might change regardless of any action taken by a user (resize, click, etc.) and so, since it is impossible to know what can cause a height change, all that can be done to absolutely guarantee 100% detection is to place an interval height checker :

function onElementHeightChange(elm, callback) {
  var lastHeight = elm.clientHeight, newHeight;

  (function run() {
    newHeight = elm.clientHeight;
    if (lastHeight != newHeight)
      callback(newHeight)
    lastHeight = newHeight

    if (elm.onElementHeightChangeTimer)
      clearTimeout(elm.onElementHeightChangeTimer)

    elm.onElementHeightChangeTimer = setTimeout(run, 200)
  })()
}

// to clear the timer use:
// clearTimeout(document.body.onElementHeightChangeTimer);

// DEMO:
document.write("click anywhere to change the height")

onElementHeightChange(document.body, function(h) {
  console.log('Body height changed:', h)
})

window.addEventListener('click', function() {
  document.body.style.height = Math.floor((Math.random() * 5000) + 1) + 'px'
})
LIVE DEMO
vsync
  • 118,978
  • 58
  • 307
  • 400
  • 6
    I know I'm digging up an old post here, but the checkDocumentheight function would just run constantly, right? It seems like that would slow down the browser checking the document's height over and over again. – Cloudkiller Jul 15 '13 at 19:42
  • 6
    @Cloudkiller - yes it runs constantly, and yes it hurts performance, but I know no better way. – vsync Mar 10 '14 at 09:16
  • 4
    @ShashwatTripathi - I see no reason why it wouldn't work. Please debug it and explain which piece of code doesn't work specifically. Also iPad is a device, not a browser. Which browser was at fault here? – vsync Jul 30 '15 at 12:06
  • How to use it inside iframe? – VISHMAY Jan 13 '17 at 05:09
  • @VISHMAY - can you explain **exactly** what you want to happen? – vsync Jan 13 '17 at 09:40
  • 1
    @vsync, Actually,In a page i've iframe and I've managed to set iframe height = iframe's inner content's height on onload and resize event of outer page so that scroll is avoided, Now a popup is displayed inside iframe's content on the control's hover (which is at bottom). so my iframe is having scroll now which i don't want on hovering that item – VISHMAY Jan 13 '17 at 11:32
  • @VISHMAY - you should open a new question and put a demo code. also you must mention if both the iframe and the parent window are on the same domain or not. – vsync Jan 13 '17 at 16:21
  • The live demo outputs the document's width, not height on both Chrome and Safari. – Chewie The Chorkie Mar 03 '17 at 19:40
  • Please never ever use `setTimeout` to poll for changes. On slower devices it will render your website unusable. Many commercial websites now have this problem. If there really is no event listener you can use, please give up on whatever minor enhancement you were trying to implement, and suggest your boss takes a computer science course. Please also if you completely change your answer, post a new separate answer. – Jake Sep 19 '21 at 01:39
  • @Jake - The `setTimeout` mentioned in my original answer runs at `200ms` intervals, which is eternity in terms of computing. It will never ever harm performance not even the slightest, unless the solution is applied for many hundreds of DOM elements, which is highly unlikely. There's only a single DOM *read* operation here. – vsync Sep 19 '21 at 06:39
  • For me it was necessary to add '{ }', see: `const resizeObserver = new ResizeObserver(entries => { //... });` – Cesar Devesa Nov 09 '22 at 13:11
30

You can use an absolute positioned iframe with zero width inside the element you want to monitor for height changes, and listen to resize events on its contentWindow. For example:

HTML

<body>
  Your content...
  <iframe class="height-change-listener" tabindex="-1"></iframe>
</body>

CSS

body {
  position: relative;
}
.height-change-listener {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  height: 100%;
  width: 0;
  border: 0;
  background-color: transparent;
}

JavaScript (using jQuery but could be adapted to pure JS)

$('.height-change-listener').each(function() {
  $(this.contentWindow).resize(function() {
    // Do something more useful
    console.log('doc height is ' + $(document).height());
  });
});

If for whatever reason you have height:100% set on body you'll need find (or add) another container element to implement this on. If you want to add the iframe dynamically you'll probably need to use the <iframe>.load event to attach the contentWindow.resize listener. If you want this to work in IE7 as well as browsers, you'll need to add the *zoom:1 hack to the container element and also listen to the 'proprietary' resize event on the <iframe> element itself (which will duplicate contentWindow.resize in IE8-10).

Here's a fiddle...

Jake
  • 948
  • 8
  • 19
  • I think this is quite a smart solution, however it would be better to use `100vh` instead of `100%` for the reasons you described. You would however lose compatibility with some older browser that do not support viewport-height. – Tim Jul 09 '17 at 09:46
  • 4
    @Tim - `100vh` would be the height of the viewport (aka window) whereas we want the height of the document (i.e. the content, which is not the same thing) to detect when that changes, which `100%` provides. (It is easy to detect when the viewport height changes with the `window.resize` event.) – Jake Jul 26 '17 at 22:34
  • you're right, don't know why I thought that would be better. – Tim Jul 27 '17 at 07:10
  • This is really clever - thank you @Jake! And if you were to define a named function, the JS can even be a one-liner (if that's your style): `$($('.height-change-listener')[0].contentWindow).on('resize', doSomethingMoreUseful);` – Bungle Aug 11 '17 at 22:10
  • 4
    If anyone wants this as an npm package: https://github.com/bertyhell/element-height-observer – Berty Mar 08 '18 at 23:51
  • When dynamically adding iframe, it seems it was working without attaching after `onLoad` is this expected? Here is a codesandbox and more details - https://stackoverflow.com/q/74295618/1828637 – Noitidart Nov 02 '22 at 21:04
  • 1
    @Noitidart It depends on timing, and specific browser implementation. It may work sometimes if you attach before the `load` event, but sometimes what you're attaching to may not have been fully initiallized. To cover all bases and also attach as soon as possible, you can attach initially and then check on `load` if your event handler was actually attached. – Jake Jan 09 '23 at 10:20
  • 1
    This is a very clever solution!! – Anthony M. Jul 15 '23 at 03:36
  • @AnthonyM. Thanks. I adapted the invisble `iframe` idea from a solution to deal with missing `resize` and/or `orientationchange` events from early mobile browsers. It can be adapted for any purpose where you want to know if some part of your displayed content has changed in size (vertically or horizontally). I would never use a continuous polling loop, as it will cripple performance on slower machines. – Jake Jul 18 '23 at 00:27
14

Update: 2020

There is now a way to accomplish this using the new ResizeObserver. This allows you to listen to a whole list of elements for when their element changes size. The basic usage is fairly simple:

const observer = new ResizeObserver(entries => {
  for (const entry of entries) {
    // each entry is an instance of ResizeObserverEntry
    console.log(entry.contentRect.height)
  }
})
observer.observe(document.querySelector('body'))

The one downside is that currently there is only support for Chrome/Firefox, but you can find some solid polyfills out there. Here's a codepen example I wrote up:

https://codepen.io/justin-schroeder/pen/poJjGJQ?editors=1111

jpschroeder
  • 6,636
  • 2
  • 34
  • 34
12

Just my two cents. If by any chance you're using angular then this would do the job:

$scope.$watch(function(){ 
 return document.height();
},function onHeightChange(newValue, oldValue){
 ...
});
Dan Ochiana
  • 3,340
  • 1
  • 30
  • 28
  • 4
    You mean $(document).height(); ? – Amyth Sep 07 '16 at 01:59
  • 3
    this doesn't work, it will only check the height on angular scope $digest, which doesn't cover many other ways the height can change, as css animations, images loading, and any js code that doesn't trigger angular digests (eg. non-angular libs) – Benja Feb 13 '17 at 22:39
  • 1
    The answer clearly states "If by any chance you're using angular". This being said - for angular based apps the above code did the job gracefully. – Dan Ochiana Feb 14 '17 at 08:58
  • How does Angular detect the change? Does it use an event listener of some sort or does it use wasteful and expensive polling that will cripple your site? We need to know. – Jake Sep 19 '21 at 01:43
4

As mentioned by vsync there is no event but you can use a timer or attach the handler somewhere else:

// get the height
var refreshDocHeight = function(){
    var h = $(document).height();
    $('#result').html("Document height: " + h);
};

// update the height every 200ms
window.setInterval(refreshDocHeight, 200);

// or attach the handler to all events which are able to change 
// the document height, for example
$('div').keyup(refreshDocHeight);

Find the jsfiddle here.

Marc
  • 6,749
  • 9
  • 47
  • 78
  • Please never ever use polling like this. It will cripple your website on slower devices. If there is no other way for the functionality you want to be implmented, please go back to your boss and say that it can't be done. The web is now full of shoddy solutions like this, some on major websites, and almost becoming unusable. – Jake Sep 19 '21 at 01:47
1

vsync's answer is completely fine. Just in case you don't like to use setTimeout and you can use requestAnimationFrame (see support) and of course you are still interested.

In the example below the body gets an extra event sizechange. And every time the height or width of the body changes it is triggered.

(function checkForBodySizeChange() {
    var last_body_size = {
        width: document.body.clientWidth,
        height: document.body.clientHeight
    };

    function checkBodySizeChange()
    {
        var width_changed = last_body_size.width !== document.body.clientWidth,
            height_changed = last_body_size.height !== document.body.clientHeight;


        if(width_changed || height_changed) {
            trigger(document.body, 'sizechange');
            last_body_size = {
                width: document.body.clientWidth,
                height: document.body.clientHeight
            };
        }

        window.requestAnimationFrame(checkBodySizeChange);
    }

    function trigger(element, event_name, event_detail)
    {
        var evt;

        if(document.dispatchEvent) {
            if(typeof CustomEvent === 'undefined') {
                var CustomEvent;

                CustomEvent = function(event, params) {
                    var evt;
                    params = params || {
                        bubbles: false,
                        cancelable: false,
                        detail: undefined
                    };
                    evt = document.createEvent("CustomEvent");
                    evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
                    return evt;
                };

                CustomEvent.prototype = window.Event.prototype;

                window.CustomEvent = CustomEvent;
            }

            evt = new CustomEvent(event_name, {"detail": event_detail});

            element.dispatchEvent(evt);
        }
        else {
            evt = document.createEventObject();
            evt.eventType = event_name;
            evt.eventName = event_name;
            element.fireEvent('on' + event_name, evt);
        }
    }

    window.requestAnimationFrame(checkBodySizeChange);
})();

A live demo

The code can be reduced much if you have an own triggerEvent function in your project. Therefore just remove the complete function trigger and replace the line trigger(document.body, 'sizechange'); with for example in jQuery $(document.body).trigger('sizechange');.

  • 2
    One disadvantage of using `requestAnimationFrame` as a replacement for `setTimout` (or perhaps `setInterval`) is that the size checking loop runs too often, every couple milliseconds. With `setTimeout` you can increase the loop interval to a reasonable amount. – Gustavo Straube Mar 06 '19 at 15:17
  • @vsync's original answer is not fine - it is awful. Please do not use polling unless you want to cripple your website. If there are no events you can listen for then please give up on whatever feature you were trying to implement, for the benefit of everyone. – Jake Sep 19 '21 at 01:51
-1

I'm using @vsync's solution, like this. I'm using it for automatic scroll on a page like twitter.

const scrollInterval = (timeInterval, retry, cb) => {
    let tmpHeight = 0;
    const myInterval = setInterval(() => {
        console.log('interval');
        if (retry++ > 3) {
            clearInterval(this);
        }
        const change = document.body.clientHeight - tmpHeight;
        tmpHeight = document.body.clientHeight;
        if (change > 0) {
            cb(change, (retry * timeInterval));
            scrollBy(0, 10000);
        }
        retry = 0;
    }, timeInterval);
    return myInterval;
};

const onBodyChange = (change, timeout) => {
    console.log(`document.body.clientHeight, changed: ${change}, after: ${timeout}`);
}

const createdInterval = scrollInterval(500, 3, onBodyChange);

// stop the scroller on some event
setTimeout(() => {
    clearInterval(createdInterval);
}, 10000);

You can also add a minimum change, and a lot of other things... But this is working for me

Johan Hoeksma
  • 3,534
  • 5
  • 28
  • 40
-2

The command watch() checks any change in a property.

See this link.

forkdbloke
  • 1,505
  • 2
  • 12
  • 29
Marlos Carmo
  • 972
  • 1
  • 8
  • 13
  • the WATCH command is not-standard and is only supported by GECKO.. http://goo.gl/11uqL BUT you can use a polyfill: https://gist.github.com/eligrey/384583 – vsync Feb 15 '13 at 17:41
  • 7
    See the [big red warning here](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/watch)? **"Generally you should avoid using watch() and unwatch() when possible. These two methods are implemented only in Gecko, and they're intended primarily for debugging use. In addition, using watchpoints has a serious negative impact on performance"** – Tomas Jan 18 '14 at 14:30