How to Load Fonts in a Way That Fights FOUT and Makes Lighthouse Happy

A web font workflow is simple, right? Choose a few nice-looking web-ready fonts, get the HTML or CSS code snippet, plop it in the project, and check if they display properly. People do this with Google Fonts a zillion times a day, dropping its <link> tag into the <head>.

Let’s see what Lighthouse has to say about this workflow.

Stylesheets in the <head> have been flagged by Lighthouse as render-blocking resources and they add a one-second delay to render? Not great.

We’ve done everything by the book, documentation, and HTML standards, so why is Lighthouse telling us everything is wrong?

Let’s talk about eliminating font stylesheets as a render-blocking resource, and walk through an optimal setup that not only makes Lighthouse happy, but also overcomes the dreaded flash of unstyled text (FOUT) that usually comes with loading fonts. We’ll do all that with vanilla HTML, CSS, and JavaScript, so it can be applied to any tech stack. As a bonus, we’ll also look at a Gatsby implementation as well as a plugin that I’ve developed as a simple drop-in solution for it.

What we mean by “render-blocking” fonts

When the browser loads a website, it creates a render tree from the DOM, i.e. an object model for HTML, and CSSOM, i.e. a map of all CSS selectors. A render tree is a part of a critical render path that represents the steps that the browser goes through to render a page. For browser to render a page, it needs to load and parse the HTML document and every CSS file that is linked in that HTML.

Here’s a fairly typical font stylesheet pulled directly from Google Fonts:

@font-face {
  font-family: 'Merriweather';
  src: local('Merriweather'), url(https://fonts.gstatic.com/...) format('woff2');
}

You might be thinking that font stylesheets are tiny in terms of file size because they usually contain, at most, a few @font-face definitions. They shouldn’t have any noticeable effect on rendering, right?

Let’s say we’re loading a CSS font file from an external CDN. When our website loads, the browser needs to wait for that file to load from the CDN and be included in the render tree. Not only that, but it also needs to wait for the font file that is referenced as a URL value in the CSS @font-face definition to be requested and loaded.

Bottom line: The font file becomes a part of the critical render path and it increases the page render delay.

Critical render path delay when loading font stylesheet and font file 
(Credit: web.dev under Creative Commons Attribution 4.0 License)

What is the most vital part of any website to the average user? It’s the content, of course. That is why content needs to be displayed to the user as soon as possible in a website loading process. To achieve that, the critical render path needs to be reduced to critical resources (e.g. HTML and critical CSS), with everything else loaded after the page has been rendered, fonts included.

If a user is browsing an unoptimized website on a slow, unreliable connection, they will get annoyed sitting on a blank screen that’s waiting for font files and other critical resources to finish loading. The result? Unless that user is super patient, chances are they’ll just give up and close the window, thinking that the page is not loading at all.

However, if non-critical resources are deferred and the content is displayed as soon as possible, the user will be able to browse the website and ignore any missing presentational styles (like fonts) — that is, if they don’t get in the way of the content.

Optimized websites render content with critical CSS as soon as possible with non-critical resources deferred. A font switch occurs between 0.5s and 1.0s on the second timeline, indicating the time when presentational styles start rendering.

The optimal way to load fonts

There’s no point in reinventing the wheel here. Harry Roberts has already done a great job describing an optimal way to load web fonts. He goes into great detail with thorough research and data from Google Fonts, boiling it all down into a four-step process:

  • Preconnect to the font file origin.
  • Preload the font stylesheet asynchronously with low priority.
  • Asynchronously load the font stylesheet and font file after the content has been rendered with JavaScript.
  • Provide a fallback font for users with JavaScript turned off.

Let’s implement our font using Harry’s approach:

<!-- https://fonts.gstatic.com is the font file origin -->
<!-- It may not have the same origin as the CSS file (https://fonts.googleapis.com) -->
<link rel="preconnect"
      href="https://fonts.gstatic.com"
      crossorigin />


<!-- We use the full link to the CSS file in the rest of the tags -->
<link rel="preload"
      as="style"
      href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />


<link rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap"
      media="print" onload="this.media='all'" />


<noscript>
  <link rel="stylesheet"
        href="https://fonts.googleapis.com/css2?family=Merriweather&display=swap" />
</noscript>

Notice the media="print" on the font stylesheet link. Browsers automatically give print stylesheets a low priority and exclude them as a part of the critical render path. After the print stylesheet has been loaded, an onload event is fired, the media is switched to a default all value, and the font is applied to all media types (screen, print, and speech).

Lighthouse is happy with this approach!

It’s important to note that self-hosting the fonts might also help fix render-blocking issues, but that is not always an option. Using a CDN, for example, might be unavoidable. In some cases, it’s beneficial to let a CDN do the heavy lifting when it comes to serving static resources.

Even though we’re now loading the font stylesheet and font files in the optimal non-render-blocking way, we’ve introduced a minor UX issue…

Flash of unstyled text (FOUT)

This is what we call FOUT:

Why does that happen? To eliminate a render-blocking resource, we have to load it after the page content has rendered (i.e. displayed on the screen). In the case of a low-priority font stylesheet that is loaded asynchronously after critical resources, the user can see the moment the font changes from the fallback font to the downloaded font. Not only that, the page layout might shift, resulting in some elements looking broken until the web font loads.

The best way to deal with FOUT is to make the transition between the fallback font and web font smooth. To achieve that we need to:

  • Choose a suitable fallback system font that matches the asynchronously loaded font as closely as possible.
  • Adjust the font styles (font-size, line-height, letter-spacing, etc.) of the fallback font to match the characteristics of the asynchronously loaded font, again, as closely as possible.
  • Clear the styles for the fallback font once the asynchronously loaded font file has has rendered, and apply the styles intended for the newly loaded font.

We can use Font Style Matcher to find optimal fallback system fonts and configure them for any given web font we plan to use. Once we have styles for both the fallback font and web font ready, we can move on to the next step.

Merriweather is the font and Georgia is the fallback system font in this example. Once the Merriweather styles are applied, there should be minimal layout shifting and the switch between fonts should be less noticeable.

We can use the CSS font loading API to detect when our web font has loaded. Why that? Typekit’s web font loader was once one of the more popular ways to do it and, while it’s tempting to continue using it or similar libraries, we need to consider the following:

  • It hasn’t been updated for over four years, meaning that if anything breaks on the plugin side or new features are required, it’s likely no one will implement and maintain them.
  • We are already handling async loading efficiently using Harry Roberts’ snippet and we don’t need to rely on JavaScript to load the font.

If you ask me, using a Typekit-like library is just too much JavaScript for a simple task like this. I want to avoid using any third-party libraries and dependencies, so let’s implement the solution ourselves and try to make it is as simple and straightforward as possible, without over-engineering it.

Although the CSS Font Loading API is considered experimental technology, it has roughly 95% browser support. But regardless, we should provide a fallback if the API changes or is deprecated in the future. The risk of losing a font isn’t worth the trouble.

The CSS Font Loading API can be used to load fonts dynamically and asynchronously. We’ve already decided not to rely on JavaScript for something simple as font loading and we’ve solved it in an optimal way using plain HTML with preload and preconnect. We will use a single function from the API that will help us check if the font is loaded and available.

document.fonts.check("12px 'Merriweather'");

The check() function returns true or false depending on whether the font specified in the function argument is available or not. The font size parameter value is not important for our use case and it can be set to any value. Still, we need to make sure that:

  • We have at least one HTML element on a page that contains at least one character with web font declaration applied to it. In the examples, we will use the &nbsp; but any character can do the job as long it’s hidden (without using display: none;) from both sighted and non-sighted users. The API tracks DOM elements that have font styles applied to them. If there are no matching elements on a page, then the API isn’t be able to determine if the font has loaded or not.
  • The specified font in the check() function argument is exactly what the font is called in the CSS.

I’ve implemented the font loading listener using CSS font loading API in the following demo. For example purposes, loading fonts and the listener for it are initiated by clicking the button to simulate a page load so you can see the change occur. On regular projects, this should happen soon after the website has loaded and rendered.

Isn’t that awesome? It took us less than 30 lines of JavaScript to implement a simple font loading listener, thanks to a well-supported function from the CSS Font Loading API. We’ve also handled two possible edge cases in the process:

  • Something goes wrong with the API, or some error occurs preventing the web font from loading.
  • The user is browsing the website with JavaScript turned off.

Now that we have a way to detect when the font file has finished loading, we need to add styles to our fallback font to match the web font and see how to handle FOUT more effectively.

The transition between the fallback font and web font looks smooth and we’ve managed to achieve a much less noticeable FOUT! On a complex site, this change would result in a fewer layout shifts, and elements that depend on the content size wouldn’t look broken or out of place.

What’s happening under the hood

Let’s take a closer look at the code from the previous example, starting with the HTML. We have the snippet in the <head> element, allowing us to load the font asynchronously with preload, preconnect, and fallback.

<body class="no-js">
  <!-- ... Website content ... -->
  <div aria-visibility="hidden" class="hidden" style="font-family: '[web-font-name]'">
      /* There is a non-breaking space here here */
  </div>
  <script> 
    document.getElementsByTagName("body")[0].classList.remove("no-js");
  </script>
</body>

Notice that we have a hardcoded .no-js class on the <body> element, which is removed the moment the HTML document has finished loading. This applies webfont styles for users with JavaScript disabled.

Secondly, remember how the CSS Font Loading API requires at least one HTML element with a single character to track the font and apply its styles? We added a <div> with a &nbsp; character that we are hiding from both sighted and non-sighted users in an accessible way, since we cannot use display: none;. This element has an inlined font-family: 'Merriweather' style. This allows us to smoothly switch between the fallback styles and loaded font styles, and make sure that all font files are properly tracked, regardless of whether they are used on the page or not.

Note that the &nbsp; character is not showing up in the code snippet but it is there!

The CSS is the most straightforward part. We can utilize the CSS classes that are hardcoded in the HTML or applied conditionally with JavaScript to handle various font loading states.

body:not(.wf-merriweather--loaded):not(.no-js) {
  font-family: [fallback-system-font];
  /* Fallback font styles */
}


.wf-merriweather--loaded,
.no-js {
  font-family: "[web-font-name]";
  /* Webfont styles */
}


/* Accessible hiding */
.hidden {
  position: absolute; 
  overflow: hidden; 
  clip: rect(0 0 0 0); 
  height: 1px;
  width: 1px; 
  margin: -1px;
  padding: 0;
  border: 0; 
}

JavaScript is where the magic happens. As described previously, we are checking if the font has been loaded by using the CSS Font Loading API’s check() function. Again, the font size parameter can be any value (in pixels); it’s the font family value that needs to match the name of the font that we’re loading.

var interval = null;


function fontLoadListener() {
  var hasLoaded = false;


  try {
    hasLoaded = document.fonts.check('12px "[web-font-name]"')
  } catch(error) {
    console.info("CSS font loading API error", error);
    fontLoadedSuccess();
    return;
  }
  
  if(hasLoaded) {
    fontLoadedSuccess();
  }
}


function fontLoadedSuccess() {
  if(interval) {
    clearInterval(interval);
  }
  /* Apply class names */
}


interval = setInterval(fontLoadListener, 500);

What’s happening here is we’re setting up our listener with fontLoadListener() that runs at regular intervals. This function should be as simple as possible so it runs efficiently within the interval. We are using the try-catch block to handle any errors and catch any issues so that web font styles still apply in the case of a JavaScript error so that the user doesn’t experience any UI issues.

Next, we’re accounting for when the font successfully loads with fontLoadedSuccess(). We need to make sure to first clear the interval so the check doesn’t unnecessarily run after it.  Here we can add class names that we need in order to apply the web font styles.

And, finally, we are initiating the interval. In this example, we’ve set it up to 500ms, so the function runs twice per second.

Here’s a Gatsby implementation

Gatsby does a few things that are different compared to vanilla web development (and even the regular create-react-app tech stack) which makes implementing what we’ve covered here a bit tricky.

To make this easy, we’ll develop a local Gatsby plugin, so all code that is relevant to our font loader is located at plugins/gatsby-font-loader in the example below.

Our font loader code and config will be split across the three main Gatsby files:

  • Plugin configuration (gatsby-config.js): We’ll include the local plugin in our project, list all local and external fonts and their properties (including the font name, and the CSS file URL), and include all preconnect URLs.
  • Server-side code (gatsby-ssr.js): We’ll use the config to generate and include preload and preconnect tags in the HTML <head> using setHeadComponents function from Gatsby’s API. Then, we’ll generate the HTML snippets that hide the font and include them in HTML using setPostBodyComponents.
  • Client-side code (gatsby-browser.js): Since this code runs after the page has loaded and after React starts up, it is already asynchronous. That means we can inject the font stylesheet links using react-helmet. We’ll also start a font loading listener to deal with FOUT.

You can check out the Gatsby implementation in the following CodeSandbox example.

I know, some of this stuff is complex. If you just want a simple drop-in solution for performant, asynchronous font loading and FOUT busting, I’ve developed a gatsby-omni-font-loader plugin just for that. It uses the code from this article and I am actively maintaining it. If you have any suggestions, bug reports, or code contributions, feel free to submit them on on GitHub.

Conclusion

Content is perhaps the most component to a user’s experience on a website. We need to make sure content gets top priority and loads as quickly as possible. That means using bare minimum presentation styles (i.e. inlined critical CSS) in the loading process. That is also why web fonts are considered non-critical in most cases — the user can still consume the content without them — so it’s perfectly fine for them to load after the page has rendered.

But that might lead to FOUT and layout shifts, so the font loading listener is needed to make a smooth switch between the fallback system font and the web font.

I’d like to hear your thoughts! Let me know in the comments how are you tackling the issue of web font loading, render-blocking resources and FOUT on your projects.


References


The post How to Load Fonts in a Way That Fights FOUT and Makes Lighthouse Happy appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Here’s How I Solved a Weird Bug Using Tried and True Debugging Strategies

Remember the last time you dealt with a UI-related bug that left you scratching your head for hours? Maybe the issue was happening at random, or occurring under specific circumstances (device, OS, browser, user action), or was just hidden in one of the many front-end technologies that are part of the project?

I was recently reminded of how convoluted UI bugs can be. I recently fixed an interesting bug that was affecting some SVGs in Safari browsers with no obvious pattern or reason. I had searched for any similar issues to get some clue about what was going on, but I found no useful results. Despite the obstacles, I managed to fix it.

I analyzed the problem by using some useful debugging strategies that I’ll also cover in the article. After I submitted the fix, I was reminded of the advice Chris has tweeted out a while back.

…and here we are.

Here’s the problem

I found the following bug on a project I’ve been working on. This was on a live site.

I reproduced this issue with any paint event, for example, resizing the screen.

I created a CodePen example to demonstrate the issue so you can check it out for yourself. If we open the example in Safari, the buttons might look as expected on load. But if we click on the first two larger buttons, the issue rears its ugly head. 

Whenever a browser paint event happens, the SVG in the larger buttons render incorrectly. It simply gets cut off. It might happen randomly on load. It might even happen when the screen is resized. Whatever the situation, it happens!

Ugh, why is the SVG icon getting cut off?!

Here’s how I approached the problem.

First off, let’s consider the environment

It’s always a good idea to go over the details of the project to understand the environment and conditions under which the bug is present.

  • This particular project is using React (but is not required for following this article).
  • The SVGs are imported as React components and are inlined in HTML by webpack.
  • The SVGs have been exported from a design tool and have no syntax errors.
  • The SVGs have some CSS applied to them from a stylesheet.
  • The affected SVGs are positioned inside a <button> HTML element.
  • The issue occurs only in Safari (and was noticed on version 13).

Down the rabbit hole

Let’s take a look at the issue and see if we can make some assumptions about what is going on. Bugs like this one get convoluted, and we won’t immediately know what is going on. We don’t have to be 100% correct in our first try because we’ll go step-by-step and form hypotheses that we can test to narrow down the possible causes. 

Forming a hypothesis

At first, this looks like a CSS issue. Some styles might be applied on a hover event that breaks the layout or the overflow property of the SVG graphic. It also looks like the issue is happening at random, whenever Safari renders the page (paint event when resizing the screen, hover, click, etc.).

Let’s start with the simple and most obvious route and assume that CSS is the cause of the issue. We can consider the possibility that there is a bug in the Safari browser that causes SVG to render incorrectly when some specific style applies to the SVG element, like a flex layout, for example.

By doing so, we’ve formed a hypothesis. Our next step is to set up a test that is either going to confirm or contradict the hypothesis. Each test result will produce new facts about the bug and help form further hypotheses. 

Problem simplification

We’ll use a debugging strategy called problem simplification to try and pinpoint the issue. Cornell University’s CS lecture describes this strategy as “an approach to gradually eliminate portions of the code that are not relevant to the bug.”

By assuming the issue lies within CSS, we can end up pinpointing the issue or eliminating the CSS from the equation, reducing the number of possible causes and the complexity of the problem. 

Let’s try and confirm our hypothesis. If we temporarily exclude all non-browser stylesheets, the issue should not occur. I did that in my source code by commenting out the following line of code in my project.

import 'css/app.css';

I have created a handy CodePen example to demonstrate these elements without CSS included. In React, we are importing SVG graphics as components, and they are inlined in HTML using webpack.

If we open this pen on Safari and click on the button, we are still getting the issue. It still happens when the page loads, but on CodePen we have to force it by clicking the button. We can conclude that the CSS isn’t the culprit, but we can also see that only the two out of five buttons break under this condition. Let’s keep this in mind and move on to the next hypothesis.

The same SVG elements still break with excluded stylesheets (Safari 13)

Isolating the issue

Our next hypothesis states that Safari has a bug when rendering SVG inside an HTML <button> element. Since the issue has occurred on the first two buttons, we’ll isolate the first button and see what happens.

Sarah Drasner explains the importance of isolation and I highly recommend reading her article if you want to learn more about debugging tools and other approaches.

Isolation is possibly the strongest core tenets in all of debugging. Our codebases can be sprawling, with different libraries, frameworks, and they can include many contributors, even people who aren’t working on the project anymore. Isolating the problem helps us slowly whittle away non-essential parts of the problem so that we can singularly focus on a solution.

A “reduced test case” it is also often called. 

I moved this button to a separate and empty test route (blank page). I created the following CodePen to demonstrate that state. Even though we’ve concluded that the CSS is not the cause of the issue, we should keep it excluded until we’ve found out the real cause of the bug, to keep the problem simple as possible.

If we open this pen in Safari, we can see that we can no longer reproduce the issue and the SVG graphic displays as expected after clicking the button. We shouldn’t consider this change as an acceptable bug fix, but it gives a good starting point in creating a minimal reproducible example.

A minimal reproducible example

The main difference between the previous two examples is the button combination. After trying out all possible combinations, we can conclude that this issue occurs only when a paint event occurs on a larger SVG graphic that is alongside a smaller SVG graphic on the same page.

We created a minimal reproducible example that allows us to reproduce the bug without any unnecessary elements. With minimal reproducible example, we can study the issue in more detail and accurately pinpoint the part of the code causing it.

I’ve created the following CodePen to demonstrate the minimal reproducible example.

If we open this demo in Safari and click on a button, we can see the issue in action and form a hypothesis that these two SVGs somehow conflict with one another. If we overlay the second SVG graphic over the first, we can see that the size of the cropped circle on the first SVG graphic matches the exact dimensions of the smaller SVG graphic.

Edited image that compares the smaller SVG graphic to the first SVG graphic with the bug present

Divide and conquer

We’ve narrowed down the issue to the combination of two SVG graphics. Now we’re going to narrow things down to the specific SVG code that’s messing things up. If we only have a basic understanding of SVG code and want to pinpoint the issue, we can use a binary tree search strategy with a divide-and-conquer approach. Cornell University’s CS lecture describes this approach:

For example, starting from a large piece of code, place a check halfway through the code. If the error doesn’t show up at that point, it means the bug occurs in the second half; otherwise, it is in the first half. 

In SVG, we can try deleting <filter> (and also <defs> since it’s empty anyway) from the first SVG. Let’s first check what <filter> does. This article by Sara Soueidan explains it best.

Just like linear gradients, masks, patterns, and other graphical effects in SVG, filters have a conveniently-named dedicated element: the <filter> element.

A <filter> element is never rendered directly; its only usage is as something that can be referenced using the filter attribute in SVG, or the url() function in CSS.

In our SVG, <filter> applies a slight inset shadow at the bottom of the SVG graphic. After we delete it from the first SVG graphic, we expect the inner shadow to be gone. If the issue persists, we can conclude that something is wrong with the rest of the SVG markup.

I’ve created the following CodePen to showcase this test.

As we can see, the issue persists anyway. The inset bottom shadow is displayed even though we’ve removed the code. Not only that, now the bug appears on every browser. We can conclude that the issue lies within the rest of the SVG code. If we delete the remaining id from <g filter="url(#filter0_ii)">, the shadow is fully removed. What is going on?

Let’s take another look at the previously mentioned definition of the <filter> property and notice the following detail:

A <filter> element is never rendered directly; its only usage is as something that can be referenced using the filter attribute in SVG.

(Emphasis mine)

So we can conclude that the filter definition from the second SVG graphic is being applied to the first SVG graphic and causing the error.

Fixing the issue

We now know that issue is related to the <filter> property. We also know that both SVGs have the filter property since they use it for the inset shadow on the circle shape. Let’s compare the code between the two SVGs and see if we can explain and fix the issue.

I’ve simplified the code for both SVG graphics so we can clearly see what is going on. The following snippet shows the code for the first SVG.

<svg width="46" height="46" viewBox="0 0 46 46">
  <g filter="url(#filter0_ii)">
    <!-- ... -->
  </g>
  <!-- ... -->
  <defs>
    <filter id="filter0_ii" x="0" y="0" width="46" height="46">
      <!-- ... -->
    </filter>
  </defs>
</svg>

And the following snippet shows the code for the second SVG graphic.

<svg width="28" height="28" viewBox="0 0 28 28">
  <g filter="url(#filter0_ii)">
    <!-- ... -->
  </g>
  <!-- ... -->
  <defs>
    <filter id="filter0_ii" x="0" y="0" width="28" height="28">
      <!-- ... -->
    </filter>
  </defs>
</svg>

We can notice that the generated SVGs use the same id property id=filter0_ii. Safari applied the filter definition it read last (which, in our case, is the second SVG markup) and caused the first SVG to become cropped to the size of the second filter (from 46px to 28px). The id property should have a unique value in DOM. By having two or more id properties on a page, browsers cannot understand which reference to apply, and the filter property redefines on each paint event, dependent on the racing condition that causes the issue to appear randomly.

Let’s try assigning unique id attribute values to each SVG graphic and see if that fixes the issue.

If we open the CodePen example in Safari and click the button, we can see that we fixed the issue by assigning a unique ID to <filter> property in each SVG graphic file. If we think about the fact that we have non-unique value for an attribute like id, it means that this issue should be present on all browsers. For some reason, other browsers (including Chrome and Firefox) seem to handle this edge-case without any bugs, although this might be just a coincidence.

Wrapping up

That was quite a ride! We started barely knowing anything about an issue that seemingly occurred at random, to fully understanding and fixing it. Debugging UI and understanding visual bugs can be difficult if the cause of the issue is unclear or convoluted. Luckily, some useful debugging strategies can help us out.

First, we simplified the problem by forming hypotheses which helped us eliminate the components that were unrelated to the issue (style, markup, dynamic events, etc.). After that, we isolated the markup and found the minimal reproducible example which allowed us to focus on a single chunk of code. Finally, we pinpointed the issue with a divide-and-conquer strategy, and fixed it.

Thank you for taking the time to read this article. Before I go, I’d like to leave you with one final debugging strategy that is also featured in Cornell University’s CS lecture

Remember to take a break, relax and clear your mind between debugging attempts.

 If too much time is spent on a bug, the programmer becomes tired and debugging may become counterproductive. Take a break, clear your mind; after some rest, try to think about the problem from a different perspective.

References


The post Here’s How I Solved a Weird Bug Using Tried and True Debugging Strategies appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Neumorphism and CSS

Neumorphism (aka neomorphism) is a relatively new design trend and a term that’s gotten a good amount of buzz lately. It’s aesthetic is marked by minimal and real-looking UI that’s sort of a new take on skeuomorphism — hence the name. It got its name in a UX Collective post from December 2019, and since then, various design and development communities have been actively discussing the trend, usually with differing opinions. Chris poked fun at it on Twitter. Adam Giebl created an online generator for it. Developers, designers, and UX specialists are weighing in on the topic of aesthetics, usability, accessibility, and practicality of this design trend.

Clearly, it’s stricken some sort of chord in the community.

Let’s dip our toes into the neumorphism pool, showcasing the various neumorphic effects that can be created using our language of choice, CSS. We’ll take a look at both the arguments for and against the style and weigh how it can be used in a web interface.

Neumorphism as a user interface

We’ve already established that the defining quality of neumorphism is a blend of minimalism and skeuomorphism. And that’s a good way to look at it. Think about the minimal aesthetic of Material Design and the hyper-realistic look of skeuomorphism. Or, think back to Apple’s design standards circa 2007-12 and compare it to the interfaces it produces today.

Nine years of Apple Calendar! The image on the left is taken from 2011 and exhibits the look and feel of a real, leather-bound journal, said to be inspired by one on Steve Jobs’ personal yacht. The right is the same app as shown today in 2020, bearing a lot less physical inspiration with a look and feel we might describe as “flat” or minimal.

If we think about Apple’s skeuomorphic designs from earlier in the century as one extreme and today’s minimal UI as another, then we might consider neumorphism as something in the middle.

Alexander Plyuto has championed and evolved neomorphic designs on his Dribbble account. (Source)

Neumorphic UI elements look like they’re connected to the background, as if the elements are extruded from the background or inset into the background. They’ve been described by some as “soft UI” because of the way soft shadows are used to create the effect.

Another way to understand neumorphic UI is to compare it to Material Design. Let’s use a regular card component to draw a distinction between the two.

Notice how the Material Design card (left) looks like it floats above the background, while the neumorphic variation(right) appears to be pushed up through the background, like a physical protrusion.

Let’s break down the differences purely from a design standpoint.

QualityMaterial DesignNeomorphism
ShadowsElements have a single or multiple dark shadows around them. Elements have two shadows: one light and one dark.
Background colorsAn element’s background color can be different than the background color of the parent element.Background colors must be the same (or very similar) as the background color of the parent element.
EdgesElements can be rounded or squared.Rounded edges are a defining quality.
BordersThere are no hard rules on borders. Using them can help prevent elements that look like they are floating off the screen.Elements can have an optional subtle border to improve contrast and make the edges a bit sharper

That should draw a pretty picture of what we’re talking about when we refer to neumorphism. Let’s move on to how it’s implemented in CSS.

Neumorphism and CSS

Creating a neumorphic interface with CSS is seemingly as easy as applying a regular box-shadow property on any element, but it’s more nuanced than that. The distinctiveness of a neumorphic UI comes from using multiple box-shadow and background-color values to achieve different types of effects and variations.

Neumorphic box shadows

Let’s do a quick refresher on the box-shadow property first so we can get a better understanding. Here’s the syntax:

box-shadow: [horizontal offset] [vertical offset] [blur radius] [optional spread radius] [color];

Following options can be adjusted:

  • Horizontal offset: A positive value offsets shadow to the right, while a negative value offsets it to the left.
  • Vertical offset: A positive value offsets shadow upwards, while a negative value offsets it downwards.
  • Blur Radius: The length of the shadow. The longer the length, the bigger and lighter the shadow becomes. There are no negative values.
  • Spread Radius: This is another length value, where larger values result in bigger, longer shadows.
  • Color: This defines the shadow’s color, just as we’d do for the CSS color property.
  • Inset: The default value (initial) results in a drop shadow. Using the inset value moves the shadow inside the frame of the element, resulting in an inner shadow.

We can apply multiple shadows using comma-separated box-shadow values. Up to four values can be concatenated, one for each side of the box.

box-shadow: 20px 20px 50px #00d2c6, 
            -30px -30px 60px #00ffff;

The following shows the box-shadow property values for a neumorphic UI element. Individual offset, blur and opacity values can be adjusted to be higher or lower, depending on the size of an element and the intensity of the effect that you’re trying to achieve. For neumorphism, it’s required to keep the shadows soft and low contrast.

As we’ve mentioned before, a core part of neumorphic elements is the use of two shadows: a light shadow and a dark shadow. That’s how we get that sort of “raised” effect and we can create variations by changing the “light source” of the shadows.

Two positive and two negative offset values need to be set. Taking this into account, we get the following four combinations, simply by changing the placement of each shadow.

Let’s use CSS variables to keep the values abstract and better understand the variations.

box-shadow: var(--h1) var(--v1) var(--blur1) var(--color-dark), 
            var(--h2) var(--v2) var(--blur2) var(--color-light);
Light SourcePositive ValuesNegative Values
Top Left--h1, --v1--h2, --v2
Top Right--h2, --v1--h1, --v2
Bottom Left--h1, --v2--h2, --v1
Bottom Right--h2, --v2--h1, --v1

We can use inset shadows to create yet more variations. Unlike drop shadows that make an element appear to be raised from beneath the background, an inset shadow gives the appearance that the element is being pressed into it.

We can change if the element is extruded from the background or inset into the background by applying the initial (not apply the option at all) or inset, respectively.

Let’s keep our light source as the top left and only toggle the inset option to see the difference.

Background colors

You might have noticed that the box-shadow values change the look of the edges of a neumorphic element. So far, we haven’t changed the background-color because it needs to be transparent or have the same (or similar) color as a background color of an underlying element, such as the element’s parent. 

We can use both solid and gradient backgrounds. Using a solid color background on the element can create a flat surface sort of look, when that solid color is the same as the color of the underlying element. 

On the other hand, using subtle gradients can change how the surface is perceived. As with the box-shadow property, there is alight and a dark value in a gradient. The gradient angle needs to be adjusted to match the light source. We have the following two variations when using gradients:

  • Convex surface variation:  The surface curves outwards where the gradient’s lighter section is aligned with the shadow’s lighter section, and the gradient’s darker section is aligned to the shadow’s darker section.
  • Concave surface variation:  The surface curves inward where the gradient’s lighter section is aligned to the shadow’s darker section, and the gradient’s darker section is aligned to the shadow’s lighter section.
.element {
  background: linear-gradient(var(--bg-angle), var(--bg-start), var(--bg-end));
  box-shadow: var(--h1) var(--v1) var(--color-dark), 
              var(--h2) var(--v2) var(--color-light);
}

Neumorphism in practice

Let’s see how Neumorphism performs when applied to a simple button. The main characteristic of a neumorphic interface is that it blends with the background and does so by having a similar or same background color as the underlying element. The main purpose of many buttons, especially a primary call-to-action, is to stand out as much as possible, usually with a prominent background color in order to separate it from other elements and other buttons on the page.

The background color constraint in neumorphism takes away that convenience. If the background color of the button matches the background color of what it’s on top of, we lose the ability to make it stand out visually with a unique color.

We can try and adjust text color, add a border below the text, add an icon or some other elements to increase the visual weight to make it stand out, etc. Whatever the case, a solid background color on a neumorphic button seems to stand out more than a gradient. Plus, it can be paired with an inset shadow on the active state to create a nice “pressed” effect.

Even though the solid color on a neumorphic button calls more attention than a gradient background, it still does not beat the way an alternate color makes a button stand out from other elements on the page.

Taking some inspiration from the real-world devices, I’ve created the following examples as an attempt to improve on the neumorphic button and toggle concept. Although the results look somewhat better, the regular button still provides a better UX, has much fewer constraints, is more flexible, is simpler to implement, and does a better job overall.

The first example was inspired by a button on my router that extrudes from the device and has a sticker with an icon on it. I added a similar “sticker” element with a solid color background as well as a slight inset to add more visual weight and make it stand out as closely as possible to the ideal button. The second example was inspired by a car control panel where the button would light up when it’s in an active (pressed) state.

Let’s take a look at some more HTML elements. One of the downsides of neumorphism that has been pointed out is that it shouldn’t be applied to elements that can have various states, like inputs, select elements, progress bars, and others. These states include:

  • User interaction: Hover, active, focus, visited
  • Validation states: Error, success, warning, disabled

UX and accessibility rules require some elements to look different in each of their respective validation states and user interaction states. Neumorphism constraints and restrictions severely limit the customization options that are required to achieve the different styles for each possible state. Variations will be very subtle and aren't possibly able to cover every single state.

Everything looks like a button! Notice how the input and button look similar and how the progress bar looks like a scrollbar or a really wide toggle.

It’s hard to see which elements are clickable! Even though this is the simplest possible example that showcases the issue, we could have added extra elements and styles to try and mitigate the issues. But as we’ve seen with the button example, some other types of elements would still perform better in terms of UX and accessibility.

It’s important to notice that Neumorphic elements also take more space (inside padding and outside margin) due to the shadow and rounded corners. A neumorphic effect wouldn’t look so good on a small-sized element simply because the visual effects consume the element.

The ideal element for neumorphism are cards, or any other static container element that doesn’t have states based on user interaction (e.g. hover, active and disabled) or validation (e.g. error, warning, and success).

In his highly critical article on neumorphism, Michal Malewicz (who helped coin “Neumorphism” as a term) suggests adding Neumorphic effects to the cards that already look good without it.

So the only way it works OK is when the card itself has the right structure, and the whole extrusion is unnecessary for hierarchy.

See?

It works well when it can be removed without any loss for the product.

Accessibility and UX

We’ve seen which elements work well with neumorphism, but there are some important rules and restrictions to keep in mind when adding the effect to elements.

First is accessibility. This is a big deal and perhaps the biggest drawback to neumorphism: color contrast.

Neumorphic UI elements rely on multiple shadows that help blend the element into the background it is on. Using subtle contrasts isn’t actually the best fit for an accessible UI. If a contrast checker is scanning your UI, it may very well call you out for not having high enough contrast between the foreground and background because shadows don’t fit into the equation and, even if they did, they’d be too subtle to make much of a difference.

Here are some valid criticisms about the accessibility of a neumorphic design:

  • Users with color blindness and poor vision would have difficulty using it due to the poor contrast caused by the soft shadows.
  • Page hierarchy is difficult to perceive when the effect is overused on a page. No particular element stands out due to the background color restrictions.
  • Users can get confused when the effect is overused on a page. Due to the extrusion effect, it’s difficult to determine which elements users can interact with and which are static.

In order to achieve a good contrast with the shadow, the background color of what a neumorphic element sits on shouldn’t get too close to the edges of RGB extremes (white and black).

Now let’s talk UX for a moment. Even though Neumorphic UI looks aesthetically pleasing, it shouldn’t be a dominant style on a page. If used too often, the UI will have an overwhelmingly plastic effect and the visual hierarchy will be all out of whack. Ae page could easily lose its intended structure when directing users to the most important content or to the main flow.

My personal take is that neumorphism is best used as an enhancement to another style. For example, it could be paired with Material Design in a way that draws distinctions between various component styles. It’s probably best to use it sparsely so that it adds a fresh alternative look to something on the screen — there’s a diminishing return on its use and it’s a good idea to watch out for it.

Here’s an  example where neumorphic qualities are used on card elements in combination with Materialize CSS:

See, it can be pretty nice when used as an accent instead of an entire framework.

That’s a wrap

So that was a deep look at neumorphism. We broke down what makes the style distinct from other popular styles, looked at a few ways to re-create the effect in CSS, and examined the implications it has on accessibility and user experience.

In practice, a full-scale neumorphic design system probably cannot be used on a website. It’s simply too restrictive in what colors can be used. Plus, the fact that it results in soft contrasts prevents it from being used on interactive elements, like buttons and toggle elements. Sure, it’s aesthetically-pleasing, modern and unique, but that shouldn’t come at the expense of usability and accessibility. It should be used sparsely, ideally in combination with another design system like Material Design.

Neumorphism is unlikely to replace the current design systems we use today (at least in my humble opinion), but it may find its place in those same design systems as a fresh new alternative to existing cards and static container styles.

References

The post Neumorphism and CSS appeared first on CSS-Tricks.

A Practical Overview Of CSS Houdini

A Practical Overview Of CSS Houdini

A Practical Overview Of CSS Houdini

Adrian Bece

It takes a long time for a new CSS feature or improvement to progress from an initial draft to a fully-supported and stable CSS feature that developers can use. JavaScript-based polyfills can be used as a substitute for the lack of browser support in order to use new CSS features before they’re officially implemented. But they are flawed in most cases. For example, scrollsnap-polyfill is one of several polyfills that can be used to fix browser support inconsistencies for the CSS Scroll Snap specification. But even that solution has some limitations, bugs and inconsistencies.

The potential downside to using polyfills is that they can have a negative impact on performance and are difficult to implement properly. This downside is related to the browser’s DOM and CSSOM. Browser creates a DOM (Document Object Model) from HTML markup and, similarly, it created CSSOM (CSS Object Model) from CSS markup. These two object trees are independent of one another. JavaScript works on DOM and has very limited access to CSSOM.

JavaScript Polyfill solutions run only after the initial render cycle has been completed, i.e. when both DOM and CSSOM have been created and the document has finished loading. After Polyfill makes changes to styles in the DOM (by inlining them), it causes the render process to run again and the whole page re-renders. Negative performance impact gets even more apparent if they rely on the requestAnimationFrame method or depend on user interactions like scroll events.

Another obstacle in web development is various constraints imposed by the CSS standards. For example, there are only a limited number of CSS properties that can be natively animated. CSS knows how to natively animate colors, but doesn’t know how to animate gradients. There has always been a need to innovate and create impressive web experiences by pushing the boundaries despite the tech limitations. That is why developers often tend to gravitate towards using less-than-ideal workarounds or JavaScript to implement more advanced styling and effects that are currently not supported by CSS such as masonry layout, advanced 3D effects, advanced animation, fluid typography, animated gradients, styled select elements, etc.

It seems impossible for CSS specifications to keep up with the various feature demands from the industry such as more control over animations, improved text truncation, better styling option for input and select elements, more display options, more filter options, etc.

What could be the potential solution? Give developers a native way of extending CSS using various APIs. In this article, we are going to take a look at how frontend developers can do that using Houdini APIs, JavaScript, and CSS. In each section, we’re going to examine each API individually, check its browser support and current specification status, and see how they can be implemented today using Progressive enhancement.

What Is Houdini?

Houdini, an umbrella term for the collection of browser APIs, aims to bring significant improvements to the web development process and the development of CSS standards in general. Developers will be able to extend the CSS with new features using JavaScript, hook into CSS rendering engine and tell the browser how to apply CSS during a render process. This will result in significantly better performance and stability than using regular polyfills.

Houdini specification consists of two API groups - high-level APIs and low-level APIs.

High-level APIs are closely related to the browser’s rendering process (style → layout → paint → composite). This includes:

  • Paint API
    An extension point for the browser’s paint rendering step where visual properties (color, background, border, etc.) are determined.
  • Layout API
    An extension point for the browser’s layout rendering step where element dimensions, position, and alignment are determined.
  • Animation API
    An extension point for browser’s composite rendering step where layers are drawn to the screen and animated.

Low-Level APIs form a foundation for high-level APIs. This includes:

  • Typed Object Model API
  • Custom Properties & Values API
  • Font Metrics API
  • Worklets

Some Houdini APIs are already available for use in some browsers with other APIs to follow suit when they’re ready for release.

The Future Of CSS

Unlike regular CSS feature specifications that have been introduced thus far, Houdini stands out by allowing developers to extend the CSS in a more native way. Does this mean that CSS specifications will stop evolving and no new official implementations of CSS features will be released? Well, that is not the case. Houdini’s goal is to aid the CSS feature development process by allowing developers to create working prototypes that can be easily standardized.

Additionally, developers will be able to share the open-source CSS Worklets more easily and with less need for browser-specific bugfixes.

Typed Object Model API

Before Houdini was introduced, the only way for JavaScript to interact with CSS was by parsing CSS represented as string values and modifying them. Parsing and overriding styles manually can be difficult and error-prone due to the value type needing to be changed back and forth and value unit needing to be manually appended when assigning a new value.

selectedElement.style.fontSize = newFontSize + "px"; // newFontSize = 20
console.log(selectedElement.style.fontSize); // "20px"

Typed Object Model (Typed OM) API adds more semantic meaning to CSS values by exposing them as typed JavaScript objects. It significantly improves the related code and makes it more performant, stable and maintainable. CSS values are represented by the CSSUnitValue interface which consists of a value and a unit property.

{
  value: 20, 
  unit: "px"
}

This new interface can be used with the following new properties:

  • computedStyleMap(): for parsing computed (non-inline) styles. This is a method of selected element that needs to be invoked before parsing or using other methods.
  • attributeStyleMap: for parsing and modifying inline styles. This is a property that is available on a selected element.
// Get computed styles from stylesheet (initial value)
selectedElement.computedStyleMap().get("font-size"); // { value: 20, unit: "px"}

// Set inline styles
selectedElement.attributeStyleMap.set("font-size", CSS.em(2)); // Sets inline style
selectedElement.attributeStyleMap.set("color", "blue"); // Sets inline style

// Computed style remains the same (initial value)
selectedElement.computedStyleMap().get("font-size"); // { value: 20, unit: "px"}

// Get new inline style
selectedElement.attributeStyleMap.get("font-size"); // { value: 2, unit: "em"}

Notice how specific CSS types are being used when setting a new numeric value. By using this syntax, many potential type-related issues can be avoided and the resulting code is more reliable and bug-free.

The get and set methods are only a small subset of all available methods defined by the Typed OM API. Some of them include:

  • clear: removes all inline styles
  • delete: removes a specified CSS property and its value from inline styles
  • has: returns a boolean if a specified CSS property is set
  • append: adds an additional value to a property that supports multiple values
  • etc.

Feature detection

var selectedElement = document.getElementById("example");

if(selectedElement.attributeStyleMap) {
  /* ... */
}

if(selectedElement.computedStyleMap) {
  /* ... */
}

W3C Specification Status

Browser Support

Google Chrome Microsoft Edge Opera Browser Firefox Safari
Supported Supported Supported Not supported Partial support (*)

* supported with “Experimental Web Platform features” or other feature flag enabled.

Data source: Is Houdini Ready Yet?

Custom Properties And Values API

The CSS Properties And Values API allows developers to extend CSS variables by adding a type, initial value and define inheritance. Developers can define CSS custom properties by registering them using the registerProperty method which tells the browsers how to transition it and handle fallback in case of an error.

CSS.registerProperty({ 
  name: "--colorPrimary",
  syntax: "<color>", 
  inherits: false,
  initialValue: "blue",
});

This method accepts an input argument that is an object with the following properties:

  • name: the name of the custom property
  • syntax: tells the browser how to parse a custom property. These are pre-defined values like <color>, <integer>, <number>, <length>, <percentage>, etc.
  • inherits: tells the browser whether the custom property inherits its parent’s value.
  • initialValue: tells the initial value that is used until it’s overridden and this is used as a fallback in case of an error.

In the following example, the <color> type custom property is being set. This custom property is going to be used in gradient transition. You might be thinking that current CSS doesn’t support transitions for background gradients and you would be correct. Notice how the custom property itself is being used in transition, instead of a background property that would be used for regular background-color transitions.

.gradientBox { 
  background: linear-gradient(45deg, rgba(255,255,255,1) 0%, var(--colorPrimary) 60%);
  transition: --colorPrimary 0.5s ease;
  /* ... */
}

.gradientBox:hover {
  --colorPrimary: red
  /* ... */
}

Browser doesn’t know how to handle gradient transition, but it knows how to handle color transitions because the custom property is specified as <color> type. On a browser that supports Houdini, a gradient transition will happen when the element is being hovered on. Gradient position percentage can also be replaced with CSS custom property (registered as <percentage> type) and added to a transition in the same way as in the example.

If registerProperty is removed and a regular CSS custom property is registered in a :root selector, the gradient transition won’t work. It’s required that registerProperty is used so the browser knows that it should treat it as color.

In the future implementation of this API, it would be possible to register a custom property directly in CSS.

@property --colorPrimary { 
  syntax: "<color>"; 
  inherits: false; 
  initial-value: blue;
}

Example

This simple example showcases gradient color and position transition on hover event using registered CSS custom properties for color and position respectively. Complete source code is available on the example repository.

Animated gradient color and position using Custom Properties & Values API. Delay for each property added for effect in CSS transition property. (Large preview)

Feature Detection

if (CSS.registerProperty) {
  /* ... */
}

W3C Specification Status

Browser Support

Google Chrome Microsoft Edge Opera Browser Firefox Safari
Supported Supported Supported Not supported Not supported

Data source: Is Houdini Ready Yet?

Font Metrics API

The Font Metrics API is still in a very early stage of development, so its specification may change in the future. In its current draft, Font Metrics API will provide methods for measuring dimensions of text elements that are being rendered on screen in order to allow developers to affect how text elements are being rendered on screen. These values are either difficult or impossible to measure with current features, so this API will allow developers to create text and font-related CSS features more easily. Multi-line dynamic text truncation is an example of one of those features.

W3C Specification Status

Browser Support

Google Chrome Microsoft Edge Opera Browser Firefox Safari
Not supported Not supported Not supported Not supported Not supported

Data source: Is Houdini Ready Yet?

Worklets

Before moving onto the other APIs, it’s important to explain the Worklets concept. Worklets are scripts that run during render and are independent of the main JavaScript environment. They are an extension point for rendering engines. They are designed for parallelism (with 2 or more instances) and thread-agnostic, have reduced access to the global scope and are called by the rendering engine when needed. Worklets can be run only on HTTPS (on production environment) or on localhost (for development purposes).

Houdini introduces following Worklets to extend the browser render engine:

  • Paint Worklet - Paint API
  • Animation Worklet - Animation API
  • Layout Worklet - Layout API

Paint API

The Paint API allows developers to use JavaScript functions to draw directly into an element’s background, border, or content using 2D Rendering Context, which is a subset of the HTML5 Canvas API. Paint API uses Paint Worklet to draw an image that dynamically responds to changes in CSS (changes in CSS variables, for example). Anyone familiar with Canvas API will feel right at home with Houdini’s Paint API.

There are several steps required in defining a Paint Worklet:

  1. Write and register a Paint Worklet using the registerPaint function
  2. Call the Worklet in HTML file or main JavaScript file using CSS.paintWorklet.addModule function
  3. Use the paint() function in CSS with a Worklet name and optional input arguments.

Let’s take a look at the registerPaint function which is used to register a Paint Worklet and define its functionality.

registerPaint("paintWorketExample", class {
  static get inputProperties() { return ["--myVariable"]; }
  static get inputArguments() { return ["<color>"]; }
  static get contextOptions() { return {alpha: true}; }

  paint(ctx, size, properties, args) {
    /* ... */
  }
});

The registerPaint function consists of several parts:

  • inputProperties:
    An array of CSS custom properties that the Worklet will keep track of. This array represents dependencies of a paint worklet.
  • inputArguments:
    An array of input arguments that can be passed from paint function from inside the CSS.
  • contextOptions: allow or disallow opacity for colors. If set to false, all colors will be displayed with full opacity.
  • paint: the main function that provides the following arguments:
    • ctx: 2D drawing context, almost identical to Canvas API’s 2D drawing context.
    • size: an object containing the width and height of the element. Values are determined by the layout rendering process. Canvas size is the same as the actual size of the element.
    • properties: input variables defined in inputProperties
    • args: an array of input arguments passed in paint function in CSS

After the Worklet has been registered, it needs to be invoked in the HTML file by simply providing a path to the file.

CSS.paintWorklet.addModule("path/to/worklet/file.js");

Any Worklet can also be added from an external URL (from a Content Delivery Network, for example) which makes them modular and reusable.

CSS.paintWorklet.addModule("https://url/to/worklet/file.js");

After the Worklet has been called, it can be used inside CSS using the paint function. This function accepts the Worklet’s registered name as a first input argument and each input argument that follows it is a custom argument that can be passed to a Worklet (defined inside Worklet’s inputArguments ). From that point, the browser determines when to call the Worklet and which user actions and CSS custom properties value change to respond to.

.exampleElement {
  /* paintWorkletExample - name of the worklet
     blue - argument passed to a Worklet */
  background-image: paint(paintWorketExample, blue);
}

Example

The following example showcases Paint API and general Worklet reusability and modularity. It’s using the ripple Worklet directly from Google Chrome Labs repository and runs on a different element with different styles. Complete source code is available on the example repository.

Ripple effect example (uses Ripple Worklet by Google Chrome Labs) (Large preview)

Feature detection

if ("paintWorklet" in CSS) {
  /* ... */
}


@supports(background:paint(paintWorketExample)){
  /* ... */
}

W3C Specification Status

Browser Support

Google Chrome Microsoft Edge Opera Browser Firefox Safari
Supported Supported Supported Not supported Not supported

Data source: Is Houdini Ready Yet?

Animation API

The Animation API extends web animations with options to listen to various events (scroll, hover, click, etc.) and improves performance by running animations on their own dedicated thread using an Animation Worklet. It allows for user action to control the flow of animation that runs in a performant, non-blocking way.

Like any Worklet, Animation Worklet needs to be registered first.

registerAnimator("animationWorkletExample", class {
  constructor(options) {
    /* ... */
  }
  animate(currentTime, effect) {
    /* ... */
  }
});

This class consists of two functions:

  • constructor: called when a new instance is created. Used for general setup.
  • animate: the main function that contains the animation logic. Provides the following input arguments:
    • currentTime: the current time value from the defined timeline
    • effect: an array of effects that this animation uses

After the Animation Worklet has been registered, it needs to be included in the main JavaScript file, animation (element, keyframes, options) needs to be defined and animation is instantiated with the selected timeline. Timeline concepts and web animation basics will be explained in the next section.

/* Include Animation Worklet */
await CSS.animationWorklet.addModule("path/to/worklet/file.js");;

/* Select element that's going to be animated */
const elementExample = document.getElementById("elementExample");

/* Define animation (effect) */
const effectExample = new KeyframeEffect(
  elementExample,  /* Selected element that's going to be animated */
  [ /* ... */ ],   /* Animation keyframes */
  { /* ... */ },   /* Animation options - duration, delay, iterations, etc. */
);

/* Create new WorkletAnimation instance and run it */
new WorkletAnimation(
  "animationWorkletExample"  /* Worklet name */
  effectExample,             /* Animation (effect) timeline */
  document.timeline,         /* Input timeline */
  {},                        /* Options passed to constructor */
).play();                    /* Play animation */

Timeline Mapping

Web animation is based on timelines and mapping of the current time to a timeline of an effect’s local time. For example, let’s take a look at a repeating linear animation with 3 keyframes (start, middle, last) that runs 1 second after a page is loaded (delay) and with a 4-second duration.

Effect timeline from the example would look like this (with the 4-second duration with no delay):

Effect timeline (4s duration) Keyframe
0ms First keyframe - animation starts
2000ms Middle keyframe - animation in progress
4000ms Last keyframe - animation ends or resets to first keyframe

In order to better understand effect.localTime, by setting its value to 3000ms (taking into account 1000ms delay), resulting animation is going to be locked to a middle keyframe in effect timeline (1000ms delay + 2000ms for a middle keyframe). The same effect is going to happen by setting the value to 7000ms and 11000ms because the animation repeats in 4000ms interval (animation duration).

animate(currentTime, effect) {
  effect.localTime = 3000; // 1000ms delay + 2000ms middle keyframe
}

No animation happens when having a constant effect.localTime value because animation is locked in a specific keyframe. In order to properly animate an element, its effect.localTime needs to be dynamic. It’s required for the value to be a function that depends on the currentTime input argument or some other variable.

The following code shows a functional representation of 1:1 (linear function) mapping of a timeline to effect local time.

animate(currentTime, effect) {
  effect.localTime = currentTime; // y = x linear function
}
Timeline (document.timeline) Mapped effect local time Keyframe
startTime + 0ms (elapsed time) startTime + 0ms First
startTime + 1000ms (elapsed time) startTime + 1000ms (delay) + 0ms First
startTime + 3000ms (elapsed time) startTime + 1000ms (delay) + 2000ms Middle
startTime + 5000ms (elapsed time) startTime + 1000ms (delay) + 4000ms Last / First
startTime + 7000ms (elapsed time) startTime + 1000ms (delay) + 6000ms Middle
startTime + 9000ms (elapsed time) startTime + 1000ms (delay) + 8000ms Last / First

Timeline isn’t restricted to 1:1 mapping to effect’s local time. Animation API allows developers to manipulate the timeline mapping in animate function by using standard JavaScript functions to create complex timelines. Animation also doesn’t have to behave the same in each iteration (if animation is repeated).

Animation doesn’t have to depend on the document’s timeline which only starts counting milliseconds from the moment it’s loaded. User actions like scroll events can be used as a timeline for animation by using a ScrollTimeline object. For example, an animation can start when a user has scrolled to 200 pixels and can end when a user has scrolled to 800 pixels on a screen.

const scrollTimelineExample = new ScrollTimeline({
  scrollSource: scrollElement,  /* DOM element whose scrolling action is being tracked */
  orientation: "vertical",      /* Scroll direction */
  startScrollOffset: "200px",   /* Beginning of the scroll timeline */
  endScrollOffset: "800px",    /* Ending of the scroll timeline */
  timeRange: 1200,              /* Time duration to be mapped to scroll values*/
  fill: "forwards"              /* Animation fill mode */
});

...

The animation will automatically adapt to user scroll speed and remain smooth and responsive. Since Animation Worklets are running off the main thread and are connected to a browser’s rending engine, animation that depends on user scroll can run smoothly and be very performant.

Example

The following example showcases how a non-linear timeline implementation. It uses modified Gaussian function and applies translation and rotation animation with the same timeline. Complete source code is available on the example repository.

Animation created with Animation API which is using modified Gaussian function time mapping (Large preview)

Feature Detection

if (CSS.animationWorklet) {
  /* ... */
}

W3C Specification Status

Browser Support

Google Chrome Microsoft Edge Opera Browser Firefox Safari
Partial support (*) Partial support (*) Partial support (*) Not supported Not supported

* supported with “Experimental Web Platform features” flag enabled.

Data source: Is Houdini Ready Yet?

Layout API

The Layout API allows developers to extend the browser’s layout rendering process by defining new layout modes that can be used in display CSS property. Layout API introduces new concepts, is very complex and offers a lot of options for developing custom layout algorithms.

Similarly to other Worklets, the layout Worklet needs to be registered and defined first.

registerLayout('exampleLayout', class {
  static get inputProperties() { return ['--exampleVariable']; }

  static get childrenInputProperties() { return ['--exampleChildVariable']; }

  static get layoutOptions() {
    return {
      childDisplay: 'normal',
      sizing: 'block-like'
    };
  }

  intrinsicSizes(children, edges, styleMap) {
    /* ... */
  }

  layout(children, edges, constraints, styleMap, breakToken) {
    /* ... */
  }
});

Worklet register contains the following methods:

  • inputProperties:
    An array of CSS custom properties that the Worklet will keep track of that belongs to a Parent Layout element, i.e. the element that calls this layout. This array represents dependencies of a Layout Worklet.
  • childrenInputProperties:
    An array of CSS custom properties that the Worklet will keep track of that belong to child elements of a Parent Layout element, i.e. the children of the elements that set this layout.
  • layoutOptions: defines the following layout properties:
    • childDisplay: can have a pre-defined value of block or normal. Determines if the boxes will be displayed as blocks or inline.
    • sizing: can have a pre-defined value of block-like or manual. It tells the browser to either pre-calculate the size or not to pre-calculate (unless a size is explicitly set), respectively.
  • intrinsicSizes: defines how a box or its content fits into a layout context.
    • children: child elements of a Parent Layout element, i.e. the children of the element that call this layout.
    • edges: Layout Edges of a box
    • styleMap: typed OM styles of a box
  • layout: the main function that performs a layout.
    • children: child elements of a Parent Layout element, i.e. the children of the element that call this layout.
    • edges: Layout Edges of a box
    • constraints: constraints of a Parent Layout
    • styleMap: typed OM styles of a box
    • breakToken: break token used to resume a layout in case of pagination or printing.

Like in the case of a Paint API, the browser rendering engine determines when the paint Worklet is being called. It only needs to be added to an HTML or main JavaScript file.

CSS.layoutWorklet.addModule('path/to/worklet/file.js');

And, finally, it needs to be referenced in a CSS file

.exampleElement {
  display: layout(exampleLayout);
}

How Layout API Performs Layout

In the previous example, exampleLayout has been defined using the Layout API.

.exampleElement {
  display: layout(exampleLayout);
}

This element is called a Parent Layout that is enclosed with Layout Edges which consists of paddings, borders and scroll bars. Parent Layout consists of child elements which are called Current Layouts. Current Layouts are the actual target elements whose layout can be customized using the Layout API. For example, when using display: flex; on an element, its children are being repositioned to form the flex layout. This is similar to what is being done with the Layout API.

Each Current Layout consists of Child Layout which is a layout algorithm for the LayoutChild (element, ::before and ::after pseudo-elements) and LayoutChild is a CSS generated box that only contains style data (no layout data). LayoutChild elements are automatically created by browser rendering engine on style step. Layout Child can generate a Fragment which actually performs layout render actions.

Example

Similarly to the Paint API example, this example is importing a masonry layout Worklet directly from Google Chrome Labs repository, but in this example, it’s used with image content instead of text. Complete source code is available on the example repository.

Masonry layout example (uses Masonry Worklet by Google Chrome Labs (Large preview)

Feature Detection

if (CSS.layoutWorklet) {
  /* ... */
}

W3C Specification Status

Browser Support

Google Chrome Microsoft Edge Opera Browser Firefox Safari
Partial support (*) Partial support (*) Partial support (*) Not supported Not supported

* supported with “Experimental Web Platform features” flag enabled.

Data source: Is Houdini Ready Yet?

Houdini And Progressive Enhancement

Even though CSS Houdini doesn’t have optimal browser support yet, it can be used today with progressive enhancement in mind. If you are unfamiliar with Progressive enhancement, it would be worth to check out this handy article which explains it really well. If you decide on implementing Houdini in your project today, there are few things to keep in mind:

  • Use feature detection to prevent errors.
    Each Houdini API and Worklet offers a simple way of checking if it’s available in the browser. Use feature detection to apply Houdini enhancements only to browsers that support it and avoid errors.
  • Use it for presentation and visual enhancement only.
    Users that are browsing a website on a browser that doesn’t yet support Houdini should have access to the content and core functionality of the website. User experience and the content presentation shouldn’t depend on Houdini features and should have a reliable fallback.
  • Make use of a standard CSS fallback.
    For example, regular CSS Custom Properties can be used as a fallback for styles defined using Custom Properties & Values API.

Focus on developing a performant and reliable website user experience first and then use Houdini features for decorative purposes as a progressive enhancement.

Conclusion

Houdini APIs will finally enable developers to keep the JavaScript code used for style manipulation and decoration closer to the browser’s rendering pipeline, resulting in better performance and stability. By allowing developers to hook into the browser rendering process, they will be able to develop various CSS polyfills that can be easily shared, implemented and, potentially, added to CSS specification itself. Houdini will also make developers and designers less constrained by the CSS limitations when working on styling, layouts, and animations, resulting in new delightful web experiences.

CSS Houdini features can be added to projects today, but strictly with progressive enhancement in mind. This will enable browsers that do not support Houdini features to render the website without errors and offer optimal user experience.

It’s going to be exciting to watch what the developer community will come up with as Houdini gains traction and better browser support. Here are some awesome examples of Houdini API experiments from the community:

References

Smashing Editorial (ra, il)