Update (2016-12-22): I am no longer supporting this library for all the reasons I address in my post The Dark Side of Polyfilling CSS. If you choose to use this library, please make sure you read the post, so you fully understand the challenges and limitations involved in writing CSS polyfills.
Polyfill.js is a library designed to make writing CSS polyfills much, much easier. It's an abstraction library that takes care of the boilerplate, so you can focus on what your polyfill actually does.
For most CSS polyfills, the hardest part is not the polyfill logic itself, it's the boring stuff, the stuff that the browser is supposed to do for you: downloading the CSS, parsing it, and finding the parts you care about. If the CSS contains media queries, you need to deal with them, detect when they apply, and manually listen for changes.
Furthermore, on the Web today, most polyfills exist isolated from each other, which means they all repeat the same expensive tasks. Polyfill.js solves this problem. It provides a common API for Polyfill authors to hook in to, so all the hard work happens only once at most. The stylesheets are downloaded, parsed, and stored in a cache so additional requests don't do double work.
Development — 28KB, with comments
Production — 3.6KB, minified
Full Project — ZIP format
Polyfill.js has been tested in all modern browsers and supports Chrome, Firefox, Safari, Opera, and Internet Explorer 7+.
Polyfill.js makes writing your own CSS Polyfill easy by breaking it down into the following three steps:
It doesn't really matter where you put it, as long as it appears after the stylesheet(s) containing the rules you want to polyfill.
<script src="path/to/polyfill.js"></script>
You create a new instance of the Polyfill object by passing in one or more keywords representing the CSS features you want to polyfill. The keywords can be declaration keywords (property-value pairs) or selector keywords.
The following expression creates an instance to polyfill the :local-link
CSS pseudo-class:
var localLinkPolyfill = Polyfill({ selectors: [":local-link"] })
Once you have your polyfill instance, you simply register two callbacks: doMatched()
and undoUnmatched()
. When the page first loads and Polyfill.js has done all its work behind the scenes, the doMatched()
callback is invoked and is passed a list of CSS rules that contain the specified keywords and match the current media.
If the media values change (usually by resizing the browser window) and new rules match, the doMatched()
callback will be invoked again, each time being passed the newly matched rules.
If the media value changes and some rules no longer match, the undoUnmatched()
callback is invoked and passed a list of rules that previously matched but no longer do.
"Sticky" is a new CSS position value to allow elements to stick in place only after a specified scroll position is met. This is most commonly used for navigation elements to stick in place after you start scrolling down the page.
Local links (:local-link
) is a new CSS pseudo-class for styling anchor tags that point to URLs within the current domain.
The Polyfill.js API consists of three public objects: Polyfill, Ruleset, and Rule. Only Polyfill is accessible in the global scope, the others are passed as arguments to callback functions.
Polyfill(options)
Create a new polyfill object. (Note: the new
operator is optional.)
keywords
: {Object} An object containing the CSS keywords to search the stylesheets for. The keywords object may consist of selector keywords and/or declaration keywords. (Note: if both are present a rule will be considered a match if it contains either selector keywords or declaration keywords.)
selectors
: {Array} a list of strings to match against CSS selectors.declarations
: {Array} a list of strings to match against CSS declarations (property-value pairs). The format of the string must be prop:value
. The string may optionally include an asterisk which will match any number of characters, e.g. display:*flex
will match vendor prefixes. (Note: whitespace on either side of the colon is not significant.)exclude
: {Array} a list of link element ID attributes. If the exclude option is present, all stylesheets but the ones with those IDs will be downloaded. include
: {Array} a list of link element ID attributes. If the include option is present, only those stylesheets will be downloaded. (Note: exclude
and include
cannot be used together)returns {Polyfill} the newly created instance
// create a polyfill for some CSS pseudo-classes
var p1 = Polyfill({
exclude: ["third-party-css"],
keywords: {
selectors: [":local-link", ":nth-of-type"],
declarations: ["*border-radius:*", "position:sticky", "display:*flex"]
}
})
// create a polyfill for the flex display property
var p2 = Polyfill({
include: ["flexbox", "box"],
keywords: {
declarations: ["display:*flex"]
}
})
// create a polyfill by only passing keywords
var p3 = Polyfill({
declarations: ["filter:*"]
})
.doMatched(callback)
Register a callback to be invoked whenever new rules match the passed keywords. This happens as soon as the page is loaded as well as when media changes. The callback is invoked with a Ruleset object that contains all of the newly matched CSS rules.
callback {Function}: the function called
returns {Polyfill} the current Polyfill instance
polyfill.doMatched(function(rules) {
// do somthing...
})
.undoUnmatched(callback)
Register a callback function to be invoked whenever previously matched rules no longer match. This could be because the media changed or the polyfill was destroyed.
callback {Function}: A callback function. Each invocation is passed a Ruleset object which contains all of the newly matched CSS rules.
returns {Polyfill} the current Polyfill instance
polyfill.undoUnmatched(function(rules) {
// do somthing...
})
.getMatches()
Fetch all the CSS rules that match the current media. (Note: rules that are not in a media block always match.)
returns {Ruleset} a Ruleset object containing all of the currently matched rules.
var matches = polyfill.getMatches()
.destroy()
Destroy the polyfill instance by removing any media listeners and invoking the undoMatched
callback.
returns {undefined}
polyfill.destroy()
.each(callback)
Iterates over a Ruleset invoking a callback for each Rule object in the Ruleset. Callbacks are invoked with the Rule object as their only arguments.
callback {Function}: the function called per iteration
returns {Ruleset} the current instance
rules.each(function(rule) {
// do something...
})
.at(index)
Returns the Rule instance at the specified index.
index (Number): the index of the rule to return
returns {Rule} the Rule object at the specified index.
var rule = rules.at(0)
.getSelectors()
Returns the full selector as a string. If the rule contains more than one selector they are joined with a comma.
returns {String} the full selector
var selector = rule.getSelectors()
.getDeclaration()
Returns an object map of the CSS declaration. (Note: since an object cannot have duplicate keys, duplicate CSS property values ignored. If you need to access duplicate CSS values, you can manually inspect the Rule instance for the raw data.)
returns {Object} the rule's declaration
var declaration = rule.getDeclaration()
.getMedia()
Returns a string of the media query value. If the rule contains more than one media query value (e.g. a nested rule) the media values are joined on and
.
returns {String} the full media query
var media = rule.getMedia()
Putting it all together, this is all the code you'd need to write a functioning polyfill for the new CSS property :local-link
, which allows you to style links based on whether their href
attribute points to a URL on the same domain (ok, there's a little more to it than that, but you get the idea).
Note: This example uses jQuery:
// First write a RegExp to match URL parts
var reURL = /^(?:(https?:)\/\/)?((?:[0-9a-z\.\-]+)(?::(?:\d+))?)/
// Then extend jQuery's selector engine to target local links
$.extend($.expr[':'], {
"local-link": function(el) {
var url = reURL.exec(el.href)
, protocol = url[1]
, host = url[2]
return protocol == location.protocol && host == location.host
}
})
// Create the polyfill and register the callbacks, that's it!
Polyfill({ selectors: [":local-link"] })
.doMatched(function(rules) {
rules.each(function(rule) {
$(rule.getSelectors()).css(rule.getDeclaration())
})
})
.undoUnmatched(function(rules) {
rules.each(function(rule) {
$(rule.getSelectors()).removeAttr("style")
})
localLinkPolyfill.getCurrentMatches().each(function(rule) {
$(rule.getSelectors()).css(rule.getDeclaration())
})
})