x

Media Queries Based on Element Width with MutationObserver

You probably have faced a situation before where you really wished CSS media queries were based on the elements width and not the entire viewport. In this article we will explore a JavaScript approach to add different attributes to an element depending on its width using resize event and MutationObserver. You can then use these attributes to style the element differently in your CSS.

 

What We Will Achieve

The end result of this JavaScript bit is to dynamically add an attribute data-width="some_size" to an element depending on its width. So for instance data-width="small"data-width="medium", etc. and then use CSS to target these attributes:

.someelement[data-size="small"] {
    ...
}
.someelement[data-size="medium"] {
    ...
}

 

The HTML Structure

We will not directly add this data-size attribute to the element since we need to manipulate it dynamically. Alternatively, we will use another attribute data-sizes which we will pass in it a JSON string to define the size ranges.

The JSON object will have keys of the sizes we would like to call and the values of these keys are objects with minWidth and maxWidth attributes like so:

{
 "small": {
 "maxWidth": 400
 },
 "medium": {
 "minWidth": 400
 },
 "large": {
 "minWidth": 700
 }
}

In the example above, for small we did not define minWidth which means any width less than maxWidth will be applied. Similarly, not defining maxWidth means for minWidth and above apply this attribute.

Finally the JSON can be passed to the data-sizes attribute as so:

<div id="container" data-sizes='{"small":{"maxWidth": 400}, "medium":{"minWidth": 400}, "large":{"minWidth": 700}}'>

</div>

 

The JavaScript Part

To watch for width changes using JavaScript, we are going to assume that the factors that can change an element’s width are:

  • Changing the window size.
  • Manipulating the width using element’s attributes like adding style=”width:100px” or adding a class that changes the width.
  • Changing the content of the element.
  • Any other factor I am not aware of (let me know)

The first factor is going to be handled using window.resize event. The other 2 will be handled using the MutationObserver. The MutationObserver can observe changes in attributes, childList and subtree which are enough for our case.

We will create a new attribute for the element called data-width. This attribute will hold the current width of the element. Whenever the window is resized or a mutation is observed, we will compare the width of the element to the current width and update data-width when needed. Inside the mutation observer, we will check if data-width is mutated then we will update the data-size attribute accordingly. Additionally, we will create a new event onWidthChange and fire it when we detect a change in width.

(function(global, undefined){

	function widthChanged(elem) {
		return elem.offsetWidth !== parseInt(elem.dataset.width);
	}

	function inRange(width, range) {
		let min = range.minWidth || 0;
		let max = range.maxWidth || Infinity;
		return width >= min && width <= max;
	}

	function isNumber(n) {
		return !isNaN(parseFloat(n)) && isFinite(n);
	}

	global.ElementMediaQuery = function(element, sizes) {

		if(arguments.length < 2) {
			throw('ElementMediaQuery expects 2 parameters ' + arguments.length + ' supplied.')
		}
		if(!(element instanceof Element)) {
			throw('The first argument for ElementMediaQuery must be a dom object');
		}
		if(typeof sizes !== "object") {
			throw('The second argument for ElementMediaQuery must be an object');
		}

		var $emqObj = this;
		$emqObj.element = element;
		$emqObj.sizes = sizes;

		element.dataset.width = element.offsetWidth;
		$emqObj.updateSize();

		var onWidthChange = new Event('onWidthChange');

		window.onresize = function(event) {
			if(widthChanged(element)) { 
				element.dataset.width = element.offsetWidth;
			}
		};
		var observer = new MutationObserver(function(mutations) {
		  	mutations.forEach(function(mutation) {
		  		if(widthChanged(element)) element.dataset.width = element.offsetWidth;
			    if(mutation.type === 'attributes' && mutation.attributeName === 'data-width') {		
			    	$emqObj.updateSize() 	
			    	element.dispatchEvent(onWidthChange);				    	
			    }
		 	});    
		});
		 			 
		observer.observe(element, { attributes: true, childList: true, characterData: true, subtree: true });

	}

	ElementMediaQuery.prototype.updateSize = function() {
		var range_flag = 0;
    	for (var size in this.sizes) {
			if(inRange(this.element.offsetWidth, this.sizes[size])) {
				this.element.dataset.size = size;
				range_flag = 1;
			}
		}
		if(!range_flag) this.element.dataset.size = '';
	}

	var allElements = document.querySelectorAll('[data-sizes]');

	for (var element of allElements) {
		new ElementMediaQuery(element, JSON.parse(element.dataset.sizes));
	}

})(this);

Alternatively, we can remove data-sizes and initialize a new object as so:

var el = new ElementMediaQuery(document.getElementById('container'), {small: {minWidth:200, maxWidth: 600}, medium: {minWidth:600, maxWidth: 800}});

With this way, you can call the method updateSize on el if you want to manually update the data-size attribute based on the current element width.

Finally, you can use the onWidthChange event to do something when the width of the element changes:

document.getElementById('container').addEventListener('onWidthChange', function (e) { 
    console.log('Width Changed')
}, false);

Here’s the final result:

See the Pen evzvzm by alialaa (@alialaa) on CodePen.dark

 

Browser Compatibility

This approach uses MutationObserver which is not supported on old IE versions. You will need another workaround if you need to support these browsers or maybe use a shim layer for MutationObserver.

Ali Alaa

Front-end developer from Egypt. Telecommunications Engineering graduate. Been working in web development since graduation.