Setting dynamic prefixed CSS grid properties with JavaScript

cover image pie charts and code

The following article first appeared on my personal blog. It gives an insight into the kinds of problems we’re solving when working on the Datawrapper app and charts.

A few months ago we launched a new generation of pie charts at Datawrapper. Among many new features this update provides a new chart type: multiple pies (and donuts). In this mode several pies appear next to each other. This concept is known as “small multiples” and allows the viewer of the visualisation to easily compare multiple charts of the same type with different data to each other.

Here’s an example of a Datawrapper multiple pies chart:

We use CSS grid in our implementation of multiple pies, as it is a 2-dimensional layout system which handles both rows and columns.

The problem

Although modern browsers have great support for the grid spec, we still needed to make it work in older browsers, specifically in Internet Explorer 11. IE11 has some grid support but relies on proprietary browser prefixes and only supports an older implementation of the spec.

Normally dealing with this problem would involve using a tool like Autoprefixer to automatically prefix all required CSS properties.

However, in this instance we were facing a problem: the required CSS is being “dynamically” generated and applied using JavaScript. The user creating the chart can adjust the width that each pie should take up in a row in the Datawrapper UI, so the number of rows and columns in the grid gets calculated based on user settings and the width currently available to the visualisation. The way these numbers get determined is a separate topic, but having arrived at e.g. 2 rows and 4 columns, this could be set in CSS with the following properties:

#grid {
  display: grid;
  grid-template-rows: repeat(2, 1fr);
  grid-template-columns: repeat(4, 1fr);
}

Since the number of rows and columns always varies, we can not declare them in our CSS stylesheet. So in our multiple pies these row and column properties are set dynamically using D3, which is used to render the visualisation. D3 has a style method which allows setting CSS properties on a selection. For instance, if we have determined that we want to display 4 columns in our grid, we use:

d3.select("#grid").style("grid-template-columns", "repeat(4, 1fr)");

to set that property (incidentally, it actually gets output as grid-template-columns: 1fr 1fr 1fr 1fr;, which has the same effect). But what about the prefixed version for IE11? The equivalent prefixed CSS rule is -ms-grid-columns: (1fr)[4];. But when trying to set it using D3:

d3.select("#grid").style("-ms-grid-columns", "(1fr)[4]");

it does not have any effect – D3 simply ignores the setting.


Okay, so if D3 doesn’t help us here, how about setting the prefixed properties using JavaScript, after the chart has rendered? After all, we can set our 4 columns in CSS like this:

const gridElement = document.querySelector("#grid");
gridElement.style["grid-template-columns"] = "repeat(4, 1fr)";

So is there a way to set a prefixed CSS property in the same way? Trying out:

gridElement.style["-ms-grid-columns"] = "(1fr)[4]";

fails again. It seems that setting prefixed CSS properties with JavaScript just does not work.


So in the end I had to work around the problem and came up with a solution that is a bit more labourious.

The solution

If we can’t set CSS properties with JavaScript and we can’t use a stylesheet because we need to generate properties dynamically, then let’s combine these two approaches! After working out the CSS that we need to set, let’s dynamically create a CSS stylesheet, write our CSS into it and then attach it to our visualisation.

Since we only need to do this for Internet Explorer, let’s first ensure that we proceed only in this specific browser by checking the user agent string:

const ua = window.navigator.userAgent;
const isIE = /MSIE|Trident/.test(ua);
if (!isIE) return;

Once we know that we are dealing with Internet Explorer, let’s create a new stylesheet:

const styleElement = document.createElement("style");
styleElement.setAttribute("type", "text/css");

We should already know the number of rows and columns needed for our grid, for this example let’s reference them in an object:

const grid = { rows: 2, cols: 4 };

Now it’s time to create the content of our stylesheet:

let styleContent = `#grid {display: -ms-grid; -ms-grid-columns: (1fr)[${grid.cols}]; 
-ms-grid-rows: (1fr)[${grid.rows}];}`;

Unfortunately this does not quite end here. If you navigate to Autoprefixer online and input our desired CSS on the left:

#grid {
  display: grid;
  grid-template-rows: repeat(2, 1fr);
  grid-template-columns: repeat(4, 1fr);
}

you’ll notice that Autoprefixer outputs additional CSS for each element within the grid, such as:

#grid > *:nth-child(1) {
  -ms-grid-row: 1;
  -ms-grid-column: 1;
}

in order for the grid to work in IE11. What a nightmare!

Since we’re already using JavaScript in our solution, let’s add a little script to loop through rows and columns and add these additional properties for each element (assuming that all elements have the class cell):

for (let row = 1; row <= grid.rows; row++) {
  for (let col = 1; col <= grid.cols; col++) {
    const cellNum = col + grid.cols * (row - 1);
    const cellStyle = `#grid .cell:nth-child(${cellNum}) 
{-ms-grid-row: ${row}; -ms-grid-column: ${col};}`;
    styleContent += cellStyle;
  }
}

Now that we have assembled required CSS properties, let’s add them to the stylesheet and finally append it to the head so that it gets parsed by the browser:

styleElement.textContent = styleContent;
document.head.appendChild(styleElement);

Voilà! When the visualisation renders, IE11 will load our dynamically generated stylesheet and display the layout in the same way as a modern browser.

Here’s the whole script.

Conclusion

This example specifically demonstrates setting prefixed CSS grid properties for IE11, but the same principle can be applied in other situations where prefixed CSS needs to be set with JavaScript.

Note: I used ES6 syntax throughout, so you would have to convert it to ES5 in order for it to work in IE11.

If you found this useful or if you can think of a different approach to solving this problem, let me know on Twitter!

Comments