Tag: Making

Making Mermaid Diagrams in Markdown

Mermaid diagrams and flowcharts have been gaining traction, especially with GitHub’s announcement that they are natively supported in Markdown. Let’s take a look at what they are, how to use them, and just as importantly: why.

Just like you might want to embed your CodePen demo directly in your documentation source, having your diagrams and charts live adjacent to your text helps prevent them from rotting — that is, drifting out of sync with the state of your document. Just as unhelpful, obsolete, or otherwise misleading comments in your code can be objectively worse than no comments, the same goes for diagrams.

Mermaid diagrams pair well with Jamstack and static site generators, which continue to grow in popularity. The pairing is natural. While Mermaid diagrams aren’t Markdown-exclusive, they are Markdown-inspired. Using the same markup abstractions Markdown provides to notate code, Mermaid can be represented the same to output diagrams and flowcharts. And Markdown is to Jamstack and static sites as peanut butter is to jelly.

If your site is authored in Markdown, processed into HTML, and you have enough control to add a bit of custom JavaScript, then you can use the ideas we’re covering in this article to fit your own needs and implement diagrams with Mermaid conveniently alongside the rest of your Markdown. Is “diagrams-as-code” a term yet? It should be.

For example, let’s say you’re working on a fancy new product and you want to provide a roadmap in the form of a Gantt chart (or some other type — say flowcharts, sequences, and class diagrams). With Mermaid, you can do this in a small handful of lines:

gantt   title My Product Roadmap   dateFormat  YYYY-MM-DD   section Cool Feature   A task           :a1, 2022-02-25, 30d   Another task     :after a1, 20d   section Rad Feature   Task in sequence :2022-03-04, 12d   Task, No. 2      :24d

Which will render a nice SVG diagram like so:

Showing a Mermaid diagram of a roadmap in shades of purple.
Nine lines of code gets us a full-fledged Gantt chart that can be used for product roadmaps and such.

Pro tip: Mermaid has a live editor which lets you try it out without the commitment over at mermaid.live.

Mermaid diagrams in Markdown

Mermaid goes well with Markdown because it presents itself as just another fenced code block, only using the mermaid language syntax set. For example, this block of code:

```mermaid graph TD;     A-->B;     A-->C;     B-->D;     C-->D; ```

…produces an HTML <pre> element with the code block contents inside:

<pre class="mermaid"><code>graph TD;     A-->B;     A-->C;     B-->D;     C-->D;</code></pre>

If you’re using a Markdown processor aligned with the CommonMark spec, it’ll more resemble this:

<pre><code class="language-mermaid">graph TD;     A-->B;     A-->C;     B-->D;     C-->D; </code></pre>

The Mermaid API’s default behavior expects a <div class="mermaid"> tag that directly contains the contents — so, no <code> or <span> (like from a syntax highlighter) that you might see in the conversion from Markdown-to-HTML.

Finessing with JavaScript

With a bit of JavaScript, it’s reasonable to take the Markdown-generated HTML and finesse it into the <div class="mermaid"> tag that Mermaid targets. It’s worth noting that $ element.textContent is purposeful here: Markdown will HTML-encode specific characters (like > into &gt;) that Mermaid uses. It also filters out any erroneous HTML elements that are descendants of the <pre> element.

// select <pre class="mermaid"> _and_ <pre><code class="language-mermaid"> document.querySelectorAll("pre.mermaid, pre>code.language-mermaid").forEach($ el => {   // if the second selector got a hit, reference the parent <pre>   if ($ el.tagName === "CODE")     $ el = $ el.parentElement   // put the Mermaid contents in the expected <div class="mermaid">   // plus keep the original contents in a nice <details>   $ el.outerHTML = `     <div class="mermaid">$ {$ el.textContent}</div>     <details>       <summary>Diagram source</summary>       <pre>$ {$ el.textContent}</pre>     </details>   ` })

Now that our HTML is properly-formatted, let’s implement Mermaid to do the rendering.

Using Mermaid

Mermaid is published as an npm package, so you can grab a copy by using a package-aware CDN, like unpkg. You’ll want to use the minified code (e.g., mermaid.min.js) instead of the default export of mermaid.core.js. For example:

<script src="https://unpkg.com/mermaid@8.14.0/dist/mermaid.min.js"></script>

Mermaid is also ESM-ready, so you can use Skypack to load it up as well:

<script type="module">   import mermaid from "https://cdn.skypack.dev/mermaid@8.14.0"; </script>

You could stop right here if you want to keep things simple. By default, Mermaid will auto-initialize itself when the document is ready. As long as you do the Markdown-to-HTML finessing with JavaScript mentioned earlier — before loading in Mermaid — you’ll be all set.

However, Mermaid has a couple settings worth configuring:

// initialize Mermaid to [1] log errors, [2] have loose security for first-party // authored diagrams, and [3] respect a preferred dark color scheme mermaid.initialize({   logLevel: "error", // [1]   securityLevel: "loose", // [2]   theme: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) ?     "dark" :     "default" // [3] })
  1. logLevel will give you a bit more visibility into any errors that may arise. If you want to see more information, you can choose a more verbose level (or vice versa).
  2. securityLevel relates to the level of trust for the diagram source. If it’s content that you are authoring, then "loose" is fine. If it’s user-generated content, it’s probably best leaving the "strict" default in place.
  3. theme changes the styling of the rendered diagrams. By querying the preferred color scheme and leveraging a ternary operator, we can specify "dark" as appropriate.

All together now!

Here are a couple of Mermaid diagrams examples in Markdown:

Deeper waters

This strategy is particularly effective because it’s progressive: if JavaScript is disabled then the original Mermaid source is displayed as-is. No foul.

There’s also a fully-fledged command line interface for Mermaid which, if you’re interesting in exploring, could potentially be leveraged to display diagrams that are completely server-side rendered. Between the Mermaid CLI and the online generator, it may even be possible to hook into whatever build process you use to generate a snapshot of a diagram and display it as an <img> fallback instead of the source code.

Hopefully, we’ll see more native Mermaid integrations like this as Mermaid continues to grow in popularity. The usefulness of having visual charts and diagrams alongside documentation is unquestionable — from product roadmaps to decision trees and everything in between. That’s the sort of information that’s just plain difficult to document with words alone.

Mermaid charts solve that, and in a way that ensures the information can be managed and maintained alongside the rest of the documentation.


Making Mermaid Diagrams in Markdown originally published on CSS-Tricks. You should get the newsletter.

CSS-Tricks

, , ,

The Making of Atomic CSS: An Interview With Thierry Koblentz

I interviewed Thierry Koblentz, creator of Atomic CSS, to understand the history and background that led to making of the popular CSS framework. Thierry, now retired, has vast experience writing CSS at large scale and has previously worked as a front-end engineer at Yahoo!.

Thierry is widely credited with bringing the concept of Atomic CSS to the mainstream, thanks to his now classic 2013 article on Smashing Magazine, “Challenging CSS Best Practices.” That article paved the way for many popular CSS libraries over the years. In this interview, Thierry returns to chronicle the history of Atomic CSS and reflect on its ongoing legacy.

Photo of Thierry Koblentz smiling.
Thierry Koblentz

Rolling back the years to the early 2000’s, how did you get into web development, especially writing CSS to make a living?

Thierry Koblentz: I taught myself HTML, CSS, and JavaScript as a hobby after moving to the U.S. in 1997.

At the time, I was using FrontPage and was relying heavily on Newsgroups for guidance. I quickly became a regular on Macromedia NewsGroups and on CSS-Discuss. Early on, I espoused the philosophy of the Web Standard Project and got really interested in Accessibility. For years, front-end was nothing more than a hobby for me (my real job was an antique dealer). I would create a website once in a while but I was mostly writing and publishing (many) articles, sharing techniques I’d learned or “discovered.”

This paid off in the form of a phone call from Yahoo! in 2007, asking if I could help fixing and building stylesheets for the Yahoo! Site Solutions (YSS) website builder template. The job description: no HTML, no JavaScript, just CSS! And a lot of it!

What was your day job at Yahoo! like?

TK: My role at Yahoo! changed a lot through the years.

My first job was to create stylesheets (à la CSS Zen Garden) for the YSS template. I then rewrote the markup and styles of the YSS website just before YSS was “shipped” to Bangalore (India) — where I was sent with my colleagues for “transfer of knowledge” purposes.

As a sidenote, it was the challenge of swapping stylesheets to create different designs for YSS that forced us to find a light (non-js) solution for resizing videos on the fly; and that’s how I came up with “Creating Intrinsic Ratios for Video.”

After YSS, I had the opportunity to only work on projects that started from scratch (rewrites or otherwise) and I got more and more involved with Yahoo! FE. I edited and created many internal docs (i.e. CSS Coding Standards); participated in the hiring process (like everybody else in my team); led code review sessions; ran CSS classes and workshops; spoke at FED London; helped other teams with HTML/CSS/accessibility; was involved in decisions regarding technology adoption (i.e. Bootstrap or not Bootstrap); created libraries; reviewed internal papers; wrote proposals; etc.

Another sidenote, during my eight years at Yahoo!, I may have written less than 100 lines of JavaScript. And if I didn’t quit or get fired from my job, it is thanks to Lingyan Zhu and Renato Iwashima; they helped me tirelessly when it came to setting up my environment or dealing with the command line (because, to this day, I am terrible at that).

TK: In the early days, there were neither libraries nor published methodologies; it was the Wild West, everything went: “non-semantic” classes, IDs, CSS hacks, conditional comments, frames, CSS expressions, “JS sniffing,” designing primarily for Internet Explorer, etc. On my old website, I even had this comment:

<!--MSIE5 Mac needs this comment -->

Everything was fair game and everything was abused as we had a very limited set of tools with the demand to do a lot.

But things had changed dramatically by the time I joined Yahoo!. Devs from the U.K. were strong supporters of Web Standards and I credit them for greatly influencing how HTML and CSS were written at Yahoo!. Semantic markup was a reality and CSS was written following the Separation of Concern (SoC) principle to the “T” (which was overzealous for my liking at times though).

YUI had CSS components but did not have a CSS framework yet. There was an in-house CSS library (called Lego) but I never had to use it. Methodologies and libraries, like OOCSS, SMACSS, ECSS (shoutout to Ben), BEM, Bootstrap, Pure, and others would come shortly after.

What led to the idea of Atomic CSS?

TK: Before YSS was moved to India, my manager, Michael Montesano, asked if there was a way for the new team in Bangalore to avoid having to edit the stylesheet, and thus reducing the risks of breakage. I guess the YSS template experience (paying customers complaining about broken pages) made him pretty paranoid when it came to making any change to a stylesheet.

So I created a “utility-sheet” in the spirit of my ez-css library — a sheet meant to let developers achieve their styling without the need to edit or add rules in a stylesheet.

A couple of years later, Michael, then Director of Engineering, asked me if I could redesign Yahoo!’s Home Page using utility classes only, knowing that, once again, we wouldn’t be in charge of the website maintenance. We talked about prioritizing utility classes over semantic classes, something I don’t think existed at such a level at the time. It was a very bold move.

This large scale exercise quickly became a proof of concept that showed the many benefits that came with styling via markup. It checked so many boxes that it was decided that we’d use that “static” stylesheet (called Stencil) to redesign the Yahoo! My Home Page product.

Screenshot of the Atomic CSS homepage. The background is bright blue with white text that says Atomic CSS on Steroids with a Get Started button below. At the bottom is a small blurb that reads CSS for component-based frameworks.

What were the guiding principles while designing Atomic CSS (ACSS) and who were the people involved?

TK: Our Stencil library being static was a great tool to impose/enforce a design style — which we thought Yahoo! was about to adopt across all its properties. We quickly realized that this was not going to happen. Every Yahoo! design team had their own view of what was the perfect font size, the perfect margin, etc., and we were constantly receiving requests to add very specific styles to the library.

That situation was unmaintainable so we decided to come up with a tool that would let developers create their own styles on the fly, while respecting the Atomic nature of the authoring method. And that’s how Atomizer was born. We stopped worrying about adding styles — CSS declarations — and instead focused on creating a rich vocabulary to give developers a wide array of styling, like media queries, descendant selectors, and pseudo-classes, among other things.

With ACSS, developers were free to use whatever they wanted; hence teams were able to adopt different design styles and styles guides while using the exact same library. And there were some immediate benefits that were new to the way developers were used to writing styles. They no longer had to worry about breaking the page with their styling or worry about writing selectors to style their components.

ACSS was built first and foremost to address Yahoo!’s problems and to work in Yahoo!’s environment.

The people involved with Atomic CSS were Renato Iwashima, Steve Carlson, and myself. Renato and Steve created Atomizer.

What misconceptions do people have about CSS when they don’t write CSS for large enterprises?

TK: When I joined Yahoo! in 2007, I quickly learned how enormous a codebase could be. There were teams working across many locations/timezones; a myriad of products; hundreds of shared components; third-party code; A/B testing strategies; scaling as a requirement; different script directions; localization and internationalization; various release cycles; complex deployment mechanisms; tons of metrics; legacies of all sorts; strict coding standards; build processes; politics; and more politics; etc.

Most of that was totally new to me and I had to learn if and how any of it could influence the way I was writing CSS. I started to revisit and challenge all my beliefs as many techniques or methods that were common practice to me seemed to be unfit, or at least counter-productive, for complex apps.

One “reality check” relates to style abstraction. We all have read articles saying that mapping a M-10 class to margin: 10px was not a good idea as it meant to edit both the HTML and CSS to change the styling. Unfortunately, this is what happens in large/complex projects:

  • Designer: I want a 15px gap
  • Developer: OK, that’s M-3x (5px increment)
  • Designer: Sure, whatever!
  • Developer: Done!
  • Designer: Actually, 15px is a bit too big, can you make it 12px?
  • Developer: No, we don’t have that, it’s either 10px or 15px.
  • Designer: Sorry, that doesn’t work for me. Can we change M-3x to be 12px?
  • Developer: Nope! We can’t do that because other teams expect M-3x to be 15px.
  • Designer: OK, try to figure a way because we want the margin to be 12px. 15px is too much and 10px is too little.
  • Developer: (F*ck this!)

To anticipate such a problem, one needs to understand the designer’s intent behind their request: is the style chosen because of its abstraction, e.g. color primary, or for its specific value, e.g. a margin of 15px in our M-3x case? If a style guide exists to enforce design principles, then classes like M-3x may be OK, but if design teams can request any style they want, then it is much safer to stay away from naming conventions that will lead to ambiguous styling. In my experience, anything ambiguous leads, sooner or later, to breakages.

Relying on the structure of a document or component for its styling — via combinators like > or + — sounds like a clean approach to authoring a stylesheet, but it is ignoring the fact that in a complex environment one cannot assume any specific markup, or construct, to be immutable.

You think z-index is complicated? Think again when you do not even own the scope of the stack your component lives in. That’s one of the most complex issues to address in a large project where teams are in charge of different parts of the page. I once wrote a proposal about this.

Qualifying selectors — like input.required vs. .input.required — may look good and semantic but it creates an unnecessary specificity level — like 0.1.1 vs. 0.2.0 — and prevents markup change; two things easy to avoid by making sure you do not qualify your selector.

Relying on the universal selector, *, for styling global scope? In a very large project, it could mean you are styling someone else’s component. Don’t make styling decisions for people unless you know their requirements.

I am sure you have read that IDs are bad and that specificity is evil but. in fact. high specificity is not as much of a problem as the number of specificity levels your rules create. It is much easier to style within an environment where only two or three levels exist — like 1.1.0, 0.1.0, 0.2.0 — rather than an environment where specificity is rather low but follows a “free for all” approach — like 0.1.0, 0.1.1, 0.2.0, 0.2.1, 0.2.2, etc. — which often comes as a defensive mechanism in large projects as a mean to “sandbox” styles.

Blindly following advice from the CSS community may lead to unpleasant surprises. Never jump on new techniques that have not yet been battle tested. Remember will-change? And always know what every style you use does or may trigger. For example, position can create a stacking context and a containing block, while overflow can create a block-formatting context.

In my experience, knowing CSS inside-out is not enough to write CSS efficiently for a large organization. During my tenure at Yahoo!, I often found myself in contradiction with people I used to be aligned with years before. The environment is brutal and one needs to be very pragmatic to avoid many pitfalls. Next time you look at the source code of a large project and see something that makes no sense to you, remember this tweet from Nicholas Zakas:

How was Yahoo!’s transition to Atomic CSS received internally?

TK: ACSS was well accepted by our My Home Page team, but it didn’t go well outside of that. Our first interaction was with the Sports team based in Santa Monica. Steve and I were in a conference call trying to convince the developers that not following the Separation of Concern’ principle was the way to go and that it would not create chaos.

We pointed them to a piece that Nicolas Gallagher had recently written, thinking that an article from an “outsider” would help, but nope! Things didn’t go well and there was a lot of friction. The main issue was the fact that the library was made of utility classes, but its syntax did not help to ease the conversation.

I recall also meeting with the Mail team who didn’t push back on the idea of Atomic CSS, but wanted to come up with their own JavaScript approach to use “plain” CSS declarations — as they could not stand the ACSS syntax. In any case, the data in favor of the library (~36% less CSS and HTML) was speaking for itself, so ACSS was eventually adopted. And today, seven-plus years later, Yahoo! Home Page, Yahoo! Sports, Yahoo! News, Yahoo! Finance, and other Yahoo! Products are all still using ACSS.

To better understand how an approach like ACSS can benefit projects where component reusability is paramount, copy the markup of a component from Yahoo! Finance and paste it inside Yahoo! News. That component should look like it belongs to the page. This is because ACSS makes these components page agnostic.

How did the idea of using parentheses for class names manifest? Was the syntax inspired from how functions are written?

TK: We had identified — through many iterations — two sets of “candidates” to be used as delimiters for property values: parentheses, (), and brackets, [].

Renato remembers that we picked parentheses over brackets because of familiarity with functions in JavaScript, even if it came at the cost of an extra Shift keystroke. The ACSS syntax was designed to:

  • facilitate the automatic generation of rules, via Atomizer
  • allow developers to create any arbitrary or complex styles they want
  • reduce the learning curve to a minimum

It looks like this:

[<context>[:<pseudo-class>]<combinator>]<Style>[(<value>,<value>?,...)][<!>][:<pseudo-class>][::<pseudo-element>][--<breakpoint_identifier>]

Developers build their classes following the above construct. The core syntax is based on Emmet, a popular toolkit. We adopted the Emmet approach to reduce idiosyncrasies as core classes are explicit property/value pairs rather than arbitrary strings.

We also created a dozen of helper classes. Those apply multiple style declarations and are either shortcuts, like hiding content from sighted users, or hacks, like using .Cf for clearfix. And we gave developers even more latitude through the use of a config file in which they can create variables — like .PrimaryColor — breakpoints, and much more.

People who’ve never worked with ACSS will tell you that the syntax is too weird (at best), but people familiar with it will tell you it’s clever in many ways.

Talk about how your “Challenging CSS Best Practices” article for Smashing Magazine came to fruition?

TK: I had written many articles in various online publications before, so it was natural for me to write an article about this “challenging” approach.

Yahoo! was sponsoring a front-end conference in October 2013 where Renato had a talk scheduled to present our solution, and I was trying to get the article published before that. I chose to not publish it on Yahoo! Developer Network because the website did not offer a comment section. A List Apart could not publish it in time, but Smashing Magazine accelerated its review process to be able to publish the piece before the end of October.

My choice of going with a publisher who had a comment section paid off as the article received 200-plus comments which turned out to be very time consuming — and frustrating — for me who had to respond to them.

Does it feel strange that the article still carries the disclaimer about the techniques discussed, even though it is widely popular in the industry now?

TK: When the article was published, I told Vitaly [Friedman, Smashing Magazine co-founder] that that note sounded like some type of a disclaimer to me; that it would sway people in their reading of the article. But I didn’t really push back as I understood where Vitaly was coming from. I do find it amusing that note is still there now this methodology has become mainstream.

Given that hindsight is 20/20, is there anything that you want to change about Atomic CSS?

TK: There is always room for improvement, even more so when you’ve pioneered the solution. You can’t look at what others have done to learn from their mistakes or shortcomings. You don’t have material to improve upon. So, it’d be pretentious for us to think we nailed it on our first try.

On the Atomic CSS side, we had a lot of experience for having developed and used a “static” stylesheet on a large project for more than a year. But on the dynamic side — the tooling side — it’s not like we could find much inspiration out there. Remember that it took six years for other libraries to follow suit.

In French, we say: essuyer les plâtres.

One mistake we made was to use “Atomic CSS” as the title for acss.io, because as John Polacek pointed out, it created some confusion. We’ve changed that title since then.

The only regret I have is how the community has treated Atomic CSS/ACSS through the years, which recently lead to a weird exchange, where somebody explained to me what “Atomic CSS” means:

The Atomic CSS library [ACSS] uses the name but I think this is misleading, because the feature you’re talking about is the dynamic style generation. “Atomic CSS” as a generic term designates small selectors as atoms, but they’re static.

Talk about being erased. 😉


A big thanks to Thierry for participating in this interview and allowing us to publish it for the community.


The Making of Atomic CSS: An Interview With Thierry Koblentz originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , ,
[Top]

Making a Site Work Offline Using the VitePWA Plugin

The VitePWA plugin from Anthony Fu is a fantastic tool for your Vite-powered sites. It helps you add a service worker that handles:

  • offline support
  • caching assets and content
  • prompting the user when new content is available
  • …and other goodies!

We’ll walk through the concept of service workers together, then jump right into making one with the VitePWA plugin.

New to Vite? Check out my prior post for an introduction.

Table of Contents

  1. Service workers, introduced
  2. Versioning and manifests
  3. Our first service worker
  4. What about offline functionality?
  5. How service workers update
  6. A better way to update content
  7. Runtime caching
  8. Adding your own service worker content
  9. Wrapping up

Service workers, introduced

Before getting into the VitePWA plugin, let’s briefly talk about the Service Worker itself.

A service worker is a background process that runs on a separate thread in your web application. Service workers have the ability to intercept network requests and do… anything. The possibilities are surprisingly wide. For example, you could intercept requests for TypeScript files and compile them on the fly. Or you could intercept requests for video files and perform an advanced transcoding that the browser doesn’t currently support. More commonly though, a service worker is used to cache assets, both to improve a site’s performance and enable it to do something when it’s offline.

When someone first lands on your site, the service worker the VitePWA plugin creates installs, and caches all of your HTML, CSS, and JavaScript files by leveraging the Cache Storage API. The result is that, on subsequent visits to your site, the browser will load those resources from cache, rather than needing to make network requests. And even on the first visit to your site, since the service worker just pre-cached everything, the next place your user clicks will probably be pre-cached already, allowing the browser to completely bypass a network request.

Versioning and manifests

You might be wondering what happens with a service worker when your code is updated. If your service worker is caching, say, a foo.js file, and you modify that file, you want the service worker to pull down the updated version, the next time a user visits the site.

But in practice you don’t have a foo.js file. Usually, a build system will create something like foo-ABC123.js, where “ABC123” is a hash of the file. If you update foo.js, the next deployment of your site may send over foo-XYZ987.js. How does the service worker handle this?

It turns out the Service Worker API is an extremely low-level primitive. If you’re looking for a native turnkey solution between it and the cache API, you’ll be disappointed. Basically, the creation of your service worker needs to be automated, in part, and connected to the build system. You’d need to see all the assets your build created, hard-code those file names into the service worker, have code to pre-cache them, and more importantly, keep track of the files that are cached.

If code updates, the service worker file also changes, containing the new filenames, complete with hashes. When a user makes their next visit to the app, the new service worker will need to install, and compare the new file manifest with the manifest that’s currently in cache, ejecting files that are no longer needed, while caching the new content.

This is an absurd amount of work and incredibly difficult to get right. While it can be a fun project, in practice you’ll want to use an established product to generate your service worker — and the best product around is Workbox, which is from the folks at Google.

Even Workbox is a bit of a low-level primitive. It needs detailed information about the files you’re pre-caching, which are buried in your build tool. This is why we use the VitePWA plugin. It uses Workbox under the hood, and configures it with all the info it needs about the bundles that Vite creates. Unsurprisingly, there are also webpack and Rollup plugins if you happen to prefer working with those bundlers.

Our first service worker

I’ll assume you already have a Vite-based site. If not, feel free to create one from any of the available templates.

First, we install the VitePWA plugin:

npm i vite-plugin-pwa

We’ll import the plugin in our Vite config:

import { VitePWA } from "vite-plugin-pwa"

Then we put it to use in the config as well:

plugins: [   VitePWA()

We’ll add more options in a bit, but that’s all we need to create a surprisingly useful service worker. Now let’s register it somewhere in the entry of our application with this code:

import { registerSW } from "virtual:pwa-register";  if ("serviceWorker" in navigator) {   // && !/localhost/.test(window.location)) {   registerSW(); }

Don’t let the code that’s commented out throw you for a loop. It’s extremely important, in fact, as it prevents the service worker from running in development. We only want to install the service worker anywhere that’s not on the localhost where we’re developing, that is, unless we’re developing the service worker itself, in which case we can comment out that check (and revert before pushing code to the main branch).

Let’s go ahead and open a fresh browser, launch DevTools, navigate to the Network tab, and run the web app. Everything should load as you’d normally expect. The difference is that you should see a whole slew of network requests in DevTools.

A screenshot of DevTools listing all of the network requests for the currant app using the VitePWA plugin. There are a total of 16 various JavaScript and CSS files.

That’s Workbox pre-caching the bundles. Things are working!

What about offline functionality?

So, our service worker is pre-caching all of our bundled assets. That means it will serve those assets from cache without even needing to hit the network. Does that mean our service worker could serve assets even when the user has no network access? Indeed, it does!

And, believe it or not, it’s already done. Give it a try by opening the Network tab in DevTools and telling Chrome to simulate offline mode, like this.

Screenshot of the DevTools UO to simulate an offline connection with the select menu open. The No throttling option is currently checked but the Offline option is highlighted in light blue.
The “No throttling” option is the default selection. Click that and select the “Offline” option to simulate an offline connection.

Let’s refresh the page. You should see everything load. Of course, if you’re running any network requests, you’ll see them hang forever since you’re offline. Even here, though, there are things you can do. Modern browsers ship with their own internal, persistent database called IndexedDB. There’s nothing stopping you from writing your own code to sync some data to there, then write some custom service worker code to intercept network requests, determine if the user is offline, and then serve equivalent content from IndexedDB if it’s in there.

But a much simpler option is to detect if the user is offline, show a message about being offline, and then bypass the data requests. This is a topic unto itself, which I’ve written about in much greater detail.

Before showing you how to write, and integrate your own service worker content, let’s take a closer look at our existing service worker. In particular, let’s see how it manages updating/changing content. This is surprisingly tricky and easy to mess up, even with the VitePWA plugin.

Before moving on, make sure you tell Chrome DevTools to put you back online.

How service workers update

Take a closer look at what happens to our site when we change the content. We’ll go ahead and remove our existing service worker, which we can do in the Application tab of DevTools, under Storage.

Screenshot showing the Storage panel of DevTools. The DevTools menu is a panel on the left and the app usage is displayed in a panel on the right, showing that 508 kilobytes of data total is used, where 392 kilobytes are cached and 16.4 are service workers. A button to clear site data is below the Usage stats with a deep blue label and a light gray background.

Click the “Clear site data” button to get a clean slate. While I’m at it, I’m going to remove most of the routes of my own site so there’s fewer resources, then let Vite rebuild the app.

Look in the generated sw.js to see the generated Workbox service worker. There should be a pre-cache manifest inside of it. Mine looks like this:

A dark mode screenshot showing a list of eight asset urls inside of a precacheAndRoute function.

If sw.js is minified, run it through Prettier to make it easier to read.

Now let’s run the site and see what’s in our cache:

Let’s focus on the settings.js file. Vite generated assets/settings.ccb080c2.js based on the hash of its contents. Workbox, being independent of Vite, generated its own hash of the same file. If that same file name were to be generated with different content, then a new service worker would be re-generated, with a different pre-cache manifest (same file, but different revision) and Workbox would know to cache the new version, and remove the old when it’s no longer needed.

Again, the filenames will always be different since we’re using a bundler that injects hash codes into our file names, but Workbox supports dev environments which don’t do that.

Since the time writing, the VitePWA plugin has been updated and no longer injects these revision hashes. If you’re attempting to follow along with the steps in this article, this specific step might be slightly different from your actual experience. See this GitHub issue for more context.

If we update our settings.js file, then Vite will create a new file in our build, with a new hash code, which Workbox will treat as a new file. Let’s see this in action. After changing the file and re-running the Vite build, our pre-cache manifest looks like this:

Now, when we refresh the page, the prior service worker is still running and loading the prior file. Then, the new service worker, with the new pre-cache manifest is downloaded and pre-cached.

A DevTools screenshot showing a table of pre-cached assets processed by the VitePWA plugin and Workbox.
The new pre-cached manifest is displayed in the list of cached assets. Notice that both versions of our settings file are there (and both versions of a few other assets were affected as well): the old version, since that’s what’s still being run, and the new version, since the new service worker has pre-cached it.

Note the corollary here: our old content is still being served to the user since the old service worker is still running. The user is unable to see the change we just made, even if they refresh because the service worker, by default, guarantees any and all tabs with this web app are running the same version. If you want the browser to show the updated version, close your tab (and any other tabs with the site), and re-open it.

The same DevTools screenshot of pre-cached assets, but now only displaying new assets instead of duplicates.
The cache should now only contain the new assets.

Workbox did all the legwork of making this all come out right! We did very little to get this going.

A better way to update content

It’s unlikely that you can get away with serving stale content to your users until they happen to close all their browser tabs. Fortunately, the VitePWA plugin offers a better way. The registerSW function accepts an object with an onNeedRefresh method. This method is called whenever there’s a new service worker waiting to take over. registerSW also returns a function that you can call to reload the page, activating the new service worker in the process.

That’s a lot, so let’s see some code:

if ("serviceWorker" in navigator) {   // && !/localhost/.test(window.location) && !/lvh.me/.test(window.location)) {   const updateSW = registerSW({     onNeedRefresh() {       Toastify({         text: `<h4 style='display: inline'>An update is available!</h4>                <br><br>                <a class='do-sw-update'>Click to update and reload</a>  `,         escapeMarkup: false,         gravity: "bottom",         onClick() {           updateSW(true);         }       }).showToast();     }   }); }

I’m using the toastify-js library to show a toast UI component to let users know when a new version of the service worker is available and waiting. If the user clicks the toast, I call the function VitePWA gives me to reload the page, with the new service worker running.

A toast component screenshot with white text and a slight background gradient that goes from light blue on the left to bright blue on the right. It reads: an update is available! Click to update and reload.
Now when we have pending updates, a nice toast component pops up on the front end. Clicking it reloads the page with the new content in there.

One thing to remember here is that, after you deploy the code to show the toast, the toast component won’t show up the next time you load your site. That’s because the old service worker (the one before we added the toast component) is still running. That requires manually closing all tabs and re-opening the web app for the new service worker to take over. Then, the next time you update some code, the service worker should show the toast, prompting you to update.

Why doesn’t the service worker update when the page is refreshed? I mentioned earlier that refreshing the page does not update or activate the waiting service worker, so why does this work? Calling this method doesn’t only refresh the page, but it calls some low-level Service Worker APIs (in particular skipWaiting) as well, giving us the outcome we want.

Runtime caching

We’ve seen the bundle pre-caching we get for free with VitePWA for our build assets. What about caching any other content we might request at runtime? Workbox supports this via its runtimeCaching feature.

Here’s how. The VitePWA plugin can take an object, one property of which is workbox, which takes Workbox properties.

const getCache = ({ name, pattern }: any) => ({   urlPattern: pattern,   handler: "CacheFirst" as const,   options: {     cacheName: name,     expiration: {       maxEntries: 500,       maxAgeSeconds: 60 * 60 * 24 * 365 * 2 // 2 years     },     cacheableResponse: {       statuses: [200]     }   } }); // ...    plugins: [     VitePWA({       workbox: {         runtimeCaching: [           getCache({              pattern: /^https://s3.amazonaws.com/my-library-cover-uploads/,              name: "local-images1"            }),           getCache({              pattern: /^https://my-library-cover-uploads.s3.amazonaws.com/,              name: "local-images2"            })         ]       }     })   ], // ...

I know, that’s a lot of code. But all it’s really doing is telling Workbox to cache anything it sees matching those URL patterns. The docs provide much more info if you want to get deep into specifics.

Now, after that update takes effect, we can see those resources being served by our service worker.

DevTools screenshot showing the resources that are loaded by the browser. There are four jpeg images.

And we can see the corresponding cache that was created.

DevTools screenshot showing the new cache instance that is stored in Cache Storage. It includes all of the cached images.

Adding your own service worker content

Let’s say you want to get advanced with your service worker. You want to add some code to sync data with IndexedDB, add fetch handlers, and respond with IndexedDB data when the user is offline (again, my prior post walks through the ins and outs of IndexedDB). But how do you put your own code into the service worker that Vite creates for us?

There’s another Workbox option we can use for this: importScripts.

VitePWA({   workbox: {     importScripts: ["sw-code.js"],

Here, the service worker will request sw-code.js at runtime. In that case, make sure there’s an sw-code.js file that can be served by your application. The easiest way to achieve that is to put it in the public folder (see the Vite docs for detailed instructions).

If this file starts to grow to a size such that you need to break things up with JavaScript imports, make sure you bundle it to prevent your service worker from trying to execute import statements (which it may or may not be able to do). You can create a separate Vite build instead.

Wrapping up

At the end of 2021, CSS-Tricks asked a bunch of front-end folks what one thing someone cans do to make their website better. Chris Ferdinandi suggested a service worker. Well, that’s exactly what we accomplished in this article and it was relatively simple, wasn’t it? That’s thanks to the VitePWA with hat tips to Workbox and the Cache API.

Service workers that leverage the Cache API are capable of greatly improving the perf of your web app. And while it might seem a little scary or confusing at first, it’s nice to know we have tools like the VitePWA plugin to simplify things a great deal. Install the plugin and let it do the heavy lifting. Sure, there are more advanced things that a service worker can do, and VitePWA can be used for more complex functionality, but an offline site is a fantastic starting point!


Making a Site Work Offline Using the VitePWA Plugin originally published on CSS-Tricks. You should get the newsletter and become a supporter.

CSS-Tricks

, , , , , ,
[Top]

Making Tables With Sticky Header and Footers Got a Bit Easier

It wasn’t long ago when I looked at sticky headers and footers in HTML <table>s in the blog post A table with both a sticky header and a sticky first column. In it, I never used position: sticky on any <thead>, <tfoot>, or <tr> element, because even though Safari and Firefox could do that, Chrome could not. But it could do table cells like <th> and <td>, which was a decent-enough workaround.

Well that’s changed.

Sounds like a big effort went into totally revamping tables in the rendering engine in Chromium, bringing tables up to speed. It’s not just the stickiness that was fixed, but all sorts of things. I’ll just focus on the sticky thing since that’s what I looked at.

The headline to me is that <thead> and <tfoot> are sticky-able. That seems like it will be the most common use case here.

table thead, table tfoot {   position: sticky; } table thead {   inset-block-start: 0; /* "top" */ } table tfoot {   inset-block-end: 0; /* "bottom" */ }

That works in all three major browsers. You might want to get clever and only sticky them at certain minimum viewport heights or something, but the point is it works.

I heard several questions about table columns as well. My original article had a sticky first column (that was kind of the point). While there is a table <col> tag, it’s… weird. It doesn’t actually wrap columns, it’s more like a pointer thing to be able to style down the column if you need to. I hardly ever see it used, but it’s there. Anyway, you totally can’t position: sticky; a <col>, but you can make sticky columns. You need to select all the cells in that column and stick them to the left or right. Here’s that using logical properties…

table tr th:first-child {   position: sticky;   inset-inline-start: 0; /* "left" */ }

Here’s a sorta obnoxious table where the <thead>, <tfoot>, and the first and last columns are all sticky.

I’m sure you could do something tasteful with this. Like maybe:


The post Making Tables With Sticky Header and Footers Got a Bit Easier appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

Making Disabled Buttons More Inclusive

Let’s talk about disabled buttons. Specifically, let’s get into why we use them and how we can do better than the traditional disabled attribute in HTML (e.g. <button disabled> ) to mark a button as disabled.

There are lots of use cases where a disabled button makes a lot of sense, and we’ll get to those reasons in just a moment. But throughout this article, we’ll be looking at a form that allows us to add a number of tickets to a shopping cart.

This is a good baseline example because there’s a clear situation for disabling the “Add to cart” button: when there are no tickets to add to the cart.

But first, why disabled buttons?

Preventing people from doing an invalid or unavailable action is the most common reason we might reach for a disabled button. In the demo below, we can only add tickets if the number of tickets being added to the cart is greater than zero. Give it a try:

Allow me to skip the code explanation in this demo and focus our attention on what’s important: the “Add to cart” button.

<button type="submit" disabled="disabled">   Add to cart </button>

This button is disabled by the disabled attribute. (Note that this is a boolean attribute, which means that it can be written as disabled or disabled="disabled".)

Everything seems fine… so what’s wrong with it?

Well, to be honest, I could end the article right here asking you to not use disabled buttons because they suck, and instead use better patterns. But let’s be realistic: sometimes disabling a button is the solution that makes the most sense.

With that being said, for this demo purpose, we’ll pretend that disabling the “Add to cart” button is the best solution (spoiler alert: it’s not). We can still use it to learn how it works and improve its usability along the way.

Types of interactions

I’d like to clarify what I mean by disabled buttons being usable. You may think, If the button is disabled, it shouldn’t be usable, so… what’s the catch? Bear with me.

On the web, there are multiple ways to interact with a page. Using a mouse is one of the most common, but there are others, like sighted people who use the keyboard to navigate because of a motor impairment.

Try to navigate the demo above using only the Tab key to go forward and Tab + Shift to go backward. You’ll notice how the disabled button is skipped. The focus goes directly from the ticket input to the “dummy terms” link.

Using the Tab key, it changes the focus from the input to the link, skipping the “Add to cart” button.

Let’s pause for a second and recap the reason that lead us to disable the button in the first place versus what we had actually accomplished.

It’s common to associate “interacting” with “clicking” but they are two different concepts. Yes, click is a type of interaction, but it’s only one among others, like hover and focus.

In other words…

All clicks are interactions, but not all interactions are clicks.

Our goal is to prevent the click, but by using disabled, we are preventing not only the click, but also the focus, which means we might be doing as much harm as good. Sure, this behavior might seem harmless, but it causes confusion. People with a cognitive disability may struggle to understand why they are unable to focus the button.

In the following demo, we tweaked the layout a little. If you use a mouse, and hover over the submit button, a tooltip is shown explaining why the button is disabled. That’s great! But if you use only the keyboard, there’s no way of seeing that tooltip because the button cannot be focused with disabled. Ouch!

Using the mouse, the tooltip on the “Add to cart” button is visible on hover. But the tooltip is missing when using the Tab key.

Allow me to once again skip past the code explanation. I highly recommend reading “Inclusive Tooltips” by Haydon Pickering and “Tooltips in the time of WCAG 2.1” by Sarah Higley to fully understand the tooltip pattern.

ARIA to the rescue

The disabled attribute in a <button> is doing more than necessary. This is one of the few cases I know of where a native HTML attribute can do more harm than good. Using an ARIA attribute can do a better job, allowing us to add not only focus on the button, but do so consistently to create an inclusive experience for more people and use cases.

The disabled attribute correlates to aria-disabled="true". Give the following demo a try, again, using only the keyboard. Notice how the button, although marked disabled, is still accessible by focus and triggers the tooltip!

Using the Tab key, the “Add to cart” button is focused and it shows the tooltip.

Cool, huh? Such tiny tweak for a big improvement!

But we’re not done quite yet. The caveat here is that we still need to prevent the click programmatically, using JavaScript.

elForm.addEventListener('submit', function (event) {   event.preventDefault(); /* prevent native form submit */    const isDisabled = elButtonSubmit.getAttribute('aria-disabled') === 'true';    if (isDisabled || isSubmitting) {     // return early to prevent the ticket from being added     return;   }    isSubmitting = true;   // ... code to add to cart...   isSubmitting = false; })

You might be familiar with this pattern as a way to prevent double clicks from submitting a form twice. If you were using the disabled attribute for that reason, I’d prefer not to do it because that causes the temporary loss of the keyboard focus while the form is submitting.

The difference between disabled and aria-disabled

You might ask: if aria-disabled doesn’t prevent the click by default, what’s the point of using it? To answer that, we need to understand the difference between both attributes:

Feature / Attribute disabled aria-disabled="true"
Prevent click
Prevent hover
Prevent focus
Default CSS styles
Semantics

The only overlap between the two is semantics. Both attributes will announce that the button is indeed disabled, and that’s a good thing.

Contrary to the disabled attribute, aria-disabled is all about semantics. ARIA attributes never change the application behavior or styles by default. Their only purpose is to help assistive technologies (e.g. screen readers) to announce the page content in a more meaningful and robust way.

So far, we’ve talked about two types of people, those who click and those who Tab. Now let’s talk about another type: those with visual impairments (e.g. blindness, low vision) who use screen readers to navigate the web.

People who use screen readers, often prefer to navigate form fields using the Tab key. Now look an how VoiceOver on macOS completely skips the disabled button.

The VoiceOver screen reader skips the “Add to cart” button when using the Tab key.

Once again, this is a very minimal form. In a longer one, looking for a submit button that isn’t there right away can be annoying. Imagine a form where the submit button is hidden and only visible when you completely fill out the form. That’s what some people feel like when the disabled attribute is used.

Fortunately, buttons with disabled are not totally unreachable by screen readers. You can still navigate each element individually, one by one, and eventually you’ll find the button.

The VoiceOver screen reader is able to find and announce the “Add to cart” button.

Although possible, this is an annoying experience. On the other hand, with aria-disabled, the screen reader will focus the button normally and properly announce its status. Note that the announcement is slightly different between screen readers. For example, NVDA and JWAS say “button, unavailable” but VoiceOver says “button, dimmed.”

The VoiceOver screen reader can find the “Add to cart” button using Tab key because of aria-disabled.

I’ve mapped out how both attributes create different user experiences based on the tools we just used:

Tool / Attribute disabled aria-disabled="true"
Mouse or tap Prevents a button click. Requires JS to prevent the click.
Tab Unable to focus the button. Able to focus the button.
Screen reader Button is difficult to locate. Button is easily located.

So, the main differences between both attributes are:

  • disabled might skip the button when using the Tab key, leading to confusion.
  • aria-disabled will still focus the button and announce that it exists, but that it isn’t enabled yet; the same way you might perceive it visually.

This is the case where it’s important to acknowledge the subtle difference between accessibility and usability. Accessibility is a measure of someone being able to use something. Usability is a measure of how easy something is to use.

Given that, is disabled accessible? Yes. Does it have a good usability? I don’t think so.

Can we do better?

I wouldn’t feel good with myself if I finished this article without showing you the real inclusive solution for our ticket form example. Whenever possible, don’t use disabled buttons. Let people click it at any time and, if necessary, show an error message as feedback. This approach also solves other problems:

  • Less cognitive friction: Allow people to submit the form at any time. This removes the uncertainty of whether the button is even disabled in the first place.
  • Color contrast: Although a disabled button doesn’t need to meet the WCAG 1.4.3 color contrast, I believe we should guarantee that an element is always properly visible regardless of its state. But that’s something we don’t have to worry about now because the button isn’t disabled anymore.

Final thoughts

The disabled attribute in <button> is a peculiar case where, although highly known by the community, it might not be the best approach to solve a particular problem. Don’t get me wrong because I’m not saying disabled is always bad. There are still some cases where it still makes sense to use it (e.g. pagination).

To be honest, I don’t see the disabled attribute exactly as an accessibility issue. What concerns me is more of a usability issue. By swapping the disabled attribute with aria-disabled, we can make someone’s experience much more enjoyable.

This is yet one more step into my journey on web accessibility. Over the years, I’ve discovered that accessibility is much more than complying with web standards. Dealing with user experiences is tricky and most situations require making trade-offs and compromises in how we approach a solution. There’s no silver bullet for perfect accessibility.

Our duty as web creators is to look for and understand the different solutions that are available. Only then we can make the best possible choice. There’s no sense in pretending the problems don’t exist.

At the end of the day, remember that there’s nothing preventing you from making the web a more inclusive place.

Bonus

Still there? Let me mention two last things about this demo that I think are worthy:

1. Live Regions will announce dynamic content

In the demo, two parts of the content changed dynamically: the form submit button and the success confirmation (“Added [X] tickets!”).

These changes are visually perceived, however, for people with vision impairments using screen readers, that just ain’t the reality. To solve it, we need to turn those messages into Live Regions. Those allow assistive technologies to listen for changes and announce the updated messages when they happen.

There is a .sr-only class in the demo that hides a <span> containing a loading message, but allows it to be announced by screen readers. In this case, aria-live="assertive" is applied to the <span> and it holds a meaningful message after the form is submitting and is in the process of loading. This way, we can announce to the user that the form is working and to be patient as it loads. Additionally, we do the same to the form feedback element.

<button type="submit" aria-disabled="true">   Add to cart   <span aria-live="assertive" class="sr-only js-loadingMsg">      <!-- Use JavaScript to inject the the loading message -->   </span> </button>  <p aria-live="assertive" class="formStatus">   <!-- Use JavaScript to inject the success message --> </p>

Note that the aria-live attribute must be present in the DOM right from the beginning, even if the element doesn’t hold any message yet, otherwise, Assistive Technologies may not work properly.

Form submit feedback message being announced by the screen reader.

There’s much more to tell you about this little aria-live attribute and the big things it does. There are gotchas as well. For example, if it is applied incorrectly, the attribute can do more harm than good. It’s worth reading “Using aria-live” by Ire Aderinokun and Adrian Roselli’s “Loading Skeletons” to better understand how it works and how to use it.

2. Do not use pointer-events to prevent the click

This is an alternative (and incorrect) implementation that I’ve seen around the web. This uses pointer-events: none; in CSS to prevent the click (without any HTML attribute). Please, do not do this. Here’s an ugly Pen that will hopefully demonstrate why. I repeat, do not do this.

Although that CSS does indeed prevent a mouse click, remember that it won’t prevent focus and keyboard navigation, which can lead to unexpected outcomes or, even worse, bugs.

In other words, using this CSS rule as a strategy to prevent a click, is pointless (get it?). 😉


The post Making Disabled Buttons More Inclusive appeared first on CSS-Tricks.

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

CSS-Tricks

, , , ,
[Top]

The Making (and Potential Benefits) of a CSS Font

Not a typical one, at least. Each character is an HTML element, built with CSS. A true web font!

Let me elaborate. This is a way to render text without using any font at all. Random text is split with PHP into words and letters, then rendered as HTML elements with classes. Every element is styled with CSS to create the characters. This is “just” HTML controlled with CSS, but still, it is software and it gets the message through. It has all the properties a conventional font does, so we’ll call it a font. A font without a format.

Disclaimer: I’m not an expert in HTML, CSS, or PHP. I’m willing to bet there is a shortcut or an easier solution to achieve what I’ve done here, but since I’m happy with the results, I will present the process and my experience. The presentation is not a tutorial; it is an experiment based on my limited skills and should be treated as such.

The idea

The project was never meant to last for five months, but that’s what it took! It all started with having a play with a CSS icon, using pseudo-elements to make shapes. Once the first S letter was finished, the rest were relatively easy. I checked to see if there were other similar projects but didn’t find much, so I was motivated to see how far I could get.

Initially, an SVG font controlled with CSS seemed like a good idea. It would make this task much easier (SVG is made for drawing) and could focus on design-specific effects, but it doesn’t have the flexibility of a raw HTML element. An SVG cannot be modified depending on context, and the process falls back to the conventional font design, where every character has a fixed shape and code.

How it works

This is a hybrid of web and font design. Each character is built like any web element and used inline to behave like a font. Metrics, Weights, OpenType Features and all the other font properties are controlled exclusively with the CSS file.

The font design is based on the border width of the elements, which makes it extremely versatile. With the exemption of script fonts, several styles and weights can result just from border variations, using the same shape. On more complex characters the clip-path and the background is used to create the cutout effect.

Nested elements are generated when the ::before and ::after pseudo-element is not enough to form a character. Using em values for width, height and border widths will help later in controlling the font size. This is one of the golden rules.

A character (left) is built like any CSS icon (right). There are no major differences. Sometimes a letter is easier to build, just like a stickman, based on circles and lines. But here is when you can really appreciate the role of the border-radius property. Personally, I never was a fan of rounded borders, but this experience changed my mind. Basically, there’s no limit for what a radius can do.

Below are the only two “real” examples of the CSS font in this article, the rest of the example figures are converted to SVG for easier display in a blog post.

1. Gray – main shape
2. Red – pseudo elements
3. Blue – Extra element

Most of the times you have to insist for the desired effect, but in the end you want to carry this icon in your wallet. Icon created with a single shape, the pseudo-element is used only to avoid the rotation of the main element, in case it is used inline.

The serif preview presents a more complex situation, but as usual, a sans font will have fewer elements to deal with, making the file is smaller and load faster. This is not really an issue and it’s only logical — the CSS is read before a font embedded with the @font-face rule.

The challenge

Naive start, putting all the math and the logic to use. None of the values work if a pixel decides so. The final solution in this case was to include a second parent that will keep the “children” tidy.

The hardest part is to beat the pixel ratio, or align the pseudo elements to the base shape. Elaborated mathematical formulas failed when the charater was resized. A browser will treat each element separately and shift them to the closest integer value.

A solution to this was to create as many pseudo-elements as possible (even including extra elements), and use a single reference for a pair of ::before and ::after, not related to the main shape. In this case, the browser will render the elements more or less to the same position.

A character without a point of reference is illustrated with the S letter below. The top and bottom section of the letter are two pseudo elements, without a base shape to rely on (e.g. the gray area in the serif above or here in the digit two).

After creating a few hundred characters, you realize that a character cannot support inline transformation (i.e. skew(), rotate(), and such) because it won’t align to siblings. This becomes more obvious visually on text selection. Because of that, a pseudo-element makes perfect sense. I would say essential: the second golden rule.

The graphic shows the basic principle of construction. When apertures cannot be controlled simply with the {border: 0;} rule, clip-path is used.

In this case, the S letter needs a smaller aperture, so the border eventually is kept and cut at the desired distance with clip-path.

CSS custom properties

It seems easier to create a style in CSS than it is in font softwares. You have the option to control shapes and sizes for multiple characters at once. In CSS, more characters are grouped together in the same ruleset.

CSS custom properties are extremely handy in this situation, especially for controlling borders, widths, and positions. The different weights are the result of changes in the variable, with adjustments afterwards. Fine tuning is unavoidable because character shapes and sizes take the border width into account and may not display proportionally with different borders, especially on asymmetrical shapes.

Cutout effect created by adding the same background color to the overlaying element. Combination of colors and effects using the mix-blend-mode.

The cutout effect is created by adding the same background color to the overlaying element, then using a combination of colors and effects using mix-blend-mode.

A global color variable is required in CSS to create a cutout effect for nested elements that otherwise would follow the parent color (overlaying elements match the background).

The background-image property won’t work on characters built exclusively with borders and the background is changed if the element has size or position transformations (scale, rotate, or other).

Where a background cannot be used, the solution is mix-blend-mode: lighten; on dark backgrounds and mix-blend-mode: darken; on light backgrounds.

Side effects

The downside is that some effects can have unexpected or even opposite results on elements with variable properties. Usually, a filter will read elements as full objects. To prevent any conflict, borders and transformation effects are reserved for the font design.

Font to text

A font won’t make a text. The idea in the first place was to create a text that will load along with the CSS, without any dependencies. For that the best option is PHP (my rookie opinion). Besides rendering HTML with inline functions, it is up to almost any task imaginable. Without PHP this project would not be possible.

Naturally, the first task with PHP was to split a random text, remove extra spaces and create matching groups for every word and letter, each one with its own class. So far, so good. I won’t insist on the part that went smoothly, it is a basic function, using split, explode and all the other words borrowed from a video game.

Still, since I never worked on this before, I had to learn the hard way. Nobody told me that PHP considers the “0” (zero) as null, so there’s a day gone. I couldn’t figure out why my zeros are not displayed.

For anyone with this issue maybe it’s helpful. Instead of using the empty() function, I used the one below:

function is_blank( $  value ) {     return empty( $  value ) && !is_numeric( $  value ); }

The other major issue was the character range. It seems that there are way too many settings in HTML, the .htaccess file, and on the server itself just to recognize special characters. The solution was found after a few days in the PHP Documentation, posted by qeremy [atta] gmail [dotta] com, obviously somebody living in a diacritic-heavy area.

function str_split_unicode( $  str, $  length = 1 ) {   $  tmp = preg_split( '~~u', $  str, -1, PREG_SPLIT_NO_EMPTY );   if ( $  length > 1 ) {     $  chunks = array_chunk( $  tmp, $  length );     foreach ( $  chunks as $  i => $  chunk ) {         $  chunks[$  i] = join( '', ( array ) $  chunk );     }     $  tmp = $  chunks;   }   return $  tmp; }

A lot of chunks, if you ask me, but it works like a charm and solves every issue. The function basically overlooks the language settings and will read any character, even the non-standard ones. Characters buried deep in the Unicode tables will be recognized if the PHP function includes that character.

This function will only create the possibility to generate each character as typed, without the need for HTML entities. This option won’t limit the use of text in HTML format, but inline codes must be avoided or replaced with alternatives. For example, instead of using non-breaking spaces (&nbsp;), elements can be wrapped in the <nobr> tag.

The HTML entities are not decoded, which is an advantage from my point of view.
Default system font on the left. On the right is CSS text rendered without any changes.

Structure

With this solved, the next step is to create a specific structure for each character. The class of the HTML elements and the positions of the nested element depends on a long list of characters that correspond with one or more classes. Some of the most basic characters are not excluded from this list (e.g. the small “a” letter needs a finial and that means an extra element/class).

The basic structure looks something like this, just to get the idea …

'Ć' => 'Cacute C acute'

…which will render three elements: the parent Cacute, the C letter, and the acute accent.The result is below, where the red square represents the parent element, containing the other two preset elements.

Cacute — a classic combination between a basic letter and an accent.

The technique is very similar to the way diacritics (sometimes ligatures) are built in font software, based on pairings. When a component element is changed, every other will adjust.

Because any element can have multiple applications, the IDs are avoided and only classes are used.

OpenType features

The PHP function is set to behave differently depending on context. The character recognition is set to replace pairings and create ligatures when rendering the CSS text.

Contextual ligatures in the CSS text are not standalone characters and don’t have specific classes. Different from conventional OpenType features, the characters are restyled, not replaced. The interaction is controlled in CSS by styling the second element, to merge or form a new character.

The features are activated with a specific class added to the parent container. Alternates are rendered in any circumstance, regardless if a character is registered or not, in every browser, with or without font feature support.

Example of the Ordinal Indicator Feature, activated with the .ordn class. The characters are styled inline and change their look according to class and their previous sibling. The same recipe is applied for Stylistic Alternates (salt), Oldstyle Figures (onum), Slashed Zeros (slsh), Superscripts (sups), Subscripts (subs) and Fractions (frac).
Classes with similar functions to OpenType are named after the Registered Features by Microsoft/Adobe.

HTML syntax

Any HTML element can include the CSS font, as long it has the .css class next to the weight of the font. To select a weight, the .thin, .light, .regular or .bold class is used, something like <pre class="regular css"> (the <pre> tag is just a safety measure to avoid any style interference).

Available weights: Thin, Light, Regular and Bold.

The text can have an HTML format. The plain text is not mandatory.

PHP will ignore a bracket (<) if this has a closing correspondent, which means that every HTML tag in a text will remain active and only the text content is rendered as the CSS font. URLs, file paths, or other additional info found in the tag are encoded just the same by the browser. The same tag can style groups of letters or entire sentences, if they’re set in CSS.

Also, depending on layout preferences, specific tags — like <a>, <u>, <ins>, and <del> — can be treated as objects to emulate and customize their native appearance and behavior.

Setup

CSS text is a group of objects with borders, open for size and color treatments. Think color as border-color and vice-versa. :first-child instead of :first-letter.

The font-size is set in the CSS file, the same as any other font, using viewport, percentage, pixels, em or rem units. Values set in pixels work with decimal values.

The text-align and text-indent properties work by default. The text will align to any setup even without text content.

Block-level elements (e.g. <div>, <p>, <ol>) placed inside texts will cause a line break, as it would normally. The <br> tag works as expected.

Except for text formatting elements (e.g. <h1><h6>, <strong>, <em>, <small>, <sup>, <sub>, etc.) that will need new rules to have the right effect on the text, most of the semantic elements (e.g. <form>, <ol>, <li>) work with their custom settings.

The font

To test the font in dynamic content, part of the PHP function was reproduced in JavaScript, with paste, mouse events, caret positions, and text selection. A single keystroke now makes it all worthwhile.

The CSS font and the complementary icons. This is what actually started the whole thing!

Review! Pluses (+) vs. Minuses (-)

Instant load

In the absence of actual text, the browser doesn’t wait for a font and a script to render the page. The CSS file along with HTML elements are cached, which means faster loads.

Universal

Every browser and server recognizes CSS. Fewer worries to find the right format that works the same in every browser. A server will not check for a specific format to allow access.

No dependencies

The CSS font doesn’t need alternate or system fonts to display the text. The same CSS that styles the page can include the font. The browser will not display a default font, neither before nor after the page load. The font does not rely on third parties and scripts, and the design is not different on browsers with disabled scripts.

No embedding

The CSS font is fully integrated into a webpage, and adapts to the layout without replacing other elements on load. Every page property is automatically valid for the text and this will show up the way it was intended, without after effects or functional issues.

Selective use

The font can be reduced to a limited number of characters. The full version is not required if the layout has a single word or a symbol, for example.

Full security

The actual text is not present on the page, which means that sensitive informations can be easily displayed without the fear of spam or phishing.

SEO friendly

Important information can be included using tag properties, the same way the alt attribute works for images.

Customizable

To build complex characters or functions, the font is open for any HTML element. No need for scripts to get specific details because every word and letter has its own entity and can be styled individually.

Contextual

The font design is not limited to predefined characters, so the style can change depending on context, without creating new characters.

Consistent

To compensate for the lack of automation found in font softwares, in CSS, the design can control several elements at once. This argument is valid, since a font software works with existing content, while CSS works with properties, creating a template for every existing or future elements.

Public

Anyone can create their own font. Short texts can be rendered manually, and the PHP function is not a requirement.

Basic

The design is accessible with any text editor or developer tool. Elementary skills using border widths, border radius, shapes and sizes is enough to redesign any character.

Live

Every adjustment result is instant. Conversions, exports, uploads or other steps to activate the font are eliminated from the process.

Moderate use

The speed of the page may suffer if a CSS font is generated for extended texts. This technique is only recommended for headlines, titles, excerpts and short paragraphs for that reason.

Common

The CSS font will not benefit from special treatments because, to the browser, this is just another HTML element. As a result, there’s no optimization or kerning support. The pixels have a hard time sharing thin lines and at small sizes the font may display improperly.

Hard coded

Your usual font settings are unavailable by default and styling tags (e.g. <strong>, <em>, etc.) have no effect. The functions must be set in the CSS file and require a different approach, working with HTML elements instead of fonts.

Exclusive

This is a webfont, so it is limited to digital media controlled with CSS. Except for some bitmap effects, the font can only be translated for offline by printing the document as a PDF, which will convert the CSS into a vector format.

Abstract

Without a standalone file the font is hard to be identified, tested, or transferred. It works like the HTML color: it’s invisible until it’s generated.

Not selectable

Without extra scripts, the text cannot be selected or used in inputs and textareas. For dynamic content, the function needs the whole character recognition found in PHP.

Not interactive

The most common display functions, such as sort or filter, will have to work with classes, and not with text content.

Not printable

The online print supports only basic CSS rules, sometimes ignoring graphics in favor of texts. The print quality will rely strictly on the browser’s capabilities.

No accessibility

The CSS font will adjust to page zoom, but the font size and the languages cannot be changed through the browser.
Custom browser functions (e.g. Find, Reader) cannot access the text content since there is none.

Limited design

There is no wide selection of styles to choose from and the design is limited to the capabilities of CSS. A CSS rule can have different meanings to different browsers, causing inconsistencies. A CSS font is written, not drawn, so the “hand-made” concept is eliminated completely.

Multitask

You need to know your CSS to make adjustments in the font, and vice versa. The design process is not automated, and some properties that are otherwise generated by a machine must be set manually.

No protection

The design code is accessible to anyone, the same as any online element. The design cannot be really protected from unauthorized copying and usage.


Thanks for reading! Here’s the fonts homepage.


The post The Making (and Potential Benefits) of a CSS Font appeared first on CSS-Tricks.

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

CSS-Tricks

, , ,
[Top]

Considerations for Making a CSS Framework

Around eight months ago, I started building a framework which would eventually go on to become Halfmoon. I made a post on this very website announcing the launch of the very first version. Halfmoon has been billed as a Bootstrap alternative with a built-in dark mode feature, that is especially good when it comes to building dashboards and tools. All of this still applies to the framework.

However, today I would like to talk about an area of the framework that is a bit understated. I believe our industry as a whole seriously underestimates the value of customization and user personalization, i.e. users being able to set their own design preferences. Chris has written before about knowing who a design system is made for, pointing out a spectrum of flexibility depending on who a system is meant to help.

But it’s more than design systems. Let’s talk about how Halfmoon addresses these issues because they’re important considerations for knowing which framework works best for your specific needs.

Dashboard built using Halfmoon

Who is Halfmoon for?

Before diving in, let’s address an important question: Is Halfmoon the right framework for you? Here’s a list of questions to help you answer that:

  • Are you building a dashboard, tool, or even a documentation website? Halfmoon has many unique components and features that are specific to these use cases.
  • Are you familiar with Bootstrap’s class names, but wish that the design was a bit more premium-looking?
  • Does your users want or expect a dark mode on your website?
  • Do you dislike dependencies? Halfmoon does not use jQuery, and also has no build process involving CSS preprocessors. Everything is pure, vanilla CSS and JavaScript.
  • Are you tired of dealing with complex build systems and front-end tooling? This ties in to the previous point. Personally, I find it difficult to deal with front-end tooling and build processes. As mentioned above, Halfmoon has no build process, so you just pull in the files (local, CDN, or npm), and start building.

If you answered yes to any (or all) of these questions, you should probably give Halfmoon a try. It is important to note however, that Halfmoon is not a UI component library for React/Vue/Angular, so you shouldn’t go into it expecting that. Moreover, if you are more fond of purely utility driven development, then Tailwind CSS is a better option for you. When it comes to CSS utilities, Halfmoon takes a middle of the road approach – there are utilities plus semantic classes for common components.

Using CSS custom properties

First, let’s get the easy stuff out of the way. CSS custom properties are incredible, and I expect them to completely replace preprocessor variables in the future. Browser support is already at a solid ~96%, and with Internet Explorer being phased out by Microsoft, they are expected to become a standard feature.

Halfmoon is built entirely using CSS variables because they provide a huge degree of customization. Now, you might immediately think that all this means is that there are a few custom properties for colors sprinkled in there, but it’s more than that. In fact, there are over 1,500 global variables in Halfmoon. Almost everything can be customized by overriding a property. Here’s a nifty example from the docs:

Halfmoon customization using CSS variables
Swapping out a few custom property values opens up a ton of possibilities in Halfmoon, whether it’s theming things for a brand, or tweaking the UI to get just the right look.

That’s what we’re talking about here when it comes to customization: does the system still stand up and work well if the person using it overrides anything. I have written extensively about this (and much more) in the official Halfmoon docs page.

Variables aren’t a new concept to frameworks. Many frameworks actually use Sass or Less variables and have done so for quite a while. That’s still a good and effective way to establish a customizable experience. But at the same time, those will lock into a preprocessor (which, again, doesn’t have to be a bad thing). By relying instead on CSS custom properties — and variable-izing all the things — we are relying on native CSS, and that doesn’t require any sort of build dependency. So, not only can custom properties make it easier to customize a framework, but they are much more flexible in terms of the tech stack being used.

There is a balance to be had. I know I suggested creating variables for everything, but it can be equally tough to manage and maintain scores and scores of variables (just like anything else in the codebase). So, lean heavily on variables to make a framework or design system more flexible, but also be mindful of how much flexibility you need to provide and whether adding another variable is part of that scope.

Deciding what components to include

When it comes to building a CSS framework, deciding what components to include is a big part of that ordeal. Of course, for a developer working on a passion project, you want to include everything. But that is simply not feasible, so a few decisions were made on my part.

As of right now, Halfmoon has most of the components you can find in similar frameworks such as Bootstrap or Bulma. These frameworks are great and widely used, so they are a good frame of reference. However, as I have mentioned already, a unique thing about Halfmoon is the focus on building tools and dashboards on the web. This niche, if you could call it that, has led me to build some unique components and features:

  • 5 different types of sidebars, with built-in toggle and overlay handlers. Sidebars are very important for most dashboards and tools (and a pain to get right), so this was a no brainer.
  • 2 different types of navbars. There is one that sticks to the bottom of the page, which can be used to great effect for action buttons. Think about the actions that pop up when you select items on data-table. You could place those action buttons here.
  • Omni-directional dropdowns (with 12 different placements, 3 for each direction).
  • Beautiful form components.
  • Built-in keyboard shortcut system, with an easy way to declare new ones for your tool.
  • Tons of utilities. Of course, this is not comparable to Tailwind CSS, but Halfmoon has enough responsive utility classes to handle a lot of use cases right out of the box.

Moreover, the built-in dark mode, huge customizability, and the standard look and feel to the components, should all work together to make Halfmoon a great tool for building web tools and dashboards. And I am hopefully nowhere close to being done! The next updates will bring in a form validator (demo video), more form components, multi-select component, date and time picker, data-table component, etc.

So what is exactly missing from Halfmoon? Well the most obvious ones are tabs, list group, and spinners. But all of these are planned to be added in v1.2.0, which is the next update. There are also other missing components such as carousels, tree navigation, avatars, etc, which are slightly out of scope.

Providing user preferences

Giving end users the ability to set their preferences is often overlooked by frameworks. Things like setting the font size of an article, or whether to use a dark or light theme. In some ways, it’s sort of funny, because the web is catching up to what operating systems have allowed users to do for decades.

Here are some examples of user personalization on the web:

  1. Being able to select your preferred color mode. And, even better, the website automatically saves and respects your preference when the page is loaded. Or better yet, looking at your operating system preferences and automatically accommodating them.
  2. Setting the default size of elements. Especially font size. A small font might look good in a design, but allowing users to set their ideal font size makes the content actually readable. Technically, every modern browser has an option to zoom into content, but that is often unwieldy, and does not actually save your settings.
  3. Setting the compactness of elements. For example, some people prefer large padding with rounded corners, while others find it a waste of space, instead preferring a tighter UI. Sort of like how Gmail lets you decide whether you want a lot of breathing room in your inbox or make it as small and tight as possible to see more content.
  4. Setting the primary color on the website. While this is entirely cosmetic, it is still charming to be able to set your favorite color on every button and link on a website.
  5. Enabling a high contrast mode. Someone pointed this out to me on GitHub. Apparently, many (and I mean many) CSS frameworks often fail the minimum contrast recommended between foreground and background colors on common elements, such as buttons. That list includes Halfmoon. This is often a tradeoff, because overly contrastive elements often look worse (purely in terms of aesthetic). User personalization can allow you to turn on a high contrast mode, if you have difficulty with the default contrast.

Allowing for user personalizations can be really difficult to pull off — especially for a framework — because that would could mean swapping out huge parts of CSS to accommodate the different personalization settings and combinations. However, with a framework like Halfmoon (i.e. built entirely using CSS variables), this becomes trivial as CSS variables can be set and changed on run-time using JavaScript, like so:

// Get the <html> tag (for reading and setting variables in global scope) var myElement = document.documentElement;  // Read CSS variable getComputedStyle(myElement).getPropertyValue("--variable-name");  // Set CSS variable myElement.style.setProperty("--variable-name", "value");

Therefore, user personalization can be implemented using Halfmoon in the following way:

  • The user sets a preference. That basically means a variable value gets changed. The variable is set with JavaScript (as shown above), and the new value is stored in a cookie or local storage.
  • When the user comes back to the website, their preferences are retrieved and set using JavaScript (again, as shown above) once the page is loaded.

Here are visual examples to really hammer the point home.

Setting and saving the default font size

In the example above, whenever the range slider is changed, the variable --base-font-size is updated to the slider’s value. This is great for people who prefer larger text. As explained in the previous section, this new value can be saved in a cookie or local storage, and the next time the user visits the website, the user preference can be set on page load.

Setting the compactness of content

Compact theme using CSS variables
Because there are CSS custom properties used as utilities, like spacing and borders, we can remove or override them easily to create a more compact or expanded component layout.

Only two variables are updated in this example to go from an expanded view to a compact one:

  • --content-and-card-spacing changed from 3rem (30px) to 2rem (20px).
  • --card-border-radius changed from 0.4rem (4px) to 0.2rem (2px).

For a real life scenario, you could have a dropdown that asks the user whether they prefer their content to be Default or Compact, and choosing one would obviously set the above CSS variables to theme the site. Once again, this could be saved and set on page load when the user visits the website on their next session.

Wait, but why?

Even with all the examples I have shown so far, you may still be asking why is this actually necessary. The answer is really simple: one size does not fit all. In my estimate, around half of the population prefers a dark UI, while the other half prefers light. Similarly, people have wild variations about the things they like when it comes to design. User personalization is a form of improving the UX, because it lets the user choose what they prefer. This may not be so important on a landing page, but when it comes to a tool or dashboard (that one has to use for a long time to get something done), having a UI that can be personalized is a boon to productivity. And knowing that is what Halfmoon is designed to do makes it ideal for these types of use cases.

Moreover, you know how people often complain that websites made with a certain framework (eg Bootstrap) all look the same? This is a step toward making sure that websites built with Halfmoon will always look distinct, so that the focus is on the website and content itself, and not on the framework that was used to build it.

Again, I am not saying that everything should be allowed to be personalized. But knowing who the framework is for and what it is designed to do helps make it clear what should be personalized.

Looking ahead

I strongly feel that flexibility for customization and accounting for user preferences are often overlooked on the web, especially in the framework landscape. That’s what I’m trying to address with Halfmoon.

In the future, I want to make it a lot easier for developers to implement user preferences, and also promote diversity of design with new templates and themes. That said, here are some things on the horizon for Halfmoon:

  • A form validator (demo video)
  • New components, including range sliders, tabs and spinners
  • High contrast mode user preference
  • Multi-select component (like Select2, only without jQuery)
  • A date and time picker
  • A data-table component
  • A GUI-based form builder
  • More themes and templates

You can, of course, learn more about Halfmoon in the documentation website, and if you want to follow the project, you can give it a star on GitHub.


The post Considerations for Making a CSS Framework appeared first on CSS-Tricks.

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

CSS-Tricks

, ,
[Top]

Making Sense of react-spring

Animation is one of the trickier things to get right with React. In this post, I’ll try to provide the introduction to react-spring I wish I had when I first started out, then dive into some interesting use cases. While react-spring isn’t the only animation library for React, it’s one of the more popular (and better) ones.

I’ll be using the newest version 9 which, as of this writing, is in release candidate status. If it’s not fully released by the time you read this, be sure to install it with react-spring@next. From on what I’ve seen and from what the lead maintainer has told me, the code is incredibly stable. The only issue I’ve seen is a slight bug when used with concurrent mode, which can be tracked in the GitHub repo.

react-spring redux

Before we get to some interesting application use cases, let’s take a whirlwind intro. We’ll cover springs, height animation, and then transitions. I’ll have a working demo at the end of this section, so don’t worry if things get a little confusing along the way. 

springs

Let’s consider the canonical “Hello world” of animation: fading content in and out. Let’s stop for a moment and consider how we’d switch opacity on and off without any kind of animation. It’d look something like this:

export default function App() {   const [showing, setShowing] = useState(false);   return (     <div>       <div style={{ opacity: showing ? 1 : 0 }}>         This content will fade in and fade out       </div>       <button onClick={() => setShowing(val => !val)}>Toggle</button>       <hr />     </div>   ); }

Easy, but boring. How do we animate the changing opacity? Wouldn’t it be nice if we could declaratively set the opacity we want based on state, like we do above, except have those values animate smoothly? That’s what react-spring does. Think of react-spring as our middle-man that launders our changing style values, so it can produce the smooth transition between animating values we want. Like this:

const [showA, setShowA] = useState(false); 
 const fadeStyles = useSpring({   config: { ...config.stiff },   from: { opacity: 0 },   to: {     opacity: showA ? 1 : 0   } });

We specify our initial style values with from, and we specify the current value in the to section, based on our current state. The return value, fadeStyles, contains the actual style values we apply to our content. There’s just one last thing we need…

You might think you could just do this:

<div style={fadeStyles}>

…and be done. But instead of using a regular div, we need to use a react-spring div that’s created from the animated export. That may sound confusing, but all it really means is this:

<animated.div style={fadeStyles}>

And that’s it. 

animating height 

Depending on what we’re animating, we may want the content to slide up and down, from a zero height to its full size, so the surrounding contents adjust and flow smoothly into place. You might wish that we could just copy the above, with height going from zero to auto, but alas, you can’t animate to auto height. That neither works with vanilla CSS, nor with react-spring. Instead, we need to know the actual height of our contents, and specify that in the to section of our spring.

We need to have the height of any arbitrary content on the fly so we can pass that value to react-spring. It turns out the web platform has something specifically designed for that: a ResizeObserver. And the support is actually pretty good! Since we’re using React, we’ll of course wrap that usage in a hook. Here’s what mine looks like:

export function useHeight({ on = true /* no value means on */ } = {} as any) {   const ref = useRef<any>();   const [height, set] = useState(0);   const heightRef = useRef(height);   const [ro] = useState(     () =>       new ResizeObserver(packet => {         if (ref.current && heightRef.current !== ref.current.offsetHeight) {           heightRef.current = ref.current.offsetHeight;           set(ref.current.offsetHeight);         }       })   );   useLayoutEffect(() => {     if (on && ref.current) {       set(ref.current.offsetHeight);       ro.observe(ref.current, {});     }     return () => ro.disconnect();   }, [on, ref.current]);   return [ref, height as any]; }

We can optionally provide an on value that switches measuring on and off (which will come in handy later). When on is true, we tell our ResizeObserver to observe out content. We return a ref that needs to be applied to whatever content we want measured, as well as the current height.

Let’s see it in action.

const [heightRef, height] = useHeight(); const slideInStyles = useSpring({   config: { ...config.stiff },   from: { opacity: 0, height: 0 },   to: {     opacity: showB ? 1 : 0,     height: showB ? height : 0   } });

useHeight gives us a ref and a height value of the content we’re measuring, which we pass along to our spring. Then we apply the ref and apply the height styles.

<animated.div style={{ ...slideInStyles, overflow: "hidden" }}>   <div ref={heightRef}>     This content will fade in and fade out with sliding   </div> </animated.div>

Oh, and don’t forget to add overflow: hidden to the container. That allows us to properly container our adjusting height values.

animating transitions

Lastly, let’s look at adding and removing animating items to and from the DOM. We already know how to animate changing values of an item that exists and is staying in the DOM, but to animate the adding, or removing of items, we need a new hook: useTransition.

If you’ve used react-spring before, this is one of the few places where version 9 has some big changes to its API. Let’s take a look.

In order to animate a list of items, like this:

const [list, setList] = useState([]);

…we’ll declare our transition function like this:

const listTransitions = useTransition(list, {   config: config.gentle,   from: { opacity: 0, transform: "translate3d(-25%, 0px, 0px)" },   enter: { opacity: 1, transform: "translate3d(0%, 0px, 0px)" },   leave: { opacity: 0, height: 0, transform: "translate3d(25%, 0px, 0px)" },   keys: list.map((item, index) => index) });

As I alluded earlier, the return value, listTransitions, is a function. react-spring is tracking the list array, keeping tabs on the items which are added and removed. We call the listTransitions function, provide a callback accepting a single styles object and a single item, and react-spring will call it for each item in the list with the right styles, based on whether it’s newly added, newly removed, or just sitting in the list.

Note the keys section: This allows us to tell react-spring how to identify objects in the list. In this case, I decided to tell react-spring that an item’s index in the array uniquely defines that item. Normally this would be a terrible idea, but for now, it lets us see the feature in action. In the demo below, the “Add item” button adds an item to the end of the list when clicked, and the “Remove last item” button removes the most recently added item from the list. So, if you type in the input box then quickly hit the add button then the remove button, you’ll see the same item smoothly begin to enter, and then immediately, from whatever stage in the animation it’s at, begin to leave. Conversely, if you add an item, then quickly hit the remove button and the add button, the same item will begin to slide off, then abruptly stop in place, and slide right back to where it was.

Here’s that demo

Whew, that was a ton of words! Here’s a working demo, showing everything we just covered in action.

Odds and Ends

Did you notice how, when you slide down the content in the demo, it sort of bounces into place, like… a spring? That’s where the name comes from: react-spring interpolates our changing values using spring physics. It doesn’t simply chop the value changing into N equal deltas that it applies over N equal delays. Instead, it uses a more sophisticated algorithm that produces that spring-like effect, which will appear more natural.

The spring algorithm is fully configurable, and it comes with a number of presets you can take off the shelf — the demo above uses the stiff preset. See the docs for more info.

Note also how I’m animating values inside of translate3d values. As you can see, the syntax isn’t the most terse, and so react-spring provides some shortcuts. There’s documentaiton on this, but for the remainder of this post I’ll continue to use the full, non-shortcut syntax for the sake of keeping things as clear as possible. 

I’ll close this section by calling attention to the fact that, when you slide the content up in the demo above, you’ll likely see the content underneath it get a little jumpy at the very end. This is a result of that same bouncing effect. It looks sharp when the content bounces down and into position, but less so when we’re sliding the content up. Stay tuned to see how we can switch it off. (Spoiler, it’s the clamp property).

A few things to consider with these sandboxes

Code Sandbox uses hot reloading. As you change the code, changes are usually reflected immediately. This is cool, but can wreck havoc on animations. If you start tinkering, and then see weird, ostensibly incorrect behavior, try refreshing the sandbox.

The other sandboxes in this post will make use of a modal. For reasons I haven’t quite been able to figure out, when the modal is open, you won’t be able to modify any code — the modal refuses to give up focus. So, be sure to close the modal before attempting any changes. 

Now let’s build something real

Those are the basic building blocks of react-spring. Let’s use them to build something more interesting. You might think, given everything above, that react-spring is pretty simple to use. Unfortunately, in practice, it can be tricky to figure out some subtle things you need to get right. The rest of this post will dive into many of these details. 

Prior blog posts I’ve written have been in some way related to my booklist side project. This one will be no different — it’s not an obsession, it’s just that that project happens to have a publicly-available GraphQL endpoint, and plenty of existing code that be can leveraged, making it an obvious target.

Let’s build a UI that allows you to open a modal and search for books. When the results come in, you can add them to a running list of selected books that display beneath the modal. When you’re done, you can close the modal and click a button to find books similar to the selection.

We’ll start with a functioning UI then animate the pieces step by step, including interactive demos along the way.

If you’re really eager to see what the final result will look like, or you’re already familiar with react-spring and want to see if I’m covering anything you don’t already know, here it is (it won’t win any design awards, I’m well aware). The rest of this post will cover the journey getting to that end state, step by step.

Animating our modal

Let’s start with our modal. Before we start adding any kind of data, let’s get our modal animating nicely.  Here’s what a basic, un-animated modal looks like. I’m using Ryan Florence’s Reach UI (specifically the modal component), but the idea will be the same no matter what you use to build your modal. We’d like to get our backdrop to fade in, and also transition our modal content.

Since a modal is conditionally rendered based on some sort of “open” property, we’ll use the useTransition hook. I was already wrapping the Reach UI modal with my own modal component, and rendering either nothing, or the actual modal based on the isOpen property. We just need to go through the transition hook to get it animating.

Here’s what the transition hook looks like:

const modalTransition = useTransition(!!isOpen, {   config: isOpen ? { ...config.stiff } : { duration: 150 },   from: { opacity: 0, transform: `translate3d(0px, -10px, 0px)` },   enter: { opacity: 1, transform: `translate3d(0px, 0px, 0px)` },   leave: { opacity: 0, transform: `translate3d(0px, 10px, 0px)` } });

There’s not too many surprises here. We want to fade things in and provide a slight vertical transition based on whether the modal is active or not. The odd piece is this:

config: isOpen ? { ...config.stiff } : { duration: 150 },

I want to only use spring physics if the modal is opening. The reason for this — at least in my experience — is when you close the modal, the backdrop takes too long to completely vanish, which leaves the underlying UI un-interactive for too long. So, when the modal opens, it’ll nicely bounce into place with spring physics, and when closed, it’ll quickly vanish in 150ms.

And, of course, we’ll render our content via the transition function our hook returns. Notice that I’m plucking the opacity style off of the styles object to apply to the backdrop, and then applying all the animating styles to the actual modal content.

return modalTransition(   (styles, isOpen) =>     isOpen && (       <AnimatedDialogOverlay         allowPinchZoom={true}         initialFocusRef={focusRef}         onDismiss={onHide}         isOpen={isOpen}         style={{ opacity: styles.opacity }}       >       <AnimatedDialogContent         style={{           border: "4px solid hsla(0, 0%, 0%, 0.5)",           borderRadius: 10,           maxWidth: "400px",           ...styles         }}       >         <div>           <div>             <StandardModalHeader caption={headerCaption} onHide={onHide} />             {children}           </div>         </div>       </AnimatedDialogContent>     </AnimatedDialogOverlay>   ) );

Base setup

Let’s start with the use case I described above. If you’re following along with the demos, here’s a full demo of everything working, but with zero animation. Open the modal, and search for anything (feel free to just hit Enter in the empty textbox). You should hit my GraphQL endpoint, and get back search results from my own personal library.

The rest of this post will focus on adding animations to the UI, which will give us a chance to see a before and after, and (hopefully) observe how much nicer some subtle, well-placed animations can make a UI. 

Animating the modal size

Let’s start with the modal itself. Open it and search for, say, “jefferson.” Notice how the modal abruptly becomes larger to accommodate the new content. Can we have the modal animate to larger (and smaller) sizes? Of course. Let’s dig out our trusty useHeight hook, and see what we can do.

Unfortunately, we can’t simply slap the height ref on a wrapper in our content, and then stick the height in a spring. If we did this, we’d see the modal slide into its initial size. We don’t want that; we want our fully formed modal to appear in the right size, and re-size from there.

What we want to do is wait for our modal content to be rendered in the DOM, then set our height ref, and switch on our useHeight hook, to start measuring. Oh, and we want our initial height to be set immediately, and not animate into place. It sounds like a lot, but it’s not as bad as it sounds.

Let’s start with this:

const [heightOn, setHeightOn] = useState(false); const [sizingRef, contentHeight] = useHeight({ on: heightOn }); const uiReady = useRef(false);

We have some state for whether we’re measuring our modal’s height. This will be set to true when the modal is in the DOM. Then we call our useHeight hook with the on property, for whether we’re active. Lastly, some state to hold whether our UI is ready, and we can begin animating.

First things fist: how do we even know when our modal is actually rendered in the DOM? It turns out we can use a ref that lets us know. We’re used to doing <div ref={someRef} in React, but you can actually pass a function, which React will call with the DOM node after it’s rendered. Let’s define that function now.

const activateRef = ref => {   sizingRef.current = ref;   if (!heightOn) {     setHeightOn(true);   } };

That sets our height ref and switches on our useHeight hook. We’re almost done!

Now how do we get that initial animation to not be immediate? The useSpring hook has two new properties we’ll look at now. It has an immediate property which tells it to make states changes immediate instead of animating them. It also has an onRest callback which fires when a state change finishes.

Let’s leverage both of them. Here’s what the final hook looks like:

const heightStyles = useSpring({   immediate: !uiReady.current,   config: { ...config.stiff },   from: { height: 0 },   to: { height: contentHeight },   onRest: () => (uiReady.current = true) });

Once any height change is completed, we set the uiReady ref to true. So long as it’s false, we tell react-spring to make immediate changes. So, when our modal first mounts, contentHeight is zero (useHeight will return zero if there’s nothing to measure) and the spring is just chilling, doing nothing. When the modal switches to open and actual content is rendered, our activateRef ref is called, our useHeight will switch on, we’ll get an actual height value for our contents, our spring will set it “immediately,” and, finally, the onRest callback will trigger, and future changes will be animated. Phew!

I should point out that if, in some alternate use case we did immediately have a correct height upon the first render, we’d be able to simplify the above hook to just this:

const heightStyles = useSpring({   to: {     height: contentHeight   },   config: config.stiff, })

…which can actually be further simplified to this:

const heightStyles = useSpring({   height: contentHeight,   config: config.stiff, })

Our hook would render initially with the correct height, and any changes to that value would be animated. But since our modal renders before it’s actually shown, we can’t avail ourselves to this simplification. 

Keen readers might wonder what happens when you close the modal. Well, the content will un-render and the height hook will just stick with the last reported height, though still “observing” a DOM node that’s no longer in the DOM. If you’re worried about that, feel free to clean things up better than I have here, perhaps with something like this:

useLayoutEffect(() => {   if (!isOpen) {     setHeightOn(false);   } }, [isOpen]);

That’ll cancel the ResizeObserver for that DOM node and fix the memory leak.

Animating the results

Next, let’s look at animating the changing of the results within the modal. If you run a few searches, you should see the results immediately swap in and out.

Take a look at the SearchBooksContent component in the searchBooks.js file. Right now, we have const booksObj = data?.allBooks; which plucks the appropriate result set off of the GraphQL response, and then later renders them.

{booksObj.Books.map(book => (   <SearchResult     key={book._id}     book={book}     selected={selectedBooksMap[book._id]}     selectBook={selectBook}     dispatch={props.dispatch}   /> ))}

As fresh results come back from our GraphQL endpoint, this object will change, so why not take advantage of that fact, and pass it to the useTransition hook from before, and get some transition animations defined.

const resultsTransition = useTransition(booksObj, {   config: { ...config.default },   from: {     opacity: 0,     position: "static",     transform: "translate3d(0%, 0px, 0px)"   },   enter: {     opacity: 1,     position: "static",     transform: "translate3d(0%, 0px, 0px)"   },   leave: {     opacity: 0,     position: "absolute",     transform: "translate3d(90%, 0px, 0px)"   } });

Note the change from position: static to position: absolute. An outgoing result set with absolute positioning has no effect on its parent’s height, which is what we want. Our parent will size to the new contents and, of course, our modal will nicely animate to the new size based on the work we did above.

As before, we’ll use our transition function to render our content:

<div className="overlay-holder">   {resultsTransition((styles, booksObj) =>     booksObj?.Books?.length ? (       <animated.div style={styles}>         {booksObj.Books.map(book => (           <SearchResult             key={book._id}             book={book}             selected={selectedBooksMap[book._id]}             selectBook={selectBook}             dispatch={props.dispatch}           />         ))}       </animated.div>     ) : null   )}

Now new result sets will fade in, while outgoing sets of results will fade (and slightly slide) out to give the user an extra cue that things have changed.

Of course, we also want to animate any messaging, such as when there’s no results, or when the user has selected everything in the result set. The code for that is pretty repetitive with everything else in here, and since this post is already getting long, I’ll leave the code in the demo.

Animating selected books (out)

Right now, selecting a book instantly and abruptly vanishes it from the list. Let’s apply our usual fade out while sliding it out to the right. And as the item is sliding out to the right (via transform), we probably want its height to animate to zero so the list can smoothly adjust to the exiting item, rather than have it slide out, leaving behind an the empty box, which then immediately disappears.

By now, you probably think this is easy. You’re expecting something like this:

const SearchResult = props => {   let { book, selectBook, selected } = props; 
   const initiallySelected = useRef(selected);   const [sizingRef, currentHeight] = useHeight(); 
   const heightStyles = useSpring({     config: { ...config.stiff, clamp: true },     from: {       opacity: initiallySelected.current ? 0 : 1,       height: initiallySelected.current ? 0 : currentHeight,       transform: "translate3d(0%, 0px, 0px)"     },     to: {       opacity: selected ? 0 : 1,       height: selected ? 0 : currentHeight,       transform: `translate3d($ {selected ? "25%" : "0%"},0px,0px)`     }   }); 

This uses our trusted useHeight hook to measure our content, using the selected value to animate the item that’s leaving. We’re tracking the selected prop and animating to, or starting with, a height of 0 if it’s already selected, rather than simply removing the item and using a transition. This allows different result sets that have the same book to correctly decline to display it, if it’s selected.

This code does work. Give it a try in this demo.

But there’s a rub. If you select most of the books in a result set, there will be a kind of bouncy animation chain as you continue selecting. The book starts animating out of the list, and then the modal’s height itself starts trailing behind.

This looks goofy in my opinion, so let’s see what we can do about it. 

We’ve already seen how we can use the immediate property to turn off all spring animations. We’ve also seen the onRest callback fire when an animation finishes, and I’m sure you won’t be surprised to learn there’s an onStart callback which does what you’d expect. Let’s use those pieces to allow the content inside our modal to “turn off” the modal’s height animation when the content itself is animating heights.

First, we’ll add some state to our modal that switches animation on and off.

const animatModalSizing = useRef(true); const modalSizingPacket = useMemo(() => {   return {     disable() {       animatModalSizing.current = false;     },     enable() {       animatModalSizing.current = true;     }   }; }, []);

Now, let’s tie it into our transition from before.

const heightStyles = useSpring({   immediate: !uiReady.current || !animatModalSizing.current,   config: { ...config.stiff },   from: { height: 0 },   to: { height: contentHeight },   onRest: () => (uiReady.current = true) });

Great. Now how do we get that modalSizingPacket down to our content, so whatever we’re rendering can actually switch off the modal’s animation, when needed? Context of course! Let’s create a piece of context.

export const ModalSizingContext = createContext(null);

Then, we’ll wrap all of our modal’s content with it:

<ModalSizingContext.Provider value={modalSizingPacket}>

Now our SearchResult component can grab it:

const { enable: enableModalSizing, disable: disableModalSizing } = useContext(   ModalSizingContext );

…and tie it right into its spring:

const heightStyles = useSpring({   config: { ...config.stiff, clamp: true },   from: {     opacity: initiallySelected.current ? 0 : 1,     height: initiallySelected.current ? 0 : currentHeight,     transform: "translate3d(0%, 0px, 0px)"   },   to: {     opacity: selected ? 0 : 1,     height: selected ? 0 : currentHeight,     transform: `translate3d($ {selected ? "25%" : "0%"},0px,0px)`   },   onStart() {     if (uiReady.current) {       disableModalSizing();     }   },   onRest() {     uiReady.current = true;     setTimeout(() => {       enableModalSizing();     });   } });

Note the setTimeout at the very end. I’ve found it necessary to make sure the modal’s animation is truly shut off until everything’s settled.

I know that was a lot of code. If I moved too fast, be sure to check out the demo to see all this in action.

Animating selected books (in)

Let’s wrap this blog post up by animating the selected books that appear on the main screen, beneath the modal. Let’s have newly selected books fade in while sliding in from the left when selected, then slide out to the right while it’s height shrinks to zero when removed.

We’ll use a transition, but there already seems to be a problem because we need to account for each of the selected books needs to have its own, individual height. Previously, when we reached for useTransition, we’ve had a single from and to object that was applied to entering and exiting items.

Here, we’ll use an alternate form instead, allowing us to provide a function for the to object. It’s invoked with the actual animating item — a book object in this case — and we return the to object that contains the animating values. Additionally, we’ll keep track of a simple lookup object which maps each book’s ID to its height, and then tie that into our transition.

First, let’s create our map of height values:

const [displaySizes, setDisplaySizes] = useState({}); const setDisplaySize = useCallback(   (_id, height) => {     setDisplaySizes(displaySizes => ({ ...displaySizes, [_id]: height }));   },   [setDisplaySizes] );

We’ll pass the setDisplaySizes update function to the SelectedBook component, and use it with useHeight to report back the actual height of each book.

const SelectedBook = props => {   let { book, removeBook, styles, setDisplaySize } = props;   const [ref, height] = useHeight();   useLayoutEffect(() => {     height && setDisplaySize(book._id, height);   }, [height]);

Note how we check that the height value has been updated with an actual value before calling it. That’s so we don’t prematurely set the value to zero before setting the correct height, which would cause our content to animate down, rather than sliding in fully-formed. Instead, no height will initially be set, so our content will default to height: auto. When our hook fires, the actual height will set. When an item is removed, the height will animate down to zero, as it fades and slides out.

Here’s the transition hook:

const selectedBookTransitions = useTransition(selectedBooks, {   config: book => ({     ...config.stiff,     clamp: !selectedBooksMap[book._id]   }),   from: { opacity: 0, transform: "translate3d(-25%, 0px, 0px)" },   enter: book => ({     opacity: 1,     height: displaySizes[book._id],     transform: "translate3d(0%, 0px, 0px)"   }),   update: book => ({ height: displaySizes[book._id] }),   leave: { opacity: 0, height: 0, transform: "translate3d(25%, 0px, 0px)" } });

Notice the update callback. It will adjust our content if any of the heights change. (You can force this in the demo by resizing the results pane after selecting a bunch of books.)

For a little icing on top of our cake, note how we’re conditionally setting the clamp property of our hook’s config. As content is animating in, we have clamp off, which produces a nice (at least in my opinion) bouncing effect. But when leaving, it animates down, but stays gone, without any of the jitteriness we saw before with clamping turned off.

Bonus: Simplifying the modal height animation while fixing a bug in the process

After finishing this post, I found a bug in the modal implementation where, if the modal height changes while it’s not shown, you’ll see the old, now incorrect height the next time you open the modal, followed by the modal animating to the correct height. To see what I mean, have a look at this update to the demo. You’ll notice new buttons to clear, or force results into the modal when it’s not visible. Open the modal, then close it, click the button to add results, and re-open it — you should see it awkwardly animate to the new, correct height.

Fixing this also allows us to simplify the code for the height animation from before. The problem is that our modal currently continues to render in the React component tree, even when not shown. The height hook is still “running,” only to be updated the next time the modal is shown, rendering the children. What if we moved the modal’s children to its own, dedicated component, and brought the height hook with it? That way, the hook and animation spring will only be render when the modal is shown, and can start with correct values. It’s less complicated than it seems. Right now our modal component has this:

<animated.div style={{ overflow: "hidden", ...heightStyles }}>   <div style={{ padding: "10px" }} ref={activateRef}>     <StandardModalHeader       caption={headerCaption}       onHide={onHide}     />     {children}   </div> </animated.div>

Let’s make a new component that renders this markup, including the needed hooks and refs:

const ModalContents = ({ header, contents, onHide, animatModalSizing }) => {   const [sizingRef, contentHeight] = useHeight();   const uiReady = useRef(false);    const heightStyles = useSpring({     immediate: !uiReady.current || !animatModalSizing.current,     config: { ...config.stiff },     from: { height: 0 },     to: { height: contentHeight },     onRest: () => (uiReady.current = true)   });    return (     <animated.div style={{ overflow: "hidden", ...heightStyles }}>       <div style={{ padding: "10px" }} ref={sizingRef}>         <StandardModalHeader caption={header} onHide={onHide} />         {contents}       </div>     </animated.div>   ); };

This is a significant reduction in complexity compared to what we had before. We no longer have that activateRef function, and we no longer have the heightOn state that was set in activateRef. This component is only rendered by the modal if it’s being shown, which means we’re guaranteed to have content, so we can just add a regular ref to our div. Unfortunately, we do still need our uiReady state, since even now we don’t initially have our height on first render; that’s not available until the useHeight layout effect fires immediately after the first render finishes.

And, of course, this solves the bug from before. No matter what happens when the modal is closed, when it re-opens, this component will render anew, and our spring will start with a fresh value for uiReady.

Parting thoughts

If you’ve stuck with me all this way, thank you! I know this post was long, but I hope you found some value in it.

react-spring is an incredible tool for creating robust animations with React. It can be low-level at times, which can make it hard to figure out for non-trivial use cases. But it’s this low-level nature that makes it so flexible.


The post Making Sense of react-spring appeared first on CSS-Tricks.

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

CSS-Tricks

, ,
[Top]

The Making of: Netlify’s Million Devs SVG Animation Site

The following article captures the process of building the Million Developers microsite for Netlify. This project was built by a few folks and we’ve captured some parts of the process of building it here- focusing mainly on the animation aspects, in case any are helpful to others building similar experiences.

Building a Vue App out of an SVG

The beauty of SVG is you can think of it, and the coordinate system, as a big game of battleship. You’re really thinking in terms of x, y, width, and height.

<div id="app">    <app-login-result-sticky v-if="user.number" />    <app-github-corner />     <app-header />     <!-- this is one big SVG -->    <svg id="timeline" xmlns="http://www.w3.org/2000/svg" :viewBox="timelineAttributes.viewBox">      <!-- this is the desktop path -->      <path        class="cls-1 timeline-path"        transform="translate(16.1 -440.3)"        d="M951.5,7107..."      />      <!-- this is the path for mobile -->      <app-mobilepath v-if="viewportSize === 'small'" />       <!-- all of the stations, broken down by year -->      <app2016 />      <app2017 />      <app2018 />      <app2019 />      <app2020 />       <!-- the 'you are here' marker, only shown on desktop and if you're logged in -->      <app-youarehere v-if="user.number && viewportSize === 'large'" />    </svg>  </div> 

Within the larger app component, we have the large header, but as you can see, the rest is one giant SVG. From there, we broke down the rest of the giant SVG into several components:

  • Candyland-type paths for both desktop and mobile, shown conditionally by a state in the Vuex store
  • There are 27 stations, not including their text counterparts, and many decorative components like bushes, trees, and streetlamps, which is a lot to keep track of in one component, so they’re broken down by year
  • The ‘you are here’ marker, only shown on desktop and if you’re logged in

SVG is wonderfully flexible because not only can we draw absolute and relative shapes and paths within that coordinate system, we can also draw SVGs within SVGs. We just need to defined the x, y, width and height of those SVGs and we can mount them inside the larger SVG, which is exactly what we’re going to do with all these components so that we can adjust their placement whenever needed. The <g> within the components stands for group, you can think of them a little like divs in HTML.

So here’s what this looks like within the year components:

<template>  <g>    <!-- decorative components -->    <app-tree x="650" y="5500" />    <app-tree x="700" y="5550" />    <app-bush x="750" y="5600" />     <!-- station component -->    <app-virtual x="1200" y="6000" xSmall="50" ySmall="15100" />    <!-- text component, with slots -->    <app-text      x="1400"      y="6500"      xSmall="50"      ySmall="15600"      num="20"      url-slug="jamstack-conf-virtual"    >      <template v-slot:date>May 27, 2020</template>      <template v-slot:event>Jamstack Conf Virtual</template>    </app-text>     ...  </template>  <script> ...  export default {  components: {    // loading the decorative components in syncronously    AppText,    AppTree,    AppBush,    AppStreetlamp2,    // loading the heavy station components in asyncronously    AppBuildPlugins: () => import("@/components/AppBuildPlugins.vue"),    AppMillion: () => import("@/components/AppMillion.vue"),    AppVirtual: () => import("@/components/AppVirtual.vue"),  }, }; ... </script>

Within these components, you can see a number of patterns:

  • We have bushes and trees for decoration that we can sprinkle around viax and y values via props
  • We can have individual station components, which also have two different positioning values, one for large and small devices
  • We have a text component, which has three available slots, one for the date, and two for two different text lines
  • We’re also loading in the decorative components synchronously, and loading those heavier SVG stations async

SVG Animation

Header animation for Million Devs

The SVG animation is done with GreenSock (GSAP), with their new ScrollTrigger plugin. I wrote up a guide on how to work with GSAP for their latest 3.0 release earlier this year. If you’re unfamiliar with this library, that might be a good place to start.

Working with the plugin is thankfully straightforward, here is the base of the functionality we’ll need:

import { gsap } from "gsap"; import { ScrollTrigger } from "gsap/ScrollTrigger.js"; import { mapState } from "vuex";  gsap.registerPlugin(ScrollTrigger);  export default {  computed: {    ...mapState([      "toggleConfig",      "startConfig",      "isAnimationDisabled",      "viewportSize",    ]),  },  ...  methods: {    millionAnim() {      let vm = this;      let tl;      const isScrollElConfig = {        scrollTrigger: {          trigger: `.million$ {vm.num}`,          toggleActions: this.toggleConfig,          start: this.startConfig,        },        defaults: {          duration: 1.5,          ease: "sine",        },      };    }  },  mounted() {    this.millionAnim();  }, }; 

First, we’re importing gsap and the package we need, as well as state from the Vuex store. I put the toggleActions and start config settings in the store and passed them into each component because while I was working, I needed to experiment with which point in the UI I wanted to trigger the animations, this kept me from having to configure each component separately.

Those configurations in the store look like this:

export default new Vuex.Store({   state: {     toggleConfig: `play pause none pause`,     startConfig: `center 90%`,   } }

This configuration breaks down to

  • toggleConfig: play the animation when it passes down the page (another option is to say restart and it will retrigger if you see it again), it pauses when it is out of the viewport (this can slightly help with perf), and that it doesn’t retrigger in reverse when going back up the page.
  • startConfig is stating that when the center of the element is 90% down from the height of the viewport, to trigger the animation to begin.

These are the settings we decided on for this project, there are many others! You can understand all of the options with this video.

For this particular animation, we needed to treat it a little differently if it was a banner animation which didn’t need to be triggered on scroll or if it was later in the timeline. We passed in a prop and used that to pass in that config depending on the number in props:

if (vm.num === 1) {   tl = gsap.timeline({     defaults: {       duration: 1.5,       ease: "sine",     },   }); } else {   tl = gsap.timeline(isScrollElConfig); }

Then, for the animation itself, I’m using what’s called a label on the timeline, you can think of it like identifying a point in time on the playhead that you may want to hang animations or functionality off of. We have to make sure we use the number prop for the label too, so we keep the timelines for the header and footer component separated.

tl.add(`million$ {vm.num}`) ... .from(   "#front-leg-r",   {     duration: 0.5,     rotation: 10,     transformOrigin: "50% 0%",     repeat: 6,     yoyo: true,     ease: "sine.inOut",   },   `million$ {vm.num}` ) .from(   "#front-leg-l",   {     duration: 0.5,     rotation: 10,     transformOrigin: "50% 0%",     repeat: 6,     yoyo: true,     ease: "sine.inOut",   },   `million$ {vm.num}+=0.25` );

There’s a lot going on in the million devs animation so I’ll just isolate one piece of movement to break down: above we have the girls swinging legs. We have both legs swinging separately, both are repeating several times, and that yoyo: true lets GSAP know that I’d like the animation to reverse every other alteration. We’re rotating the legs, but what makes it realistic is the transformOrigin starts at the center top of the leg, so that when it’s rotating, it’s rotating around the knee axis, like knees do 🙂

Adding an Animation Toggle

animation toggle

We wanted to give users the ability to explore the site without animation, should they have a vestibular disorder, so we created a toggle for the animation play state. The toggle is nothing special- it updates state in the Vuex store through a mutation, as you might expect:

export default new Vuex.Store({   state: {     ...     isAnimationDisabled: false,   },   mutations: {     updateAnimationState(state) {       state.isAnimationDisabled = !state.isAnimationDisabled     },   ... })

The real updates happen in the topmost App component where we collect all of the animations and triggers, and then adjust them based on the state in the store. We watch the isAnimationDisabled property for changes, and when one occurs, we grab all instances of scrolltrigger animations in the app. We don’t .kill() the animations, which one option, because if we did, we wouldn’t be able to restart them.

Instead, we either set their progress to the final frame if animations are disabled, or if we’re restarting them, we set their progress to 0 so they can restart when they are set to fire on the page. If we had used .restart() here, all of the animations would have played and we wouldn’t see them trigger as we kept going down the page. Best of both worlds!

watch: {    isAnimationDisabled(newVal, oldVal) {      ScrollTrigger.getAll().forEach((trigger) => {        let animation = trigger.animation;        if (newVal === true) {          animation && animation.progress(1);        } else {          animation && animation.progress(0);        }      });    },  }, 

SVG Accessibility

I am by no means an accessibility expert, so please let me know if I’ve misstepped here- but I did a fair amount of research and testing on this site, and was pretty excited that when I tested on my Macbook via voiceover, the site’s pertinent information was traversable, so I’m sharing what we did to get there.

For the initial SVG that cased everything, we didn’t apply a role so that the screenreader would traverse within it. For the trees and bushes, we applied role="img" so the screenreader would skip it and any of the more detailed stations we applied a unique id and title, which was the first element within the SVG. We also applied role="presentation".

<svg    ...    role="presentation"    aria-labelledby="analyticsuklaunch"  >    <title id="analyticsuklaunch">Launch of analytics</title>

I learned a lot of this from this article by Heather Migliorisi, and this great article by Leonie Watson.

The text within the SVG does announce itself as you tab through the page, and the link is found, all of the text is read. This is what that text component looks like, with those slots mentioned above.

<template>  <a    :href="`https://www.netlify.com/blog/2020/08/03/netlify-milestones-on-the-road-to-1-million-devs/#$ {urlSlug}`"  >    <svg      xmlns="http://www.w3.org/2000/svg"      width="450"      height="250"      :x="svgCoords.x"      :y="svgCoords.y"      viewBox="0 0 280 115.4"    >      <g :class="`textnode text$ {num}`">        <text class="d" transform="translate(7.6 14)">          <slot name="date">Jul 13, 2016</slot>        </text>        <text class="e" transform="translate(16.5 48.7)">          <slot name="event">Something here</slot>        </text>        <text class="e" transform="translate(16.5 70)">          <slot name="event2" />        </text>        <text class="h" transform="translate(164.5 104.3)">View Milestone</text>      </g>    </svg>  </a> </template> 

Here’s a video of what this sounds like if I tab through the SVG on my Mac:

If you have further suggestions for improvement please let us know!

The repo is also open source if you want to check out the code or file a PR.

Thanks a million (pun intended) to my coworkers Zach Leatherman and Hugues Tennier who worked on this with me, their input and work was invaluable to the project, it only exists from teamwork to get it over the line! And so much respect to Alejandro Alvarez who did the design, and did a spectacular job. High fives all around. 🙌


The post The Making of: Netlify’s Million Devs SVG Animation Site appeared first on CSS-Tricks.

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

CSS-Tricks

, , , , ,
[Top]

Making lil’ me

Cassie Evans made a lovely illustration of herself and then used Greensock to add a flourish of animations to polish it off. Cassie wrote a series of posts about how she did it:

In this post we’ll cover how to get values from the mouse movement and plug them into an animation. This is my favourite thing about SVG animation. Not mouse movement in particular, but interactivity. Exporting animation as an mp4 is cool and all. But you can’t play with it. Animating on the web opens up so many cool possibilities!

Speaking of combining great illustrations with impressive animations, Jhey Tompkins recently shared a bunch of tips he’s learned from building complex CSS illustrations. This first piece of advice? It takes time and have patience. I’m sure he needed all the patience in the world to pull off his animated Matryoshka dolls.

Direct Link to ArticlePermalink


The post Making lil’ me appeared first on CSS-Tricks.

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

CSS-Tricks

,
[Top]