Tag: Making

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.


, , , , , ,

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.


, , , , ,

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

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.


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.


, , , ,

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.


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.


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.


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.


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.


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


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.


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


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.


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.


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.


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.


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.


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.


, , ,

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.


, ,

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. 


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.


, ,

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.


, , , , ,

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.



Making My Netlify Build Run Sass

Let’s say you wanted to build a site with Eleventy as the generator. Popular choice these days! Eleventy doesn’t have some particularly blessed way of preprocessing your CSS, if that’s something you want to do. There are a variety of ways to do it and perhaps that freedom is part of the spirit of Eleventy.

I’ve seen people set up Gulp for this, which is cool, I still use and like Gulp for some stuff. I’ve seen someone use templating to return preprocessed CSS, which seems weird, but hey, whatever works. I’ve even seen someone extend the Eleventy config itself to run the processing.

So far, the thing that has made the most sense to me is to use npm scripts do the Sass processing. Do the CSS first, then the HTML, with npm-run-all. So, you’d set up something like this in your package.json:

  "scripts": {     "build": "npm-run-all build:css build:html",     "build:css": "node-sass src/site/_includes/css/main.scss > src/site/css/main.css",     "build:html": "eleventy",     "watch": "npm-run-all --parallel watch:css watch:html",     "watch:css": "node-sass --watch src/site/_includes/css/main.scss > src/site/css/main.css",     "watch:html": "eleventy --serve --port=8181",     "start": "npm run watch"   },

I think that’s fairly nice. Since Eleventy doesn’t have a blessed CSS processing route anyway, it feels OK to have it de-coupled from Eleventy processing.

But I see Netlify has come along nicely with their build plugins. As Sarah put it:

What the Build Plugin does is give you access to key points in time during that process, for instance, onPreBuildonPostBuildonSuccess, and so forth. You can execute some logic at those specific points in time

There is something really intuitive and nice about that structure. A lot of build plugins are created by the community or Netlify themselves. You just click them on via the UI or reference them in your config. But Sass isn’t a build-in project (as I write), which I would assume is because people are a pretty opinionated about what/where/how their CSS is processed that it makes sense to just let people do it themselves. So let’s do that.

In our project, we’d create a directory for our plugins, and then a folder for this particular plugin we want to write:

project-root/   src/   whatever/   plugins/     sass/       index.js       manifest.yml

That index.js file is where we write our code, and we’ll specifically want to use the onPreBuild hook here, because we’d want our Sass to be done preprocessing before the build process runs Eleventy and Eleventy moves things around.

module.exports = {   onPreBuild: async ({ utils: { run } }) => {     await run.command(       "node-sass src/site/_includes/css/main.scss src/site/css/main.css"     );   }, };

Here’s a looksie into all the relevant files together:

Now, if I netlify build from the command line, it will run the same build process that Netlify itself does, and it will hook into my plugin and run it!

One little thing I noticed is that I was trying to have my config be the (newer) netlify.yml format, but the plugins didn’t work, and I had to re-do the config as netlify.toml.

So we de-coupled ourselves from Eleventy with this particular processing, and coupled ourselves to Netlify. Just something to be aware of. I’m down with that as this way of configuring a build is so nice and I see so much potential in it.

I prefer the more explicit and broken up configuration of this style. Just look at how much cleaner the package.json gets:

Removed a bunch of lines from the scripts area of a package.json file, like the specific build:css and build:html commands

I still have this idea…

…of building a site that is a dog-fooded example of all the stuff you could/should do during a build process. I’ve started the site here, (and repo), but it’s not doing too much yet. I think it would be cool to wire up everything on that list (and more?) via Build Plugins.

If you wanna contribute, feel free to let me know. Maybe email me or open an issue to talk about what you’d want to do. You’re free to do a Pull Request too, but PRs without any prior communication are a little tricky sometimes as it’s harder to ensure our visions are aligned before you put in a bunch of effort.

The post Making My Netlify Build Run Sass appeared first on CSS-Tricks.


, , ,

I’m getting back to making videos

It’s probably one part coronavirus, one part new-fancy-video setup, and one part “hey this is good for CodePen too,” but I’ve been doing more videos lately. It’s nice to be back in the swing of that for a minute. There’s something fun about coming back to an old familiar workflow.

Where do the videos get published? I’m a publish-on-your-own site kinda guy, as I’m sure you know, so there is a whole Videos section of this site where every video we’ve ever published lives. There is also a YouTube channel, of course, which is probably the most practical way for most people to subscribe. We’re about halfway to Wes Bos-level, so let’s go people!

I had literally forgotten about it, but ages ago when I set this up, I created a special RSS feed for the videos so I could submit it as a video podcast on iTunes. That’s all still there and working! An interesting side note is that this enables offline viewing, as most podcatchers can cache subscriptions. Why build an app when you get the core ability for free, right?

I keep the original videos, of course. On individual video pages, I show a YouTube player that could be somewhat easily swapped out for another player if something crazy happened, like YouTube closes down or drastically changed their business model in some way that makes it problematic to show videos with their player. The originals are stored in an S3 bucket. If you’re an MVP Supporter, I give you the original high-quality download link right on the video pages.

If your curious about my workflow, I’m still using ScreenFlow. I don’t make nearly enough use of it, but it feels good in that it’s fairly easy to use, very reliable and fast, and I can always learn and do more with it. Shooting my screen is easy and a built-in feature of ScreenFlow of course. I also have a Rode Podcaster on a boom arm at my desk so the audio is passable. And I just went through a whole process to use a DSLR camera at my desk too, and I think the quality from that is great. It’s all a little funny because I have this whole sound recording booth as well, with a $ 1,000 audio setup in there, but I only use that for podcasting. The lighting sucks in there, making it no good for video.

It’s this new desk setup that has inspired me to do more video, and I suspect it will continue! One thing I could really use is a new high quality intro video. Just like a five-second thing with refreshed aesthetics. Anyone do that kind of work?

The post I’m getting back to making videos appeared first on CSS-Tricks.


, , , ,