A Quick Look at Constructable Stylesheets
Banso D. Wisdom
Posted on April 5, 2019
"Constructable Stylesheets". This might be the first time you're hearing of this and you must be thinking "what the flux is that?", and that's fine, that was my reaction when I first heard of it too.
What are Constructable Stylesheets?
Simply put, Constructable Stylesheets are a way to create and distribute reusable styles when working with the Shadow DOM.
What is the Shadow DOM?
To understand how Constructable Stylesheets work, we need to understand what the Shadow DOM is and to do that we need to understand what the DOM is.
The DOM which stands for Document Object Model is a representation of an HTML document, it is used in Javascript to modify a page's content and is also utilized by browsers to figure out what to render on a page.
The Shadow DOM is a DOM within "The DOM". It is a completely separate DOM tree from "The DOM" with its own elements and styling. It was created for the purpose of encapsulation and most applications of the Shadow DOM revolve around creating complex components/elements in such a way that the styling of these components/elements is unaffected by other style rules in "The DOM".
A good example of this is Ionic 4 UI components.
To better understand how the DOM and Shadow DOM works, here is an article What is the Shadow DOM by Ire Aderinokun.
Why Constructable Stylesheets?
"Why do we need a new way of creating stylesheets?" you might ask. I asked the same question too. As we all might know, we have always created/been able to create stylesheets on the fly using Javascript like this:
const style = document.createElement('style');
and obtain a reference to the underlying CssStyleSheet instance by accessing the sheet property.
This method works quite alright, but it has a few downsides, some of which are:
- It can result in duplicate CSS code and thus cause CSS bloat.
What is CSS Bloat?
CSS bloat is unnecessarily repeated CSS code and while it directly doesn't affect your performance it indirectly affects your performance as having redundant selectors and rules increases your bundle size and makes your page heavier to load and slow to render.
- It can lead to FOUC.
What is FOUC?
FOUC - Flash of Unstyled Content is a scenario where the content web page loads un-styled briefly then shortly after appears styled. This occurs when the browser renders the page before completely loading all the required assets.
FOUC can be caused by having duplicate CSS code (CSS bloat) which in turn causes a larger and heavier bundle which is slow to render.
The aforementioned problems are easily solved by using Constructable Stylesheets.
How to use Constructable Stylesheets
Creating a Stylesheet
To create a stylesheet according to the Constructable Stylesheets specification, we do by invoking the CSSStyleSheet() constructor.
const sheet = new CSSStyleSheet();
The resulting object, in this case, sheet has two methods that we can use to add and update stylesheet rules without the risk of FOUC. These methods both take a single argument which is a string of style rules.
These methods are:
- replace(): This method allows the use of external references i.e. @import in addition to CSS rules and it returns a promise that resolves once any imports are loaded.
sheet.replace('@import url("app.css"); p { color: #a1a1a1 }').then(sheet => {
console.log('Imports added and styles added');
}).catch(error => {
console.error('Error adding styles: ', error)
});
- replaceSync(): This method doesn't allow @import, only CSS Rules.
// this works
sheet.replaceSync('p { color: #a1a1a1 }');
// this throws an exception
try {
sheet.replaceSync('@import url("app.css"); p { color: #a1a1a1 }');
} catch(error) => {
console.error(error);
}
Using a Constructed Stylesheet
After creating a stylesheet, we'd like to use it, of course. We use created stylesheets by using the adoptedStyleSheets property which Documents and Shadow DOMs possess.
This property lets us explicitly apply the styles we have defined in our Constructed Stylesheet to a DOM subtree by setting the value of this adoptedStyleSheets property to an array of stylesheets.
// applying the earlier created stylesheet to a document
document.adoptedStyleSheets = [sheet];
// creating an element and applying stylesheet to its shadow root
const el = document.createElement('div');
const shadowRoot = el.attachShadow({ mode: open });
shadowRoot.adoptedStyleSheets = [sheet];
We can also create new stylesheets and add them to the adoptedStyleSheets property.
Now normally, since the property is an array, using mutations like push() would be the way to go. However, in this case, it's not so.
This is because the adoptedStyleSheets property array is frozen and therefore in-place mutations like push() won't work.
When is an array said to be frozen?
A frozen array is an array which has been frozen as an object via the Object.freeze() method. The Object.freeze() method "freezes" an object which prevents new properties from being added to it, prevents the values of existing properties from being changed and also prevents the object's prototype from being changed.
What is an in-place mutation?
The term "in-place" is used to describe an algorithm that transforms the input given to it without using any additional data structure. While an algorithm that utilizes an additional data structure in transforming input is said to be out-of-place/not-in-place.
Consider the following methods, both to reverse the order of an array:
P.S: This is just for explanation purposes.
const reverseOutOfPlace = (input) => {
const output = [];
input.forEach((element, index) => {
output[index] = input[input.length - (index + 1)];
})
return output;
}
const reverseInPlace = (input) => {
const len = input.length;
for(let i = 0; i <= Math.floor((len-2)/2); i++) {
const temp = input[i];
input[i] = input[len - 1 - i];
input[len - 1 - i] = temp;
}
return input;
}
They both reverse the order of a given array, however, the reverseOutOfPlace method uses an additional array output to reverse the input while the reverseInPlace method doesn't use any additional arrays, as such the former is out-of-place while the latter is in-place.
Array [mutation] methods like pop and push are in-place because they don't use any additional arrays while others like concat and map are out-of-place because they use additional arrays in transforming the input array.
Since the adoptedStyleSheets property array is frozen and the values of its properties cannot be changed, the best way to add new stylesheets to the array is using concat() or the spread operator
const newSheet = new CSSStyleSheet();
newSheet.replaceSync('p { color: #eaeaea }');
// using concat
shadowRoot.adoptedStyleSheets = shadowRoot.adoptedStyleSheets.concat(newSheet);
// using the spread operator
shadowRoot.adoptedStyleSheets = [...shadowRoot.adoptedStyleSheets, newSheet]
What can I do with Constructable Stylesheets
Constructable Stylesheets has wide possibilities of usages, below are some of them:
- Create shared CSS styles on the fly and apply them to the document or multiple Shadow roots without CSS bloat.
When a shared CSSStyleSheet has been applied to elements, any updates to it reflect on all the elements that it has been applied to. This can be used to implement hot replacement of styles within Shadow DOMs.
Change CSS custom properties on the fly for specific DOM subtrees.
Create a central theme that is used by/applied to several components.
As a direct interface to the browser's parser to preload stylesheets.
Here is a pen I made that shows most of what is in this post.
For more information on Constructable StyleSheets, check out these posts: Constructable Stylesheets: Seamless reusable styles and Constructable Stylesheet Objects.
P.S: At the time of writing this article, Constructable StyleSheets has only shipped to Chrome, so the aforementioned pen will work on only chromium-based browsers.
Posted on April 5, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.