f w h

PSA: Viewport Units on SVG

A quick note, learned from experience: viewport units (vw, vh, vmin, vmax) do not work as height or width attributes in Firefox. I tested the latest (as of this post’s pub date) versions of: Chrome, Firefox, Edge and IE11. It seems to work perfectly in all of those except Firefox.

What do I mean? Take a look:


<svg height="10vh" width="10vw">
	<!-- SVG innards -->
</svg>

This works with no issue in Chrome most browsers, but will be ignored in Firefox. Why? I’m guessing it’s Firefox sticking closely to the spec, as Mozilla doesn’t list them as an available option in their MDN document on SVG sizes, and neither does the W3C spec for the length data-type on SVGs.

Fret not, however, because I have a fix for it.

Viewport units are simply a percentage of the viewport’s, or as the CSS spec states, ‘initial containing block’ size. Thus, if your viewport is 1000px wide, 1vw is equivalent to 10px. The only problem is that it will need to adjust automatically when the viewport changes.

The script below sets the viewport units aside in a data-* attribute, then uses them to calculate a pixel value for them:


// My viewport script from a previous post
function viewport(){

	let w = window.innerWidth;
	let h = window.innerHeight;
	
	let orientation = w > h ? 'landscape' : 'portrait';
	
	let output = {
		w: w,
		h: h,
		orientation: orientation
	};

	return output;

}

// Converts the viewport units to a pixel-based length
function viewport2px(size){

	let int = parseInt(size, 10); // Grabs the size and extracts only the number
	let output;
	
	size.includes('h') ? output = ((int / 100) * viewport().h) + 'px' :  output = ((int / 100) * viewport().w) + 'px';
	
	return output;

}

// Sets the new units according to their current size
function sizeSVGs(svgsIn){

	// If a NodeList object is not provided, just select all of them
	let svgs = svgsIn || document.querySelectorAll('svg');
	
	Array.from(svgs).forEach(function(svg){
		// Check the height for viewport units (doesn't check for vmin/vmax - easy enough to add),
		// Sets those units aside for later use,
		// Sets the new px size
		if(svg.getAttribute('height').includes('vw') || svg.getAttribute('height').includes('vh')){
			svg.setAttribute('data-height', svg.getAttribute('height'));
			svg.setAttribute('height', viewport2px(svg.getAttribute('height')));
		}
		// Does the same thing for width
		if(svg.getAttribute('width').includes('vw') || svg.getAttribute('width').includes('vh')){
			svg.setAttribute('data-width', svg.getAttribute('width'));
			svg.setAttribute('width', viewport2px(svg.getAttribute('width')));
		}
	});

}

// Fire on load
window.addEventListener('load', function(){

	sizeSVGs();

});

But what about the resize? Well, remember how we set aside the initial values in a data-*attribute? We just need to grab that value again and recalculate any time a resize event fires.


window.addEventListener('resize', function(){
	Array.from(svgs).forEach(function(svg){
		if(svg.hasAttribute('data-height')) svg.setAttribute('height', vp2px(svg.getAttribute('data-height')));
		if(svg.getAttribute('data-width')) svg.setAttribute('width', vp2px(svg.getAttribute('data-width')));
	});
});

Caveat: This script uses the Array.from()method to convert the SVG NodeList into an array for iteration. No version of IE supports this method. If IE support is required, MDN has a great drop-in polyfill.