Edge Goes Chromium: What Does it Mean for Front-End Developers?

In December 2018, Microsoft announced that Edge would adopt Chromium, the open source project that powers Google Chrome. Many within the industry reacted with sadness at the loss of browser diversity. Personally, I was jubilant. An official release date has yet to be announced, but it will be at some point this year. With its release, a whole host of HTML, JavaScript and CSS features will have achieved full cross-browser support.

The preview build is now available for Windows, and coming soon for Mac.

Not so long ago, I penned an article titled “The Long Slow Death of Internet Explorer.” Some of us are lucky enough have abandoned that browser already. But it wasn’t the only thing holding us back. Internet Explorer was the browser we all hated and Edge was meant to be its much-improved replacement. Unfortunately, Edge itself was quite the laggard. EdgeHTML is a fork of Trident, the engine that powered Internet Explorer. Microsoft significantly under-invested in Edge. The apple didn’t fall far from the tree. Edge’s User Voice website was a nice idea, allowing developers to vote for which features they wanted to be implemented. Unfortunately, as Dave Rupert put it, voting on the site was “like throwing coins in a wishing well.” The most requested features were left unimplemented for years.

There are a lot of features that pre-Chromium Edge doesn’t currently support but are available in other modern browsers and, once they’ve made the switch, we’ll be able to use them. Many of them can’t be polyfilled or worked around, so this release is a big deal.

Features we can look forward to using

So just what are those features, exactly? Let’s outline them right here and start to get excited about all the new things we’ll be able to do.

Custom Elements and Shadow DOM

Together, custom elements and shadow DOM allow developers to define custom, reusable and encapsulated components. A lot of people were asking for this one. People have been voting for its implementation since 2014, and we’re finally getting it.

HTML details and summary elements

The <details> and <summary> elements are part of HTML5 and have been supported since 2011 in Chrome. Used together, the elements generate a simple widget to show and hide content. While it is trivial to implement something similar using JavaScript, the <details> and <summary> elements work even when JavaScript is disabled or has failed to load.

See the Pen
details/summary
by CSS GRID (@cssgrid)
on CodePen.

Javascript Font Loading API

This one means a lot to some people. All modern browsers now support the CSS font-display property. However, you still might want to load your fonts with JavaScript. Font-loading monomaniac Zach Leatherman has an explainer of why you might want to load fonts with JavaScript even though we now have broad support for font-display. Ditching polyfills for this API is important because this JavaScript is, according to Zach:

[…] usually inlined in the critical path. The time spent parsing and executing polyfill JavaScript is essentially wasted on browsers that support the native CSS Font Loading API.”

In an article from 2018, Zach lamented:

[…] browser-provided CSS Font Loading API has pretty broad support and has been around for a long time but is confoundedly still missing from all available versions of Microsoft Edge.”

No longer!

JavaScript flat and flatMap

Most easily explained with a code snippet, flat() is useful when you have an array nested inside another array.

const things = ['thing1', 'thing2', ['thing3', ['thing4']]]
const flattenedThings = things.flat(2); // Returns ['thing1', 'thing2', 'thing3', 'thing4']

As its name suggests, flatMap() is equivalent to using both the map() method and flat().

These methods are also supported in Node 11. 🎉

JavaScript TextEncoder and TextDecoder

TextEncoder and TextDecoder are part of the encoding spec. They look to be useful when working with streams.

JavaScript Object rest and object spread

These are just like rest and spread properties for arrays.

const obj1 = { a: 100, b: 2000
} const obj2 = { c: 11000, d: 220
} const combinedObj = {...obj1, ...obj2} // {a: 100, b: 2000, c: 11000, d: 220}

JavaScript modules: dynamic import

Using a function-like syntax, dynamic imports allow you to lazy-load ES modules when a user needs them.

button.addEventListener("click", function() { import("./myModule.js").then(module => module.default());
});

CSS background-blend-mode property

background-blend-mode brings Photoshop style image manipulation to the web.

CSS prefers-reduced-motion media query

I can’t help feeling that not making people feel sick should be the default of a website, particularly as not all users will be aware that this setting exists. As animation on the web becomes more common, it’s important to recognize that animation can cause causes dizziness, nausea and headaches for some users.

CSS caret-color property

Admittedly a rather trivial feature, and one that could have safely and easily been used as progressive enhancement. It lets you style the blinking cursor in text input fields.

8-digit hex color notation

It’s nice to have consistency in a codebase. This includes sticking to either
the RGB, hexadecimal or HSL color format. If your preferred format is hex, then you had a problem because it required a switch to rgba() any time you needed to define transparency. Hex can now include an alpha (transparency) value. For example, #ffffff80 is equivalent to rgba(255, 255, 255, .5). Arguably, it’s not the most intuitive color format and has no actual benefit over rgba().

Intrinsic sizing

I’ve not seen as much hype or excitement for intrinsic sizing as some other new CSS features, but it’s the one I’m personally hankering for the most. Intrinsic sizing determines sizes based on the content of an element and introduces three new keywords into CSS: min-content, max-content and fit-content(). These keywords can be used most places that you would usually use a length, like height, width, min-width, max-width, min-height, max-height, grid-template-rows, grid-template-columns, and flex-basis.

CSS text-orientation property

Used in conjunction with the writing-mode property, text-orientation, specifies the orientation of text, as you might expect.

See the Pen
text-orientation: upright
by CSS GRID (@cssgrid)
on CodePen.

CSS :placeholder-shown pseudo-element

placeholder-shown was even available in Internet Explorer, yet somehow never made it into Edge… until now. UX research shows that placeholder text should generally be avoided. However, if you are using placeholder text, this is a handy way to apply styles conditionally based on whether the user has entered any text into the input.

CSS place-content property

place-content is shorthand for setting both the align-content and justify-content.

See the Pen
place-content
by CSS GRID (@cssgrid)
on CodePen.

CSS will-change property

The will-change property can be used as a performance optimization, informing the browser ahead of time that an element will change. Pre-Chromium Edge was actually good at handling animations performantly without the need for this property, but it will now have full cross-browser support.

CSS all property

https://css-tricks.com/almanac/properties/a/all/”>all is a shorthand for setting all CSS properties at once.

For example, setting button { all: unset; } is equivalent to:

button { background: none; border: none; color: inherit; font: inherit; outline: none; padding: 0;
}

Sadly, though, the revert keyword still hasn’t been implemented anywhere other than Safari, which somewhat limits the mileage we can get out of the all property.

CSS Shapes and Clip Path

Traditionally, the web has been rectangle-centric. It has a box model, after all. While we no longer need floats for layout, we can use them creatively for wrapping text around images and shapes with the shape-outside property. This can be combined with the clip-path property, which brings the ability to display an image inside a shape.

Clippy is an online clip-path editor

CSS :focus-within pseudo-class

If you want to apply special styles to an entire form when any one of its inputs are in focus, then :focus-within is the selector for you.

CSS contents keyword

This is pretty much essential if you’re working with CSS grid. This had been marked as “not planned” by Edge, despite 3,920 votes from developers.

For both flexbox and grid, only direct children become flex items or grid items, respectively. Anything that is nested deeper cannot be placed using flex or grid-positioning. In the words of the spec, when display: contents is applied to a parent element, “the element must be treated as if it had been replaced in the element tree by its contents,” allowing them to be laid out with a grid or with flexbox. Chris goes into a more thorough explanation that’s worth checking out.

There are, unfortunately, still some bugs with other browser implementations that affect accessibility.

The future holds so much more promise

We’ve only looked at features that will be supported by all modern browsers when Edge makes the move to Chromium. That said, the death of legacy Edge also makes a lot of other features feel a lot closer. Edge was the only browser dragging its feet on the Web Animation API and that showed no interest in any part of the Houdini specs, for example.

Credit: https://ishoudinireadyyet.com

The impact on browser testing

Testing in BrowserStack (left) and various browser apps on my iPhone (right)

Of course, the other huge plus for web developers is less testing. A lot of neglected Edge during cross-browser testing, so Edge users were more likely to have a broken experience. This was the main reason Microsoft decided to switch to Chromium. If your site is bug-free in one Chromium browser, then it’s probably fine in all of them. In the words of the Edge team, Chromium will provide “better web compatibility for our customers and less-fragmentation of the web for all web developers.” The large variety of devices and browsers makes browser testing one of the least enjoyable tasks that we’re responsible for as front-end developers. Edge will now be available for macOS users which is great for the many of us who work on a Mac. A subscription to BrowserStack will now be slightly less necessary.

Do we lose anything?

To my knowledge, the only feature that was supported everywhere except Chrome is SVG color fonts, which will no longer work in the Edge browser. Other color font formats (COLR, SBIX, CBDT/CBLC) will continue to work though.

What about other browsers?

Admittedly, Edge wasn’t the last subpar browser. All the features in this article are unsupported in Internet Explorer, and always will be. If you have users in Russia, you’ll need to support Yandex. If you have users in Africa, you’ll need to support Opera Mini. If you have users in China, then UC and QQ will be important to test against. If you don’t have these regional considerations, there’s never been a better time to ditch support for Internet Explorer and embrace the features the modern web has to offer. Plenty of PC users have stuck with Internet Explorer purely out of habit. Hopefully, a revamped Edge will be enough to tempt them away. An official Microsoft blog entry titled “The perils of using Internet Explorer as your default browser” concluded that, “Internet Explorer is a compatibility solution…developers by and large just aren’t testing for Internet Explorer these days.” For its remaining users, the majority of the web must look increasingly broken. It’s time to let it die.

Is Google a megalomaniac?

Life is about to get easier for web developers, yet the response to the Microsoft’s announcement was far from positive. Mozilla, for one, had a stridently pessimistic response, which accused Microsoft of “officially giving up on an independent shared platform for the internet.” The statement described Google as having “almost complete control of the infrastructure of our online lives” and a “monopolistic hold on unique assets.” It concluded that “ceding control of fundamental online infrastructure to a single company is terrible.”

Many have harked back to the days of IE6, the last time a browser achieved such an overwhelming market share. Internet Explorer, having won the browser war, gave in to total stagnation. Chrome, by contrast, ceaselessly pushes new features. Google participates actively with the web standards bodies the W3C and the WHATWG. Arguably though, it has an oversized influence in these bodies and the power to dictate the future shape of the web. Google Developer Relations does have a tendency to hype features that have shipped only in Chrome.

From competition to collaboration

Rather than being the new IE, Edge can help innovate the web forward. While it fell behind in many areas, it did lead the way for CSS grid, CSS exclusions, CSS regions and the new HTML imports spec. In a radical departure from historical behavior, Microsoft have become one of the world’s largest supporters of open source projects. That means all major browsers are now open source. Microsoft have stated that they intend to become a significant contributor to Chromium — in fact, they’ve already racked up over 300 merges. This will help Edge users, but will also benefit users of Chrome, Opera, Brave, and other Chromium-based browsers.

The post Edge Goes Chromium: What Does it Mean for Front-End Developers? appeared first on CSS-Tricks.

CSS Grid in IE: Duplicate area names now supported!

Autoprefixer is now up to version 9.3.1 and there have been a lot of updates since I wrote the original three-part CSS Grid in IE series — the most important update of which is the new grid-areas system. This is mostly thanks to Bogdan Dolin, who has been working like crazy to fix loads of Autoprefixer issues. Autoprefixer’s grid translations were powerful before, but they have gotten far more powerful now!

Article Series:

  1. Debunking common IE Grid misconceptions
  2. CSS Grid and the new Autoprefixer
  3. Faking an auto-placement grid with gaps
  4. Duplicate area names now supported! (This Post)

How Autoprefixer supports duplicate area names

In Part 2 of the series, I pointed out why Autoprefixer wasn’t able to handle duplicate area names that were used across multiple selectors.

To summarize, this is the example I gave in the article:

.grid-alpha { grid-template-areas: "delta echo";
} .grid-beta { grid-template-areas: "echo delta";
} .grid-cell { /* What column does .grid-cell go in? */ -ms-grid-column: ???; grid-area: echo;
}

We thought that since Autoprefixer didn’t have access to the DOM, there was no way of knowing what grid the grid cell belonged to and thus which column the grid cell should go in.

However, I realized something. Grid cells are only ever affected by their direct parent element (ignoring display: contents). That meant that if the grid cell exists, it will always be a direct child of the grid template element. This gave me an idea for how we can solve the grid-area name conflict! 😁

.grid-alpha { grid-template-areas: "delta echo";
} .grid-beta { /* Uh oh, duplicate area names! */ grid-template-areas: "echo delta";
} .grid-cell { /* We will use the first occurrence by default */ -ms-grid-column: 2; grid-area: echo;
} /* We have detected a conflict! Prefix the class with the parent selector.
*/
.grid-beta > .grid-cell { /* NO MORE CONFLICT! */ -ms-grid-column: 1;
}

The entire grid-areas system needed to be re-written to achieve this, but it was totally worth the effort.

The way this new system works is that .grid-cell will default to using the first grid-template-areas property that it comes across. When it hits a conflicting grid template, it will create a new rule placing the parent selector in front of the child selector like so:

[full parent selector] > [direct child selector]

This is what we know purely from looking at the CSS and knowing how CSS Grid works:

  • A grid cell must be a direct descendant of the parent grid container for CSS Grid to work (assuming display: contents isn’t used).
  • grid-beta > .grid-cell will only work on .grid-cell elements that have been placed directly inside a .grid-beta element.
  • .grid-beta > .grid-cell will have no effect on .grid-cell elements placed directly inside .grid-alpha elements.
  • .grid-beta > .grid-cell will have no effect on .grid-cell elements nested deeply inside .grid-beta elements.
  • .grid-beta > .grid-cell will override the styling of the lonely .grid-cell CSS rule both because of rule order and specificity.

Because of those reasons, Autoprefixer can pretty safely resolve these conflicts without having any access to the DOM.

That last point in the list can be a little bit dangerous. It increases specificity which means that it may cause some IE styles to override others in a way that is different from how modern overrides are working. Since the generated rules only hold IE-specific grid styles, and they only apply under very specific circumstances, this is unlikely to cause an issue in 99.999% of circumstances. There is still potential for edge cases though.

If you ever find yourself needing to increase the specificity of the grid cell selector, here’s how to go about it.

Instead of writing this:

.grid { grid-template-areas: "area-name";
} .grid-cell { grid-area: area-name;
}

…we write the rule like this:

.grid { grid-template-areas: "area-name";
} .grid > .grid-cell { grid-area: area-name;
}

Autoprefixer will retain the selector and output something like this:

.grid { grid-template-areas: "area-name";
} .grid > .grid-cell { -ms-grid-column: 1; -ms-grid-row: 1; grid-area: area-name;
}

The exciting new possibilities!

So why is duplicate area name support so exciting?

Well, for one, it was just plain annoying when accidentally using a duplicate area name in older versions of Autoprefixer. It would break in IE and, in older versions of Autoprefixer, it would fail silently.

The main reason it is exciting though is that it opens up a whole new world of IE-friendly CSS Grid possibilities!

Use modifier classes to adjust a grid template

This was the use case that really made me want to get duplicate area name support into Autoprefixer. Think about this. You have a typical site with a header, a footer, a main area, and a sidebar down either side.

See the Pen Basic website layout by Daniel Tonon (@daniel-tonon) on CodePen.

Sometimes we want both sidebars, sometimes we want one sidebar, and sometimes we want no sidebars. This was very difficult to manage back when Autoprefixer didn’t support duplicate area names since we couldn’t share area names across multiple grid templates. We would have to do something like this for it to work:

/* Before Duplicate area names were supported - n = no side-bars - f = first sidebar only - s = second sidebar only - fs = first and second sidebars
*/
.main-area { display: grid; grid-template: "content-n" / 1fr;
} .main-area.has-first-sidebar { grid-template: "first-sb-f content-f" / 300px 1fr;
} .main-area.has-second-sidebar { grid-template: "content-s second-sb-s" / 1fr 300px;
}
.main-area.has-first-sidebar.has-second-sidebar { grid-template: "first-sb-fs content-fs second-sb-fs" / 200px 1fr 200px;
} .main-area > .content { grid-area: content-n; /* no side-bars */
} .main-area > .sidebar.first { grid-area: first-sb-f; /* first sidebar only */
} .main-area > .sidebar.second { grid-area: second-sb-s; /* second sidebar only */
} .main-area.has-first-sidebar > .content { grid-area: content-f; /* first sidebar only */
} .main-area.has-second-sidebar > .content { grid-area: content-s; /* second sidebar only */
} .main-area.has-first-sidebar.has-second-sidebar > .content { grid-area: content-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.first { grid-area: first-sb-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.second { grid-area: second-sb-fs; /* first and second sidebars */
}
Autoprefixer translation
/* Before Duplicate area names were supported - n = no sidebars - f = first sidebar only - s = second sidebar only - fs = first and second sidebars
*/ .main-area { display: -ms-grid; display: grid; -ms-grid-rows: auto; -ms-grid-columns: 1fr; grid-template: "content-n" / 1fr;
} .main-area.has-first-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 300px 1fr; grid-template: "first-sb-f content-f" / 300px 1fr;
} .main-area.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 1fr 300px; grid-template: "content-s second-sb-s" / 1fr 300px;
} .main-area.has-first-sidebar.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 200px 1fr 200px; grid-template: "first-sb-fs content-fs second-sb-fs" / 200px 1fr 200px;
} .main-area > .content { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: content-n; /* no side-bars */
} .main-area > .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: first-sb-f; /* first sidebar only */
} .main-area > .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: second-sb-s; /* second sidebar only */
} .main-area.has-first-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: content-f; /* first sidebar only */
} .main-area.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: content-s; /* second sidebar only */
} .main-area.has-first-sidebar.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: content-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: first-sb-fs; /* first and second sidebars */
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 3; grid-area: second-sb-fs; /* first and second sidebars */
}

🤢 Oh yeah, and don’t forget media queries! 🤮

That example was based off actual code that I had to write for an actual project… so yeah, I really wanted to get duplicate area name support into Autoprefixer.

Now that we do have duplicate area name support, we can simplify that code down to something much nicer 😊

/* Duplicate area names now supported! This code will work perfectly in IE with Autoprefixer 9.3.1
*/ .main-area { display: grid; grid-template: "content" / 1fr;
} .main-area.has-first-sidebar { grid-template: "first-sb content" / 300px 1fr;
} .main-area.has-second-sidebar { grid-template: "content second-sb" / 1fr 300px;
} .main-area.has-first-sidebar.has-second-sidebar { grid-template: "first-sb content second-sb" / 200px 1fr 200px;
} .content { grid-area: content;
} .sidebar.first { grid-area: first-sb;
} .sidebar.second { grid-area: second-sb;
}
Autoprefixer translation
.main-area { display: -ms-grid; display: grid; -ms-grid-rows: auto; -ms-grid-columns: 1fr; grid-template: "content" / 1fr;
} .main-area.has-first-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 300px 1fr; grid-template: "first-sb content" / 300px 1fr;
} .main-area.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 1fr 300px; grid-template: "content second-sb" / 1fr 300px;
} .main-area.has-first-sidebar.has-second-sidebar { -ms-grid-rows: auto; -ms-grid-columns: 200px 1fr 200px; grid-template: "first-sb content second-sb" / 200px 1fr 200px;
} .content { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: content;
} .main-area.has-first-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2;
} .main-area.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 1;
} .main-area.has-first-sidebar.has-second-sidebar > .content { -ms-grid-row: 1; -ms-grid-column: 2;
} .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: first-sb;
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.first { -ms-grid-row: 1; -ms-grid-column: 1;
} .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: second-sb;
} .main-area.has-first-sidebar.has-second-sidebar > .sidebar.second { -ms-grid-row: 1; -ms-grid-column: 3;
}

With that CSS, we are now able to use the has-first-sidebar and has-second-sidebar classes on the grid element to modify what grid template the browser uses. We no longer have to worry about defining exactly what grid grid cells are placed in.

Assign components reusable area names

One of the limitations of being forced to use unique area names was that components could not be given a single, consistent re-usable area name. We were forced to come up with unique area names for every grid.

Now that Autoprefixer has duplicate area name support, that limitation is gone. We can provide every component with a single area name that is based on their component name. We can then reference that one area name whenever we want to place that component inside a grid layout.

/* header.scss */
.header { grid-area: header;
} /* nav.scss */
.nav { grid-area: nav;
} /* sidebar.scss */
.sidebar { grid-area: sidebar;
} /* content.scss */
.content { grid-area: content;
} /* subscribe.scss */
.subscribe { grid-area: subscribe;
} /* footer.scss */
.footer { grid-area: footer;
} /* layout.scss */
.single-sidebar-layout { display: grid; grid-template-areas: "header header" "nav content" "subscribe content" "footer footer";
} .double-sidebar-layout { display: grid; grid-template-areas: "header header header" "nav content sidebar" "nav subscribe sidebar" "footer footer footer";
}
Autoprefixer translation
/* header.scss */
.header { -ms-grid-row: 1; -ms-grid-column: 1; -ms-grid-column-span: 2; grid-area: header;
} .double-sidebar-layout > .header { -ms-grid-row: 1; -ms-grid-column: 1; -ms-grid-column-span: 3;
} /* nav.scss */
.nav { -ms-grid-row: 2; -ms-grid-column: 1; grid-area: nav;
} .double-sidebar-layout > .nav { -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 1;
} /* sidebar.scss */
.sidebar { -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 3; grid-area: sidebar;
} /* content.scss */
.content { -ms-grid-row: 2; -ms-grid-row-span: 2; -ms-grid-column: 2; grid-area: content;
} .double-sidebar-layout > .content { -ms-grid-row: 2; -ms-grid-column: 2;
} /* subscribe.scss */
.subscribe { -ms-grid-row: 3; -ms-grid-column: 1; grid-area: subscribe;
} .double-sidebar-layout > .subscribe { -ms-grid-row: 3; -ms-grid-column: 2;
} /* footer.scss */
.footer { -ms-grid-row: 4; -ms-grid-column: 1; -ms-grid-column-span: 2; grid-area: footer;
} .double-sidebar-layout > .footer { -ms-grid-row: 4; -ms-grid-column: 1; -ms-grid-column-span: 3;
} /* layout.scss */
.single-sidebar-layout { display: -ms-grid; display: grid; grid-template-areas: "header header" "nav content" "subscribe content" "footer footer";
} .double-sidebar-layout { display: -ms-grid; display: grid; grid-template-areas: "header header header" "nav content sidebar" "nav subscribe sidebar" "footer footer footer";
}

Here’s what we’ve got. Note that this should be viewed in IE.

See the Pen component-area-name technique by Daniel Tonon (@daniel-tonon) on CodePen.

Duplicate area name limitation

There is one fairly common use case that Autoprefixer still can’t handle at the moment. When the parent selector of the grid cell does not match up with the grid template selector, it tries to resolve a duplicate area name:

.grand-parent .mother { grid-template-areas: "child";
} .grand-parent .father { grid-template-areas: "child child";
} /* This will work */
.grand-parent .mother .child { grid-area: child;
} /* This does not work because: - ".uncle" != ".grand-parent .mother" - ".uncle" != ".grand-parent .father" - "child" is a duplicate area name
*/
.uncle .child { grid-area: child;
}

Here is a more realistic scenario of the current limitation in Autoprefixer’s algorithm:

.component .grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} /* This rule triggers duplicate area name conflicts. */
.component.modifier .grid { grid-template-areas: "one ... two"; grid-template-columns: 1fr 1fr 1fr;
} /* This does not work because: - ".component" != ".component .grid" - ".component" != ".component.modifier .grid" - area names "one" and "two" both have duplicate area name conflicts
*/
.component .cell-one { grid-area: one;
} .component .cell-two { grid-area: two;
}

There are really only three ways of resolving this conflict at the moment.

Option 1: Remove the parent selector from child elements

Without any safeguards in place for scoping styles to a particular component, this is by far the most dangerous way to resolve the issue. I don’t recommend it.

.component .grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} .component.modifier .grid { grid-template-areas: "one ... two"; grid-template-columns: 1fr 1fr 1fr;
} .cell-one { grid-area: one;
} .cell-two { grid-area: two;
}
Autoprefixer translation
.component .grid { display: -ms-grid; display: grid; grid-template-areas: "one two"; -ms-grid-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} .component.modifier .grid { grid-template-areas: "one ... two"; -ms-grid-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} .cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .component.modifier .grid > .cell-one { -ms-grid-row: 1; -ms-grid-column: 1;
} .cell-two { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: two;
} .component.modifier .grid > .cell-two { -ms-grid-row: 1; -ms-grid-column: 3;
}

Option 2: Go back to using unique area names

This solution is pretty ugly, but if you have no control over the HTML, then this is probably the best way of handling the issue.

.component .grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} .component .cell-one { grid-area: one;
} .component .cell-two { grid-area: two;
} .component.modifier .grid { grid-template-areas: "modifier_one ... modifier_two"; grid-template-columns: 1fr 1fr 1fr;
} .component.modifier .cell-one { grid-area: modifier_one;
} .component.modifier .cell-two { grid-area: modifier_two;
}
Autoprefixer translation
.component .grid { display: -ms-grid; display: grid; grid-template-areas: "one two"; -ms-grid-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} .component .cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .component .cell-two { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: two;
} .component.modifier .grid { grid-template-areas: "modifier_one ... modifier_two"; -ms-grid-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} .component.modifier .cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: modifier_one;
} .component.modifier .cell-two { -ms-grid-row: 1; -ms-grid-column: 3; grid-area: modifier_two;
}

Option 3: Use a BEM-style naming convention

Yep, if you use the BEM naming convention (or something similar), this will practically never be an issue for you. This is easily the preferred way of dealing with the issue if it’s an option.

.component__grid { display: grid; grid-template-areas: "one two"; grid-template-columns: 1fr 1fr;
} .component__grid--modifier { grid-template-areas: "one ... two"; grid-template-columns: 1fr 1fr 1fr;
} .component__cell-one { grid-area: one;
} .component__cell-two { grid-area: two;
}
Autoprefixer translation
.component__grid { display: -ms-grid; display: grid; grid-template-areas: "one two"; -ms-grid-columns: 1fr 1fr; grid-template-columns: 1fr 1fr;
} .component__grid--modifier { grid-template-areas: "one ... two"; -ms-grid-columns: 1fr 1fr 1fr; grid-template-columns: 1fr 1fr 1fr;
} .component__cell-one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .component__grid--modifier > .component__cell-one { -ms-grid-row: 1; -ms-grid-column: 1;
} .component__cell-two { -ms-grid-row: 1; -ms-grid-column: 2; grid-area: two;
} .component__grid--modifier > .component__cell-two { -ms-grid-row: 1; -ms-grid-column: 3;
}

I have some issues with BEM, but with a few tweaks to the syntax, it can be a really great way to control CSS specificity and scoping.

Other Autoprefixer updates

While I’m here, I’ll give you an update on a couple of other developments that have occurred in Autoprefixer since I wrote the original CSS Grid in IE series.

Before we dive in though, here is how to set things up in CodePen in case you want to follow along with your own experiments.

Using Autoprefixer Grid translations in CodePen

CodePen has already upgraded to version 9.3.1 of Autoprefixer. You will now easily be able to integrate IE-friendly CSS Grid styles into your pens. First, make sure that Autoprefixer is enabled in the CodePen settings (Settings > CSS > [Vendor Prefixing] > Autoprefixer).

Step 1: Turn on Autoprefixer in CodePen

Then add the all new /* autoprefixer grid: on */ control comment to the top of the CSS Panel. I will dive into this awesome new feature a bit more later.

Step 2: Add the control comment

You can now write modern IE-friendly CSS Grid code and CodePen will automatically add in all of the IE prefixes for you.

“View compiled CSS” button
The compiled CSS view

In order to actually test your pens in IE though, you will need to view the pen in “Debug Mode” (Change View > Debug Mode). The pen will need to be saved before you will have access to this view.

Enabling Debug Mode for testing in IE

If you’d like to try out a tool that shows you real-time Autoprefixer output, try the online tool just for that. Input CSS on the left. It will output Autoprefixer’s translated CSS on the right as you type.

autoprefixer.github.io

New Autoprefixer control comments

We don’t always have the ability to alter the Autoprefixer configuration settings. If you have ever tried to do some IE-friendly CSS Grid experiments in CodePen in the past, you will know the pain of not having direct access to the Autoprefixer settings. There are also some frameworks like Create React App and Angular that don’t allow users to alter the Autoprefixer settings. There was also a bit of a skill barrier that prevented some users from using Grid because they were unsure of how to enable Grid translations.

This limitation was causing pain for many users but then Andrey Alexandrov submitted a pull request that introduced a new control comment. This new control comment gave users the ability to easily turn grid translations on and off from inside the CSS file. This was far easier than doing it through configuration settings. It was also accessible to all users no matter how they compile their CSS.

Adding /* autoprefixer grid: on */ will enable grid translations for that entire block while /* autoprefixer grid: off */ will disable grid translations for that block. Only the first grid control comment in a block will be applied.

/* Globally enable grid prefixes */
/* autoprefixer grid: on */ .grid { display: grid;
} .non-ie .grid { /* Turn off grid prefixes but only for this block */ /* autoprefixer grid: off */ display: grid; /* Grid control comments affect the whole block. This control comment is ignored */ /* autoprefixer grid: on */ grid-column: 1;
}

The above code translates into the following (I’ve filtered out the explanation comments):

/* autoprefixer grid: on */ .grid { display: -ms-grid; display: grid;
} .non-ie .grid { /* autoprefixer grid: off */ display: grid; /* autoprefixer grid: on */ grid-column: 1;
}

@supports can disable grid translations

The new control comments aren’t the only way to selectively prevent Autoprefixer from outputting grid translation code.

Autoprefixer will never be able to support implicit grid auto-placement. If you use an @supports statement that checks for something like grid-auto-rows: 0, Autoprefixer will not output Grid translations for code entered inside that statement.

.prefixed .grid { display: -ms-grid; display: grid;
} /* Checking grid-auto-* support prevents prefixes */
@supports (grid-auto-rows: 0) { .modern .grid { display: grid; }
} /* Checking basic grid support still outputs prefixes */
@supports (display: grid) { .still-prefixed .grid { display: -ms-grid; display: grid; }
}

To support IE, Autoprefixer can generate something like 50 lines of extra CSS sometimes. If you are writing Grid code inside an @supports statement, you will want IE translations to be turned off to reduce the weight of your CSS. If you don’t want to use @supports (grid-auto-rows: 0) then you can use a control comment inside the @supports statement instead.

@supports (display: grid) { .prefixed .grid { display: -ms-grid; display: grid; }
} @supports (display: grid) { /* autoprefixer grid: off */ .modern .grid { display: grid; }
}

Autoprefixer now inherits grid-gaps

In the original article series, I said that Autoprefixer was unable to inherit grid-gap values. In version 9.1.1, grid-gap values were able to inherit through media queries. In version 9.3.1, they have become inheritable through more specific selectors as well.

.grid { display: grid; grid-gap: 20px; /* grid-gap is stated here */ grid-template: "one two" / 1fr 1fr;
} @media (max-width: 600px) { .grid { /* grid-gap is now inhereited here */ grid-template: "one" "two" / 1fr; }
} .grid.modifier { /* grid-gap is now inhereited here as well */ grid-template: "one" "two" / 1fr;
} .one { grid-area: one; }
.two { grid-area: two; }
Autoprefixer translation
.grid { display: -ms-grid; display: grid; grid-gap: 20px; /* grid-gap is stated here */ -ms-grid-rows: auto; -ms-grid-columns: 1fr 20px 1fr; grid-template: "one two" / 1fr 1fr;
} @media (max-width: 600px) { .grid { /* grid-gap is now inhereited here */ -ms-grid-rows: auto 20px auto; -ms-grid-columns: 1fr; grid-template: "one" "two" / 1fr; }
} .grid.modifier { /* grid-gap is now inherited here as well */ -ms-grid-rows: auto 20px auto; -ms-grid-columns: 1fr; grid-template: "one" "two" / 1fr;
} .one { -ms-grid-row: 1; -ms-grid-column: 1; grid-area: one;
} .grid.modifier > .one { -ms-grid-row: 1; -ms-grid-column: 1;
} .two { -ms-grid-row: 1; -ms-grid-column: 3; grid-area: two;
} .grid.modifier > .two { -ms-grid-row: 3; -ms-grid-column: 1;
} @media (max-width: 600px) { .one { -ms-grid-row: 1; -ms-grid-column: 1; } .two { -ms-grid-row: 3; -ms-grid-column: 1; }
}

grid-template: span X; now works

In Part 2 this series, I mentioned that the following syntax doesn’t work:

.grid-cell { grid-column: span 2;
}

This was fixed in version 9.1.1. It now translates into the following:

.grid-cell { -ms-grid-column-span: 2; grid-column: span 2;
}

New warning for mixing manual and area based placement

We had a user complain that Autoprefixer wasn’t handling grid areas correctly. Upon further investigation, we realized that they were applying both a grid-area and a grid-column setting within the same CSS rule.

.grid { display: grid; grid-template-areas: "a b";
} /* This doesn't work very well */
.grid-cell { grid-column: 2; grid-area: a;
}

Either use grid-area on its own, or us grid-column & grid-row. Never use both at the same time.

If you are curious, this is what Autoprefixer outputs for the above code:

.grid { display: -ms-grid; display: grid; grid-template-areas: "a b";
} /* This doesn't work very well */
.grid-cell { -ms-grid-row: 1; -ms-grid-column: 1; -ms-grid-column: 2; grid-column: 2; grid-area: a;
}

There is one exception to this. You may need a grid cell to overlap other grid cells in your design. You want to use grid-template-areas for placing your grid cells due to how much easier Autoprefixer makes the overall cell placement. You can’t really create cell overlaps using grid-template-areas though. To create this overlap, you can use grid-[column/row]-end: span X; after the grid-area declaration to force the overlap.

.grid { display: grid; grid-template-areas: "a . ." "a b b" "a . ."; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr;
} .grid-cell-a { grid-area: a; /* place cell span after the grid-area to force an overlap */ grid-column-end: span 2;
}

See the Pen column-span + grid-area experiment by Daniel Tonon (@daniel-tonon) on CodePen.

However if you have declared grid gaps in your grid, you will need to write the column/row span prefix manually while taking the extra columns/rows generated by Autoprefixer into consideration (IE doesn’t support grid-gap). There is an issue for this on GitHub.

.grid { display: grid; grid-template-areas: "a . ." "a b c" "a . ."; grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr 1fr 1fr; grid-gap: 10px;
} .grid-cell-a { grid-area: a; /* IE column span added manually */ -ms-grid-column-span: 3; /* IE spans 2 columns + the gap */ grid-column-end: span 2; /* Modern browsers only span 2 columns */
}

See the Pen column-span + grid-area experiment 2 by Daniel Tonon (@daniel-tonon) on CodePen.

This technique currently produces a warning message in Autoprefixer that can’t easily be hidden. An issue for this is on GitHub.

New warning when using [align/justify/place]-[content/items]

Unfortunately. IE does not have the ability to align grid cells from the parent container. The only way to align grid cells in IE is by using the -ms-grid-row-align and the -ms-grid-column-align properties. These translate to align-self and justify-self in modern day grid syntax.

The following properties will trigger a warning in Autoprefixer if used in conjunction with a CSS Grid property:

  • align-items
  • align-content
  • justify-items
  • justify-content
  • place-items
  • place-content

The align-content, justify-content and place-content properties are pretty much impossible to replicate in IE. The align-items, justify-items and place-items properties, on the other hand, are quite easy to replicate using their self equivalents on the child elements (place-self support was added in 9.3.0).

/* [align/justify/place]-items is *NOT* IE friendly */ .align-justify-items { display: grid; align-items: start; justify-items: end;
} .place-items { display: grid; place-items: start end;
} /*[align/justify/place]-self *IS* IE friendly */ .align-justify-grid { display: grid; }
.align-justify-grid > * { align-self: start; justify-self: end;
} .place-grid { display: grid; }
.place-grid > * { place-self: start end;
}
Autoprefixer translation
/* [align/justify/place]-items is *NOT* IE friendly */ .align-justify-items { display: -ms-grid; display: grid; align-items: start; justify-items: end;
} .place-items { display: -ms-grid; display: grid; place-items: start end;
} /*[align/justify/place]-self *IS* IE friendly */ .align-justify-grid { display: -ms-grid; display: grid; }
.align-justify-grid > * { -ms-grid-row-align: start; align-self: start; -ms-grid-column-align: end; justify-self: end;
} .place-grid { display: -ms-grid; display: grid; }
.place-grid > * { -ms-grid-row-align: start; -ms-grid-column-align: end; place-self: start end;
}

.grid { [align/justify/place]-items } and .grid > * { [align/justify/place]-self } are generally quite interchangeable with one another. This substitution doesn’t always work, but in most cases, the two methods tend to act in much the same way.

Below is a pen demonstrating the difference between the IE friendly alignment method and the IE unfriendly alignment method. In modern browsers they look identical, but in IE one looks the same as the modern browsers and one does not.

See the Pen place-items experiment by Daniel Tonon (@daniel-tonon) on CodePen.

That’s all, folks

I hope you have enjoyed reading about the awesome new improvements the Autoprefixer community has made to the project over the past few months. The new control comments make enabling and disabling grid translations an absolute breeze. The new grid-areas system is also exciting. I love all the new IE-friendly CSS Grid options that the new areas system opens up, and I think you will too. 🙂

Article Series:

  1. Debunking common IE Grid misconceptions
  2. CSS Grid and the new Autoprefixer
  3. Faking an auto-placement grid with gaps
  4. Duplicate area names now supported! (This Post)

The post CSS Grid in IE: Duplicate area names now supported! appeared first on CSS-Tricks.

Using feature detection to write CSS with cross-browser support

In early 2017, I presented a couple of workshops on the topic of CSS feature detection, titled CSS Feature Detection in 2017.

A friend of mine, Justin Slack from New Media Labs, recently sent me a link to the phenomenal Feature Query Manager extension (available for both Chrome and Firefox), by Nigerian developer Ire Aderinokun. This seemed to be a perfect addition to my workshop material on the subject.

However, upon returning to the material, I realized how much my work on the subject has aged in the last 18 months.

The CSS landscape has undergone some tectonic shifts:

  • The Atomic CSS approach, although widely hated at first, has gained some traction through libraries like Tailwind, and perhaps influenced the addition of several new utility classes to Bootstrap 4.
  • CSS-in-JS exploded in popularity, with Styled Components at the forefront of the movement.
  • The CSS Grid Layout spec has been adopted by browser vendors with surprising speed, and was almost immediately sanctioned as production ready.

The above prompted me to not only revisit my existing material, but also ponder the state of CSS feature detection in the upcoming 18 months.

In short:

  1. ❓ Why do we need CSS feature detection at all?
  2. 🛠️ What are good (and not so good) ways to do feature detection?
  3. 🤖 What does the future hold for CSS feature detection?

Cross-browser compatible CSS

When working with CSS, it seems that one of the top concerns always ends up being inconsistent feature support among browsers. This means that CSS styling might look perfect on my browsers of choice, but might be completely broken on another (perhaps an even more popular) browser.

Luckily, dealing with inconsistent browser support is trivial due to a key feature in the design of the CSS language itself. This behavior, called fault tolerance, means that browsers ignore CSS code they don’t understand. This is in stark contrast to languages like JavaScript or PHP that stop all execution in order to throw an error.

The critical implication here is that if we layer our CSS accordingly, properties will only be applied if the browser understands what they mean. As an example, you can include the following CSS rule and the browser will just ignore it— overriding the initial yellow color, but ignoring the third nonsensical value:

background-color: yellow;
background-color: blue; /* Overrides yellow */
background-color: aqy8godf857wqe6igrf7i6dsgkv; /* Ignored */

To illustrate how this can be used in practice, let me start with a contrived, but straightforward situation:

A client comes to you with a strong desire to include a call-to-action (in the form of a popup) on his homepage. With your amazing front-end skills, you are able to quickly produce the most obnoxious pop-up message known to man:

Unfortunately, it turns out that his wife has an old Windows XP machine running Internet Explorer 8. You’re shocked to learn that what she sees no longer resembles a popup in any shape or form.

But! We remember that by using the magic of CSS fault tolerance, we can remedy the situation. We identify all the mission-critical parts of the styling (e.g., the shadow is nice to have, but does not add anything useful usability-wise) and buffer prepend all core styling with fallbacks.

This means that our CSS now looks something like the following (the overrides are highlighted for clarity):

.overlay { background: grey; background: rgba(0, 0, 0, 0.4); border: 1px solid grey; border: 1px solid rgba(0, 0, 0, 0.4); padding: 64px; padding: 4rem; display: block; display: flex; justify-content: center; /* if flex is supported */ align-items: center; /* if flex is supported */ height: 100%; width: 100%;
} .popup { background: white; background-color: rgba(255, 255, 255, 1); border-radius: 8px; border: 1px solid grey; border: 1px solid rgba(0, 0, 0, 0.4); box-shadow: 0 7px 8px -4px rgba(0,0, 0, 0.2), 0 13px 19px 2px rgba(0, 0, 0, 0.14), 0 5px 24px 4px rgba(0, 0, 0, 0.12); padding: 32px; padding: 2rem; min-width: 240px;
} button { background-color: #e0e1e2; background-color: rgba(225, 225, 225, 1); border-width: 0; border-radius: 4px; border-radius: 0.25rem; box-shadow: 0 1px 3px 0 rgba(0,0,0,.2), 0 1px 1px 0 rgba(0,0,0,.14), 0 2px 1px -1px rgba(0,0,0,.12); color: #5c5c5c; color: rgba(95, 95, 95, 1); cursor: pointer; font-weight: bold; font-weight: 700; padding: 16px; padding: 1rem;
} button:hover { background-color: #c8c8c8; background-color: rgb(200,200,200); }

The above example generally falls under the broader approach of Progressive Enhancement. If you’re interested in learning more about Progressive Enhancement check out Aaron Gustafson’s second edition of his stellar book on the subject, titled Adaptive Web Design: Crafting Rich Experiences with Progressive Enhancement (2016).

If you’re new to front-end development, you might wonder how on earth does one know the support level of specific CSS properties. The short answer is that the more you work with CSS, the more you will learn these by heart. However, there are a couple of tools that are able to help us along the way:

  • Can I Use is a widely used directory that contains searchable, up to date support matrices for all CSS features.
  • Stylelint has a phenomenal plugin-called called No Unsupported Browser Features that gives scours errors for unsupported CSS (defined via Browserslist) either in your editor itself or via a terminal command.
  • There are several tools like BrowserStack or Cross Browser Testing that allow you to remotely test your website on different browsers. Note that these are paid services, although BrowserStack has a free tier for open source projects.

Even with all the above at our disposal, learning CSS support by heart will help us plan our styling up front and increase our efficiency when writing it.

Limits of CSS fault tolerance

The next week, your client returns with a new request. He wants to gather some feedback from users on the earlier changes that were made to the homepage—again, with a pop-up:

Once again it will look as follows in Internet Explorer 8:

Being more proactive this time, you use your new fallback skills to establish a base level of styling that works on Internet Explorer 8 and progressive styling for everything else. Unfortunately, we still run into a problem…

In order to replace the default radio buttons with ASCII hearts, we use the ::before pseudo-element. However this pseudo-element is not supported in Internet Explorer 8. This means that the heart icon does not render; however the display: none property on the <input type="radio"> element still triggers on Internet Explorer 8. The implication being that neither the replacement behavior nor the default behavior is shown.

Credit to John Faulds for pointing out that it is actually possible to get the ‘::before’ pseudo-element to work in Internet Explorer 8 if you replace the official double colon syntax with a single colon.

In short, we have a rule (display: none) whose execution should not be bound to its own support (and thus its own fallback structure), but to the support level of a completely separate CSS feature (::before).

For all intents and purposes, the common approach is to explore whether there are more straightforward solutions that do not rely on ::before. However, for the sake of this example, let’s say that the above solution is non-negotiable (and sometimes they are).

Enter User Agent Detection

A solution might be to determine what browser the user is using and then only apply display: none if their browser supports the ::before pseudo-element.

In fact, this approach is almost as old as the web itself. It is known as User Agent Detection or, more colloquially, browser sniffing.

It is usually done as follows:

  • All browsers add a JavaScript property on the global window object called navigator and this object contains a userAgent string property.
  • In my case, the userAgent string is: Mozilla/5.0 (Windows NT10.0;Win64;x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.9 Safari/537.36.
  • Mozilla Developer Network has a comprehensive list of how the above can be used to determine the browser.
  • If we are using Chrome, then the following should return true: (navigator.userAgent.indexOf("chrome") !== -1).
  • However, under the Internet Explorer section on MDN, we just get Internet Explorer. IE doesn’t put its name in the BrowserName/VersionNumber format.
  • Luckily, Internet Explorer provides its own native detection in the form of Conditional Comments.

This means that adding the following in our HTML should suffice:

<!--[if lt IE 9]> <style> input { display: block; } </style>
<![endif]-->

This means that the above will be applied, should the browser be a version of Internet Explorer lower than version 9 (IE 9 supports ::before)—effectively overriding the display: none property.
Seems straightforward enough?

Unfortunately, over time, some critical flaws emerged in User Agent Detection. So much so that Internet Explorer stopped supporting Conditional Comments from version 10 onward. You will also notice that in the Mozilla Developer Network link itself, the following is presented in an orange alert:

It’s worth re-iterating: it’s very rarely a good idea to use user agent sniffing. You can almost always find a better, more broadly compatible way to solve your problem!

The biggest drawback of User Agent Detection is that browser vendors started spoofing their user agent strings over time due to the following:

  • Developer adds CSS feature that is not supported in the browser.
  • Developer adds User Agent Detection code to serve fallbacks to the browser.
  • Browser eventually adds support for that specific CSS feature.
  • Original User Agent Detection code is not updated to take this into consideration.
  • Code always displays the fallback, even if the browser now supports the CSS feature.
  • Browser uses a fake user agent string to give users the best experience on the web.

Furthermore, even if we were able to infallibly determine every browser type and version, we would have to actively maintain and update our User Agent Detection to reflect the feature support state of those browsers (notwithstanding browsers that have not even been developed yet).

It is important to note that although there are superficial similarities between feature detection and User Agent Detection, feature detection takes a radically different approach than User Agent Detection. According to the Mozilla Developer Network, when we use feature detection, we are essentially doing the following:

  1. 🔎 Testing whether a browser is actually able to run a specific line (or lines) of HTML, CSS or JavaScript code.
  2. 💪 Taking a specific action based on the outcome of this test.

We can also look to Wikipedia for a more formal definition (emphasis mine):

Feature detection (also feature testing) is a technique used in web development for handling differences between runtime environments (typically web browsers or user agents), by programmatically testing for clues that the environment may or may not offer certain functionality. This information is then used to make the application adapt in some way to suit the environment: to make use of certain APIs, or tailor for a better user experience.

While a bit esoteric, this definition does highlight two important aspects of feature detection:

  • Feature detection is a technique, as opposed to a specific tool or technology. This means that there are various (equally valid) ways to accomplish feature detection.
  • Feature detection programmatically tests code. This means that browsers actually run a piece of code to see what happens, as opposed to merely using inference or comparing it against a theoretical reference/list as done with User Agent Detection.

CSS feature detection with @supports

The core concept is not to ask “What browser is this?” It’s to ask “Does your browser support the feature I want to use?”.

—Rob Larson, The Uncertain Web: Web Development in a Changing Landscape (2014)

Most modern browsers support a set of native CSS rules called CSS conditional rules. These allow us to test for certain conditions within the stylesheet itself. The latest iteration (known as module level 3) is described by the Cascading Style Sheets Working Group as follows:

This module contains the features of CSS for conditional processing of parts of style sheets, conditioned on capabilities of the processor or the document the style sheet is being applied to. It includes and extends the functionality of CSS level 2 [CSS21], which builds on CSS level 1 [CSS1]. The main extensions compared to level 2 are allowing nesting of certain at-rules inside ‘@media’, and the addition of the ‘@supports’ rule for conditional processing.

If you’ve used @media, @document or @import before, then you already have experience working with CSS conditional rules. For example when using CSS media queries we do the following:

  • Wrap a single or multiple CSS declarations in a code block with curly brackets, { }.
  • Prepend the code block with a @media query with additional information.
  • Include an optional media type. This can either be all, print, speech or the commonly used screen type.
  • Chain expressions with and/or to determine the scope. For example, if we use (min-width: 300px) and (max-width: 800px), it will trigger the query if the screen size is wider than 300 pixels and smaller than 800 pixels.

The feature queries spec (editor’s draft) prescribes behavior that is conveniently similar to the above example. Instead of using a query expression to set a condition based on the screen size, we write an expression to scope our code block according to a browser’s CSS support (emphasis mine):

The ‘@supports rule allows CSS to be conditioned on implementation support for CSS properties and values. This rule makes it much easier for authors to use new CSS features and provide good fallback for implementations that do not support those features. This is particularly important for CSS features that provide new layout mechanisms, and for other cases where a set of related styles needs to be conditioned on property support.

In short, feature queries are a small built-in CSS tool that allow us to only execute code (like the display: none example above) when a browser supports a separate CSS feature—and much like media queries, we are able to chain expressions as follows: @supports (display: grid) and ((animation-name: spin) or (transition: transform(rotate(360deg)).

So, theoretically, we should be able to do the following:

@supports (::before) { input { display: none; }
}

Unfortunately, it seems that in our example above the display: none property did not trigger, in spite of the fact that your browser probably supports ::before.

That’s because there are some caveats to using @supports:

  • First and foremost, CSS feature queries only support CSS properties and not CSS pseudo-element, like ::before.
  • Secondly, you will see that in the above example our @supports (transform: scale(2)) and (animation-name: beat) condition fires correctly. However if we were to test it in Internet Explorer 11 (which supports both transform: scale(2) and animation-name: beat) it does not fire. What gives? In short, @supports is a CSS feature, with a support matrix of its own.

CSS feature detection with Modernizr

Luckily, the fix is fairly easy! It comes in the form of an open source JavaScript library named Modernizr, initially developed by Faruk Ateş (although it now has some pretty big names behind it, like Paul Irish from Chrome and Alex Sexton from Stripe).

Before we dig into Modernizr, let’s address a subject of great confusion for many developers (partly due to the name “Modernizr” itself). Modernizr does not transform your code or magically enable unsupported features. In fact, the only change Modernzr makes to your code is appending specific CSS classes to your <html> tag.

This means that you might end up with something like the following:

<html class="js flexbox flexboxlegacy canvas canvastext webgl no-touch geolocation postmessage websqldatabase indexeddb hashchange history draganddrop websockets rgba hsla multiplebgs backgroundsize borderimage borderradius boxshadow textshadow opacity cssanimations csscolumns cssgradients cssreflections csstransforms csstransforms3d csstransitions fontface generatedcontent video audio localstorage sessionstorage webworkers applicationcache svg inlinesvg smil svgclippaths">

That is one big HTML tag! However, it allows us do something super powerful: use the CSS descendant selector to conditionally apply CSS rules.

When Modernizr runs, it uses JavaScript to detect what the user’s browser supports, and if it does support that feature, Modernizr injects the name of it as a class to the <html>. Alternatively, if the browser does not support the feature, it prefixes the injected class with no- (e.g., no-generatedcontent in our ::before example). This means that we can write our conditional rule in the stylesheet as follows:

.generatedcontent input { display: none
}

In addition, we are able to replicate the chaining of @supports expressions in Modernizr as follows:

/* default */
.generatedcontent input { } /* 'or' operator */
.generatedcontent input, .csstransforms input { } /* 'and' operator */
.generatedcontent.csstransformsinput { } /* 'not' operator */
.no-generatedcontent input { }

Since Modernizr runs in JavaScript (and does not use any native browser APIs), it’s effectively supported on almost all browsers. This means that by leveraging classes like generatedcontent and csstransforms, we are able to cover all our bases for Internet Explorer 8, while still serving bleeding-edge CSS to the latest browsers.

It is important to note that since the release of Modernizr 3.0, we are no longer able to download a stock-standard modernizr.js file with everything except the kitchen sink. Instead, we have to explicitly generate our own custom Modernizr code via their wizard (to copy or download). This is most likely in response to the increasing global focus on web performance over the last couple of years. Checking for more features contributes to more loading, so Modernizr wants us to only check for what we need.

So, I should always use Modernizr?

Given that Modernizr is effectively supported across all browsers, is there any point in even using CSS feature queries? Ironically, I would not only say that we should but that feature queries should still be our first port of call.

First and foremost, the fact that Modernizr does not plug directly into the browser API is it’s greatest strength—it does not rely on the availability of a specific browser API. However, this benefit comes a cost, and that cost is additional overhead to something most browsers support out of the box through @supports—especially when you’re delivering this additional overhead to all users indiscriminately in order to a small amount of edge users. It is important to note that, in our example above, Internet Explorer 8 currently only stands at 0.18% global usage).

Compared to the light touch of @supports, Modernizr has the following drawbacks:

  • The approach underpinning development of Modernizr is driven by the assumption that Modernizr was “meant from day one to eventually become unnecessary.”
  • In the majority of cases, Modernizr needs to be render blocking. This means that Modernizr needs to be downloaded and executed in JavaScript before a web page can even show content on the screen—increasing our page load time (especially on mobile devices)!
  • In order to run tests, Modernizr often has to actually build hidden HTML nodes and test whether it works. For example, in order to test for <canvas> support, Modernizr executes the follow JavaScript code: return !!(document.createElement('canvas').getContext && document.createElement('canvas').getContext('2d'));. This consumes CPU processing power that could be used elsewhere.
  • The CSS descendant selector pattern used by Modernizr increases CSS specificity. (See Harry Roberts’ excellent article on why “specificity is a trait best avoided.”)
  • Although Modernizr covers a lot of tests (150+), it still does not cover the entire spectrum of CSS properties like @support does. The Modernizr team actively maintains a list of these undetectables.

Given that feature queries have already been widely implemented across the browser landscape, (covering about 93.42% of global browsers at the time of writing), it’s been a good while since I’ve used Modernizr. However, it is good to know that it exists as an option should we run into the limitations of @supports or if we need to support users still locked into older browsers or devices for a variety of potential reasons.

Furthermore, when using Modernizr, it is usually in conjunction with @supports as follows:

.generatedcontent input { display: none;
} label:hover::before { color: #c6c8c9;
} input:checked + label::before { color: black;
} @supports (transform: scale(2)) and (animation-name: beat) { input:checked + label::before { color: #e0e1e2; animation-name: beat; animation-iteration-count: infinite; animation-direction: alternate; }
}

This triggers the following to happen:

  • If ::before is not supported, our CSS will fallback to the default HTML radio select.
  • If neither transform(scale(2)) nor animation-name: beat are supported but ::before is, then the heart icon will change to black instead of animate when selected.
  • If transform(scale(2), animation-name: beat and ::before are supported, then the heart icon will animate when selected.

The future of CSS feature detection

Up until this point, I’ve shied away from talking about feature detection in a world being eaten by JavaScript, or possibly even a post-JavaScript world. Perhaps even intentionally so, since current iterations at the intersection between CSS and JavaScript are extremely contentious and divisive.

From that moment on, the web community was split in two by an intense debate between those who see CSS as an untouchable layer in the “separation of concerns” paradigm (content + presentation + behaviour, HTML + CSS + JS) and those who have simply ignored this golden rule and found different ways to style the UI, typically applying CSS styles via JavaScript. This debate has become more and more intense every day, bringing division in a community that used to be immune to this kind of “religion wars”.

—Cristiano Rastelli, Let there be peace on CSS (2017)

However, I think exploring how to apply feature detection in the modern CSS-in-JS toolchain might be of value as follows:

  • It provides an opportunity to explore how CSS feature detection would work in a radically different environment.
  • It showcases feature detection as a technique, as opposed to a specific technology or tool.

With this in mind, let us start by examining an implementation of our pop-up by means of the most widely-used CSS-in-JS library (at least at the time of writing), Styled Components:

This is how it will look in Internet Explorer 8:

In our previous examples, we’ve been able to conditionally execute CSS rules based on the browser support of ::before (via Modernizr) and transform (via @supports). However, by leveraging JavaScript, we are able to take this even further. Since both @supports and Modernizr expose their APIs via JavaScript, we are able to conditionally load entire parts of our pop-up based solely on browser support.

Keep in mind that you will probably need to do a lot of heavy lifting to get React and Styled Components working in a browser that does not even support ::before (checking for display: grid might make more sense in this context), but for the sake of keeping with the above examples, let us assume that we have React and Styled Components running in Internet Explorer 8 or lower.

In the example above, you will notice that we’ve created a component, called ValueSelection. This component returns a clickable button that increments the amount of likes on click. If you are viewing the example on a slightly older browser, you might notice that instead of the button you will see a dropdown with values from 0 to 9.

In order to achieve this, we’re conditionally returning an enhanced version of the component only if the following conditions are met:

if ( CSS.supports('transform: scale(2)') && CSS.supports('animation-name: beat') && Modernizr.generatedcontent
) { return ( <React.Fragment> <Modern type="button" onClick={add}>{string}</Modern> <input type="hidden" name="liked" value={value} /> </React.Fragment> )
} return ( <Base value={value} onChange={select}> { [1,2,3,4,5,6,7,8,9].map(val => ( <option value={val} key={val}>{val}</option> )) } </Base>
);

What is intriguing about this approach is that the ValueSelection component only exposes two parameters:

  • The current amount of likes
  • The function to run when the amount of likes are updated
<Overlay> <Popup> <Title>How much do you like popups?</Title> <form> <ValueInterface value={liked} change={changeLike} /> <Button type="submit">Submit</Button> </form> </Popup>
</Overlay>

In other words, the component’s logic is completely separate from its presentation. The component itself will internally decide what presentation works best given a browser’s support matrix. Having the conditional presentation abstracted away inside the component itself opens the door to exciting new ways of building cross-browser compatible interfaces when working in a front-end and/or design team.

Here’s the final product:

…and how it should theoretically look in Internet Explorer 8:

Additional Resources

If you are interested in diving deeper into the above you can visit the following resources:

  • Mozilla Developer Network article on feature detection
  • Mozilla Developer Network article on user agent detection
  • Mozilla Developer Network article on CSS feature queries
  • Official feature queries documentation by the CSSWG
  • Modernizr Documentation

Schalk is a South African front-end developer/designer passionate about the role technology and the web can play as a force for good in his home country. He works full time with a group of civic tech minded developers at a South African non-profit called OpenUp.

He also helps manage a collaborative space called Codebridge where developers are encouraged to come and experiment with technology as a tool to bridge social divides and solve problems alongside local communities.

The post Using feature detection to write CSS with cross-browser support appeared first on CSS-Tricks.