V is for Viewport

You are testing in quirks mode. Test in standards mode.

IE Has it Right

Measuring the client area of the viewport is very easy in MSHTML. The viewport is actually an element. Which one? The Big One With the Scroll Bar. We need the area inside the border, excluding space taken up by scroll bars. MS invented the clientWidth/Height properties for just this purpose.

Finding the Right Element

In MSHTML, the HTML element (document.documentElement) is used for the viewport in standards mode and the body in quirks (HTML is not rendered). In IE6-8, the document.compatMode property indicates the rendering mode. This property does not exist in IE < 6, but quirks mode can be detected easily enough as we know that the HTML element is not rendered in quirks mode. Inspecting the HTML element's clientWidth/Height properties reveals they are 0 in quirks mode. A document is more likely to legitimately have an 0 pixel height than width, so we will check the clientWidth property.

// This code works for all IE versions and modes

var getRoot, getViewportDimensions;

if (typeof doc.compatMode == 'string') {
	getRoot = function(win) {
		var doc = win.document, html = doc.documentElement, compatMode = doc.compatMode;

		return (html && compatMode.toLowerCase().indexOf('css') != -1) ? html : doc.body;
	};
} else {
	getRoot = function(win) {
		var doc = win.document, html = doc.documentElement;

		return (!html || html.clientWidth === 0) ? doc.body : html;
	};
}

getViewportDimensions = function(win) {
	if (!win) {
		win = window;
	}
	var root = getRoot(win);
	return [root.clientWidth, root.clientHeight];
};

The Trouble with the Others

The history of this critical implementation is a comedy of errors.

Tried to Copy IE

Over the years, the other major browsers have copied IE, but there have been troubles along the way. For one, the browser developers observed that the properties needed to measure the client area of the viewport moved between the HTML and body elements when the rendering mode was switched, but apparently did not correlate this with the MSHTML rendering behavior. In quirks mode, the HTML element is typically rendered, but its rightful (per MS) clientHeight/Width properties are reported by the body element. This makes no logical sense, but doesn't hinder the initial rendition as the document.compatMode property still indicates the proper element in most cases.

Opera Fouled Up Worse

Prior to 9.5, Opera used the body exclusively to report what should be the HTML clientHeight/Width, which makes even less sense and requires feature testing to work around, with the body's border(s) figuring in when present. As of 9.5, they have caught up to the other major non-IE browsers.

The Window innerHeight/Width Properties

More often than not, especially in modern browsers released in the last five years, these properties include space occupied by scroll bars. Therefore we refer to these properties only as a last resort.


// Last resort, return innerWidth/Height
// NOTE: May include space occupied by scroll bars

getViewportDimensions = function(win) {
	if (!win) {
		win = window;
	}

	return [win.innerWidth, win.innerHeight];
};

Safari 2 Oddity

Some older KHTML-based browsers (e.g. Safari 2) feature document.clientHeight/Width properties. These properties are arguably a better alternative to measuring elements, but are apparently no longer in production.


// This code will work in Safari 2 and other older KHTML-based browsers

getViewportDimensions = function(win) {
	if (!win) {
		win = window;
	}
	var doc = win.document;
	return [doc.clientWidth, doc.clientHeight];
};

Putting it All Together

We need a feature test to find the correct element.


var scrollChecks, html = doc.documentElement;

if (html && typeof doc.createElement != 'undefined') {

	// Test to resolve ambiguity between HTML and body

	scrollChecks = (function() {
		var oldBorder, body = doc.body, result = { compatMode: doc.compatMode };
		var clientHeight = html.clientHeight, bodyClientHeight = body.clientHeight;
		var elDiv = doc.createElement('div');
		elDiv.style.height = '100px';
		body.appendChild(elDiv);
		result.body = !clientHeight || clientHeight != html.clientHeight;
		result.html = bodyClientHeight != body.clientHeight;
		body.removeChild(elDiv);
		if (result.body || result.html && (result.body != result.html)) {

			// found single scroller--check if borders should be included
			// skipped if body is the real root (as opposed to just reporting the HTML element's client dimensions)

			if (typeof body.clientTop == 'number' && body.clientTop && html.clientWidth) {
				oldBorder = body.style.borderWidth;
				body.style.borderWidth = '0px';
				result.includeBordersInBody = body.clientHeight != bodyClientHeight;
				body.style.borderWidth = oldBorder;
			}
			return result;
		}
	})();
}

With this and the getRoot function, we can create a simple wrapper, choosing between 4 (5 in the longer version) different algorithms.


var doc = window.document, html = doc.documentElement;

if (typeof doc.clientWidth == 'number') {

	// If the document itself implements clientWidth/Height (e.g. Safari 2)
	// This one is a rarity.  Coincidentally KHTML-based browsers reported to implement
	// these properties have trouble with clientHeight/Width as well as innerHeight/Width.

	getViewportDimensions = function(win) {
		if (!win) {
			win = window;
		}
		var doc = win.document;
		return [doc.clientWidth, doc.clientHeight];
	};
} else if (html && typeof html.clientWidth == 'number') {

	// If the document element implements clientWidth/Height
		
	getViewportDimensions = function(win) {
		if (!win) {
			win = window;
		}

		var root = getRoot(win), doc = win.document;
		var clientHeight = root.clientHeight, clientWidth = root.clientWidth;				

		// Replace previous guess at root container

		if (scrollChecks) {
			root = scrollChecks.body ? doc.body : doc.documentElement;
		}

		clientHeight = root.clientHeight;
		clientWidth = root.clientWidth;

		if (scrollChecks && scrollChecks.body && scrollChecks.includeBordersInBody) {

			// Add body borders

			clientHeight += doc.body.clientTop * 2;
			clientWidth += doc.body.clientLeft * 2;
		}

		return [clientWidth, clientHeight];
	};
} else if (typeof window.innerWidth == 'number') {

	// Last resort, return innerWidth/Height
	// NOTE: May include space occupied by scroll bars

	getViewportDimensions = function(win) {
		if (!win) {
			win = window;
		}
			
		return [win.innerWidth, win.innerHeight];
	};
}

// Discard unneeded host object references

doc = html = null;

This can be simplified further for contexts where body borders are not used.

See the source for the complete rendition. It is based on a method from My Library.

CLJ FAQ article on the same subject

Other Primers

Home
dmark@cinsoft.net