You can use the stroke-dasharray property in CSS to make dashes:
line { stroke-dasharray: 5; }
That 5 value is a relative unit based on the size of the SVG’s viewBox. We could use any CSS length, really. But what it does is make a pattern of dashes that are 5 units long with 5 unit gaps between them.
So far, so good. We can use two values where the second value individually sets the gap length:
Now we have dashes that are 5 units and gaps that are 10. Let’s try a third value:
See how we’re forming a pattern here? It goes:
Dash: 5 units
Gap: 10 units
Dash: 15 units
You’d think it repeats after that in the exact same cadence. But no! It if did, we’d have dashes bumping into one another:
Dash: 5 units
Gap: 10 units
Dash: 15 units
Dash: 5 units
Gap: 10 units
Dash: 15 units
…and so on.
Instead, stroke-dasharray gets all smart and duplicates the pattern if there are an odd number of values So…
stroke-dasharray: 5 10 15; /* is the same as */ stroke-dasharray: 5 10 15 5 10 15;
That’s actually why a single value works! Earlier, we declared a single 5 value. That’s really the same as saying stroke-dasharray: 5 5. Without the second value, stroke-dasharray implicitly duplicates the first value to get a repeatable pattern. Otherwise, it’d just be a solid line of dashes that are 5 units long, but no gaps between them!
The pattern also depends on the size of the shape itself. Our SVG line is 500 units. Let’s set larger stroke-dasharray values and add them up:
stroke-dasharray: 10 20 30 40 50; /* 150 units */
If the pattern runs four times (150 units ⨉ 4 iterations), we’re dealing with 600 total units. That additional 100 units is lopped off to prevent the pattern from overflowing itself.
Those of us who’ve been web developers more than a few years have probably written code using more than one JavaScript framework. With all the choices out there — React, Svelte, Vue, Angular, Solid — it’s all but inevitable. One of the more frustrating things we have to deal with when working across frameworks is re-creating all those low-level UI components: buttons, tabs, dropdowns, etc. What’s particularly frustrating is that we’ll typically have them defined in one framework, say React, but then need to rewrite them if we want to build something in Svelte. Or Vue. Or Solid. And so on.
Wouldn’t it be better if we could define these low-level UI components once, in a framework-agnostic way, and then re-use them between frameworks? Of course it would! And we can; web components are the way. This post will show you how.
As of now, the SSR story for web components is a bit lacking. Declarative shadow DOM (DSD) is how a web component is server-side rendered, but, as of this writing, it’s not integrated with your favorite application frameworks like Next, Remix or SvelteKit. If that’s a requirement for you, be sure to check the latest status of DSD. But otherwise, if SSR isn’t something you’re using, read on.
First, some context
Web Components are essentially HTML elements that you define yourself, like <yummy-pizza> or whatever, from the ground up. They’re covered all over here at CSS-Tricks (including an extensive series by Caleb Williams and one by John Rhea) but we’ll briefly walk through the process. Essentially, you define a JavaScript class, inherit it from HTMLElement, and then define whatever properties, attributes and styles the web component has and, of course, the markup it will ultimately render to your users.
Being able to define custom HTML elements that aren’t bound to any particular component is exciting. But this freedom is also a limitation. Existing independently of any JavaScript framework means you can’t really interact with those JavaScript frameworks. Think of a React component which fetches some data and then renders some other React component, passing along the data. This wouldn’t really work as a web component, since a web component doesn’t know how to render a React component.
Web components particularly excel as leaf components. Leaf components are the last thing to be rendered in a component tree. These are the components which receive some props, and render some UI. These are not the components sitting in the middle of your component tree, passing data along, setting context, etc. — just pure pieces of UI that will look the same, no matter which JavaScript framework is powering the rest of the app.
The web component we’re building
Rather than build something boring (and common), like a button, let’s build something a little bit different. In my last post we looked at using blurry image previews to prevent content reflow, and provide a decent UI for users while our images load. We looked at base64 encoding a blurry, degraded versions of our images, and showing that in our UI while the real image loaded. We also looked at generating incredibly compact, blurry previews using a tool called Blurhash.
That post showed you how to generate those previews and use them in a React project. This post will show you how to use those previews from a web component so they can be used by any JavaScript framework.
But we need to walk before we can run, so we’ll walk through something trivial and silly first to see exactly how web components work.
Everything in this post will build vanilla web components without any tooling. That means the code will have a bit of boilerplate, but should be relatively easy to follow. Tools like Lit or Stencil are designed for building web components and can be used to remove much of this boilerplate. I urge you to check them out! But for this post, I’ll prefer a little more boilerplate in exchange for not having to introduce and teach another dependency.
A simple counter component
Let’s build the classic “Hello World” of JavaScript components: a counter. We’ll render a value, and a button that increments that value. Simple and boring, but it’ll let us look at the simplest possible web component.
In order to build a web component, the first step is to make a JavaScript class, which inherits from HTMLElement:
class Counter extends HTMLElement {}
The last step is to register the web component, but only if we haven’t registered it already:
if (!customElements.get("counter-wc")) { customElements.define("counter-wc", Counter); }
And, of course, render it:
<counter-wc></counter-wc>
And everything in between is us making the web component do whatever we want it to. One common lifecycle method is connectedCallback, which fires when our web component is added to the DOM. We could use that method to render whatever content we’d like. Remember, this is a JS class inheriting from HTMLElement, which means our this value is the web component element itself, with all the normal DOM manipulation methods you already know and love.
At it’s most simple, we could do this:
class Counter extends HTMLElement { connectedCallback() { this.innerHTML = "<div style='color: green'>Hey</div>"; } } if (!customElements.get("counter-wc")) { customElements.define("counter-wc", Counter); }
…which will work just fine.
Adding real content
Let’s add some useful, interactive content. We need a <span> to hold the current number value and a <button> to increment the counter. For now, we’ll create this content in our constructor and append it when the web component is actually in the DOM:
If you’re really grossed out by the manual DOM creation, remember you can set innerHTML, or even create a template element once as a static property of your web component class, clone it, and insert the contents for new web component instances. There’s probably some other options I’m not thinking of, or you can always use a web component framework like Lit or Stencil. But for this post, we’ll continue to keep it simple.
Moving on, we need a settable JavaScript class property named value
It’s just a standard class property with a setter, along with a second property to hold the value. One fun twist is that I’m using the private JavaScript class property syntax for these values. That means nobody outside our web component can ever touch these values. This is standard JavaScript that’s supported in all modern browsers, so don’t be afraid to use it.
Or feel free to call it _value if you prefer. And, lastly, our update method:
Obviously this is not code you’d want to maintain at scale. Here’s a full working example if you’d like a closer look. As I’ve said, tools like Lit and Stencil are designed to make this simpler.
Adding some more functionality
This post is not a deep dive into web components. We won’t cover all the APIs and lifecycles; we won’t even cover shadow roots or slots. There’s endless content on those topics. My goal here is to provide a decent enough introduction to spark some interest, along with some useful guidance on actually using web components with the popular JavaScript frameworks you already know and love.
To that end, let’s enhance our counter web component a bit. Let’s have it accept a color attribute, to control the color of the value that’s displayed. And let’s also have it accept an increment property, so consumers of this web component can have it increment by 2, 3, 4 at a time. And to drive these state changes, let’s use our new counter in a Svelte sandbox — we’ll get to React in a bit.
We’ll start with the same web component as before and add a color attribute. To configure our web component to accept and respond to an attribute, we add a static observedAttributes property that returns the attributes that our web component listens for.
static observedAttributes = ["color"];
With that in place, we can add a attributeChangedCallback lifecycle method, which will run whenever any of the attributes listed in observedAttributes are set, or updated.
Let’s use what we just made. We’ll go into our Svelte app component and add something like this:
<script> let color = "red"; </script> <style> main { text-align: center; } </style> <main> <select bind:value={color}> <option value="red">Red</option> <option value="green">Green</option> <option value="blue">Blue</option> </select> <counter-wc color={color}></counter-wc> </main>
And it works! Our counter renders, increments, and the dropdown updates the color. As you can see, we render the color attribute in our Svelte template and, when the value changes, Svelte handles the legwork of calling setAttribute on our underlying web component instance. There’s nothing special here: this is the same thing it already does for the attributes of any HTML element.
Things get a little bit interesting with the increment prop. This is not an attribute on our web component; it’s a prop on the web component’s class. That means it needs to be set on the web component’s instance. Bear with me, as things will wind up much simpler in a bit.
First, we’ll add some variables to our Svelte component:
let increment = 1; let wcInstance;
Our powerhouse of a counter component will let you increment by 1, or by 2:
But, in theory, we need to get the actual instance of our web component. This is the same thing we always do anytime we add a ref with React. With Svelte, it’s a simple bind:this directive:
We obviously don’t want to do this for every web component or prop we need to manage. Wouldn’t it be nice if we could just set increment right on our web component, in markup, like we normally do for component props, and have it, you know, just work? In other words, it’d be nice if we could delete all usages of wcInstance and use this simpler code instead:
It turns out we can. This code works; Svelte handles all that legwork for us. Check it out in this demo. This is standard behavior for pretty much all JavaScript frameworks.
So why did I show you the manual way of setting the web component’s prop? Two reasons: it’s useful to understand how these things work and, a moment ago, I said this works for “pretty much” all JavaScript frameworks. But there’s one framework which, maddeningly, does not support web component prop setting like we just saw.
React is a different beast
React. The most popular JavaScript framework on the planet does not support basic interop with web components. This is a well-known problem that’s unique to React. Interestingly, this is actually fixed in React’s experimental branch, but for some reason wasn’t merged into version 18. That said, we can still track the progress of it. And you can try this yourself with a live demo.
The solution, of course, is to use a ref, grab the web component instance, and manually set increment when that value changes. It looks like this:
As we discussed, coding this up manually for every web component property is simply not scalable. But all is not lost because we have a couple of options.
Option 1: Use attributes everywhere
We have attributes. If you clicked the React demo above, the increment prop wasn’t working, but the color correctly changed. Can’t we code everything with attributes? Sadly, no. Attribute values can only be strings. That’s good enough here, and we’d be able to get somewhat far with this approach. Numbers like increment can be converted to and from strings. We could even JSON stringify/parse objects. But eventually we’ll need to pass a function into a web component, and at that point we’d be out of options.
Option 2: Wrap it
There’s an old saying that you can solve any problem in computer science by adding a level of indirection (except the problem of too many levels of indirection). The code to set these props is pretty predictable and simple. What if we hide it in a library? The smart folks behind Lit have one solution. This library creates a new React component for you after you give it a web component, and list out the properties it needs. While clever, I’m not a fan of this approach.
Rather than have a one-to-one mapping of web components to manually-created React components, what I prefer is just one React component that we pass our web component tag name to (counter-wc in our case) — along with all the attributes and properties — and for this component to render our web component, add the ref, then figure out what is a prop and what is an attribute. That’s the ideal solution in my opinion. I don’t know of a library that does this, but it should be straightforward to create. Let’s give it a shot!
This is how we create an element in React with a dynamic name. In fact, this is what React normally transpiles JSX into. All our divs are converted to createElement("div") calls. We don’t normally need to call this API directly but it’s there when we need it.
Beyond that, we want to run a layout effect and loop through every prop that we’ve passed to our component. We loop through all of them and check to see if it’s a property with an in check that checks the web component instance object as well as its prototype chain, which will catch any getters/setters that wind up on the class prototype. If no such property exists, it’s assumed to be an attribute. In either case, we only set it if the value has actually changed.
If you’re wondering why we use useLayoutEffect instead of useEffect, it’s because we want to immediately run these updates before our content is rendered. Also, note that we have no dependency array to our useLayoutEffect; this means we want to run this update on every render. This can be risky since React tends to re-render a lot. I ameliorate this by wrapping the whole thing in React.memo. This is essentially the modern version of React.PureComponent, which means the component will only re-render if any of its actual props have changed — and it checks whether that’s happened via a simple equality check.
The only risk here is that if you’re passing an object prop that you’re mutating directly without re-assigning, then you won’t see the updates. But this is highly discouraged, especially in the React community, so I wouldn’t worry about it.
Before moving on, I’d like to call out one last thing. You might not be happy with how the usage looks. Again, this component is used like this:
Specifically, you might not like passing the web component tag name to the <WcWrapper> component and prefer instead the @lit-labs/react package above, which creates a new individual React component for each web component. That’s totally fair and I’d encourage you to use whatever you’re most comfortable with. But for me, one advantage with this approach is that it’s easy to delete. If by some miracle React merges proper web component handling from their experimental branch into main tomorrow, you’d be able to change the above code from this:
You could probably even write a single codemod to do that everywhere, and then delete <WcWrapper> altogether. Actually, scratch that: a global search and replace with a RegEx would probably work.
The implementation
I know, it seems like it took a journey to get here. If you recall, our original goal was to take the image preview code we looked at in my last post, and move it to a web component so it can be used in any JavaScript framework. React’s lack of proper interop added a lot of detail to the mix. But now that we have a decent handle on how to create a web component, and use it, the implementation will almost be anti-climactic.
I’ll drop the entire web component here and call out some of the interesting bits. If you’d like to see it in action, here’s a working demo. It’ll switch between my three favorite books on my three favorite programming languages. The URL for each book will be unique each time, so you can see the preview, though you’ll likely want to throttle things in your DevTools Network tab to really see things taking place.
And a few helpers methods to tie everything together:
export function syncSingleChild(container, child) { const currentChild = container.firstElementChild; if (currentChild !== child) { clearContainer(container); if (child) { container.appendChild(child); } } } export function clearContainer(el) { let child; while ((child = el.firstElementChild)) { el.removeChild(child); } }
It’s a little bit more boilerplate than we’d need if we build this in a framework, but the upside is that we can re-use this in any framework we’d like — although React will need a wrapper for now, as we discussed.
As I mentioned, all frameworks not named React will handle setting web component properties for you. Just note that some have some special flavors of syntax. For example, with Solid.js, <your-wc value={12}> always assumes that value is a property, which you can override with an attr prefix, like <your-wc attr:value={12}>.
Wrapping up
Web components are an interesting, often underused part of the web development landscape. They can help reduce your dependence on any single JavaScript framework by managing your UI, or “leaf” components. While creating these as web components — as opposed to Svelte or React components — won’t be as ergonomic, the upside is that they’ll be widely reusable.
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.
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.
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.
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:
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.
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 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.
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.
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.
And we can see the corresponding cache that was created.
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.
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!
Zach did that thing where each of his blog posts has a special URL with the design of social image card that is screenshat by a headless browser (like Puppeteer) and used as a true meta Open Graph image, meaning it’s displayed on Twitter, Facebook, iMessage, Slack, Discord, and whatever else supports that card look.
I like it. Even though I’ve got a pretty good solution cooking now (for WordPress), the templates aren’t controlled with HTML/CSS like I wish they were.
As bit of yang to the ying here, Jim has some thoughts on the not-so-great aspects of Open Graph images:
I feel like they’ve been hijacked by auto-generated computer imagery serving as attention-grabbing filler more than supportive expression.
It’s kinda like… we can add Open Graph images, and we essentially get a totally free massive clickable target for hungry fingers, so we do add Open Graph images — even when that image is, well, boring. Just auto-generated computer barf of title text with branding. Jim’s post has examples.
I get where Jim is coming from, and I suppose I’m guilty to some degree. I feel like we’re a cut-above on CSS-Tricks though, if you’ll pardon a taste of defensiveness, because:
We have a variety of templates to choose from to switch it up, like a quote design.
We incorporate custom imagery into the final card, meaning most cards are somewhat visually unique.
We don’t just brand the cards, we usually incorporate the author for a little extra high five for the person, rather than just our brand.
There is a conversation that has been percolating for as long as I’ve been in the web design and development industry. It’s centered around the conflict between design tools and development tools. The final product of web design is often a mockup. The old joke was that web developers make websites and web designers make paintings of websites. That disconnect is a source of immense friction. Which is the source of truth?
What if there really could be a single source of truth. What if the design tool works on the same exact code as the production website? The latest chapter in this epic conversation is UXPin.
Let’s set up the facts so you can see this all play out.
UXPin is an in-browser design tool.
UXPin is a powerful design tool with all the features you’d expect, particularly focused on digital screen-based design.
The fact that it is in-browser is extra great here. Designing websites… on a website is an obvious and natural fit. It means what you are looking at is how it’s going to look. This is particularly important with typography! The implementer of this card component can see exact colors (in the right formats) are being used, as well as the exact pixel dimensions, etc.
Over a decade ago, Jason Santa Maria thought a lot about what a next-gen design tool would look like. Could we just use the browser directly?
I don’t think the browser is enough. A web designer jumping into the browser before tackling the creative and messaging problems is akin to an architect hammering pieces of wood together and then measuring afterwards. The imaginative process is cut short by the tools at hand; and it’s that imagination—or spark—at the beginning of a design that lays the path for everything that follows.
Perhaps not the browser directly, but a design tool within a browser, that could be the best of both worlds:
An application like this could change the process of web design considerably. Most importantly, it wouldn’t be a proxy application that we use to simulate the way webpages look—it would already speak the language of the web. It would truly be designing in the browser.
It’s so cool to see this play out in a way that aligns with what great designers envisioned before it was possible, and with new aspects that melt with today’s technological possibilities.
You can work on your own React components within UXPin.
This is where the source of truth magic can happen. It’s one thing if a design tool can output a React (or any other framework) component. That’s a neat trick. But it’s likely to be a one-way trip. Components in real-world projects are full of other things that aren’t entirely the domain of design. Perhaps a component uses a hook to return the current user’s permissions and disable a button if they don’t have access. The disabled button has an element of design to it, but most of that code does not.
It’s impractical to have a design tool that can’t respect other code in that component and essentially just leave it alone. Essentially, it’s not really a design tool if it exports components but can’t import them.
Now, fair is fair, this is going to take a little work to set up. Might just be a couple of hours, or it might take few days for a complete design system. UXPin only works with React and uses a webpack configuration to integrate it.
Once you’ve gotten in going, the components you use in UXPin are very literally the components you use to build your production website.
It’s pretty impressive really, to see a design tool digest pre-built components and allow them to be used on an entirely new canvas for prototyping.
They’ve got lots of help for you on getting this going on your project, including:
As it should, it’s likely to influence how you build components.
Components tend to have props, and props control things like design and content inside. UXPin gives you a UI for the props, meaning you have total control over the component.
Knowing that, you might give yourself a prop interface for your components that provides you with lots of design control. For example, integrating theme switching.
This is all even faster with Storybook.
Another awfully popular tool in JavaScript-components-land to view your components is Storybook. It’s not a design tool like UXPin—it’s more like a zoo for your components. You might already have it set up, or you might find value in using Storybook as well.
The great news? UXPin Merge works together awesomely with Storybook. It makes integration super quick and easy. Plus then it supports any framework, like Angular, Svelte, Vue, etc—in addition to React.
What if designers could use the very same components used by engineers and they’re all stored in a shared design system (with accurate documentation and tests)? Many of the frustrating and expensive misunderstandings between designers and engineers would stop happening.
And a plan:
Connect to any Git repo.
Learn about all the components in that repo.
Make those components available to the UXPin visual editor.
Watch for any changes to the repo and reflect those changes in the visual editor.
Let designer’s design and deliver accurate specs to developers for using those components.
As the web gets more and more capable, developers are able to make richer online experiences. There are times, however, where some new web capabilities may not work as you would expect in the interest of usability, security and privacy.
I have run into situations like this. Like lazy loading in HTML. It’s easy to drop that attribute onto an image element only to realize… it actually needs more than that to do its thing. We’ll get into that specific one in a moment as we look at a few other features that might not work exactly as you‘d expect.
:visited links have limited styling and getComputedStyle lies about their style
This limitation has been around for a while, but it does show how browser features can be exploited. One possible exploit is an anchor gets some :visited link style in CSS and is positioned off screen. With the off-screen anchor, one could use JavaScript to change the anchor’s href value and see if a particular href causes the link to appear visited—reconstructing a user’s history in the process.
These days, attempting to use getComputedStyle on a :visited link returns the style of the :unvisited link instead. That’s just one of those things you have to know because that’s different from how it intuitively ought to work.
But we can get around this in two ways:
make the visited link’s style trigger a side effect (e.g. a layout shift), or
leverage the sibling (~ or +) or child (>) CSS selectors to render another style.
Regarding side effects, while there are some clever yet fragile ways to do this, the options we have for styling :visited links are limited and some styles (like background-color) will only work if they’re applied to unvisited links. As for using a sibling or child, executing getComputedStyle on these returns the style as if the link wasn’t visited to begin with.
Browsers don’t cache assets across sites anymore
One advantage of a CDN was that they allowed for a particular resource (like Google Fonts) to be cached in the browser for use across different websites. While this does provide a big performance win, it has grave privacy implications.
Given that an asset that’s already cached will take longer to load than one that’s not, a site could perform a timing attack to not only see your site history but also expose both who you are and your online activity. Jeff Kaufman gives an example:
Unfortunately, a shared cache enables a privacy leak. Summary of the simplest version:
I want to know if you’re a moderator on www.forum.example.
I know that only pages under load www.forum.example/moderators/header.css.
When you visit my page I load www.forum.example/moderators/header.css and see if it came from cache.
In light of this, browsers don’t offer this anymore.
performance.now() may be inaccurate
A scary group of vulnerabilities came out as couple of years ago, one of which was called Spectre. For an in depth explanation, see Google’s leaky.page (works best in Chromium) as a proof of concept. But for the purposes of this article, just know that the exploit relies on getting highly accurate timing, which is something that performance.now() provides, to try and map sensitive CPU data.
To mitigate Spectre, browsers have reduced its accuracy and may add noise as well. These range from 20μs to 1ms and can be changed based on various conditions like HTTP headers and browser settings.
Lazy loading with the loading attribute doesn’t work without JavaScript
Lazy loading is a technique where assets are only loaded in the browser when it scrolls into the viewport. Until recently, we could only implement this in JavaScript using IntersectionObserver or onscroll. Except for Safari, we can apply the loading attribute to images and iframes (in Chromium) and the browser will handle lazy loading.
Note that lazy loading can’t be polyfilled since an image is probably loading by the time you check for the loading attribute’s support.
Being able to do this in HTML makes it sound like the attribute doesn’t require JavaScript at all, but it does. From the WHATWG spec:
If scripting is disabled for an element, return false.
Note This is an anti-tracking measure, because if a user agent supported lazy loading when scripting is disabled, it would still be possible for a site to track a user’s approximate scroll position throughout a session, by strategically placing images in a page’s markup such that a server can track how many images are requested and when.
I’ve seen articles mention that this attribute is how you support lazy loading “without JavaScript” which isn’t true, though it is true you don’t have to write any.
Browsers can limit features based on user preferences
Some users might opt to heavily restrict browser functionality in the interest of further security and privacy. Firefox and Tor are two browsers that do this through the resist fingerprint setting which does things like reducing the precision of certain variables (dimensions and time), omitting certain variables entirely, limiting or disabling some Web APIs and never matching media queries. WebKit has a document outlining how browsers can approach fingerprint resistance.
Note that this goes beyond the standard anti-tracking features that browsers implement. It’s unlikely that a user will enable this as they would need a very specific threat model to do so. Part of this can be countered with progressive enhancement, graceful degradation, and understanding your users. This limitation is a big issue when you actually need fingerprinting, like fraud detection. So, if it’s absolutely necessary, look for an alternative means.
Screen readers might not relay the semantics of certain elements
Semantic HTML is great for many reasons, most notably that it conveys meaning in markup that software, like screen readers, interpret and announce to users who rely on them to navigate the web. It’s essential for crafting accessible websites. But, at times, those semantics aren’t conveyed—at least how you might expect. Something might be accessible, but still have usability issues.
In case of conflict, consider users over authors over implementors over specifiers over theoretical purity. In other words costs or difficulties to the user should be given more weight than costs to authors;
Another case where semantics might not be relayed is with emphasis. Take inline elements like strong, em, mark, ins, del, and data—all elements that have semantic meanings, but are unlikely to be read out because they can get noisy. This can be changed in a user’s screenreader’s settings, but if you really want it to be read you can declare it in visually hidden in the content property of either a :before or :after pseudo-element.
To illustrate this I made a brief example to see how NVDA with Firefox 89 and VoiceOver with Safari 14.6 read out semantic elements.
Unlike VoiceOver, NVDA reads out some of the semantic elements (del, ins and mark) and tries to emphasize text by gradually increasing the volume of emphasized text. Both of them have no trouble reading out the :before/:after psudo-elements however. Also, VoiceOver read out the tag’s brackets (greater than, less than), though both screenreaders have the ability to change how much punctuation is read.
To see whether or not you need to emphasize the emphasis, make sure you test with your users and see what they need. I didn’t focus on the visual aspect but the default styling of emphasis elements may be inconsistent across browsers, so make sure you provide suitable styling to go along with it.
Interesting, isn’t it? Some web features that we might expect to work a certain way just don’t. That isn’t to say that the features are wrong and need to be fixed, but more of a heads up as we write code.
It’s worth examining your own assumptions during development. Critically examine what your users need and factor it in as you make your site. You’re certainly welcome to work around these these as you encounter them, but in cases where you’re unable to, make sure to find and provide reasonable progressive enhancement and graceful degradation. It’s OK if users don’t experience a website the exact same way in every browser as long as they’re able to do what they need to.
That’s my list of things that don’t work the way I expect them to. What’s on your list? I’m sure you’ve got some and I’d love to see them in the comments!
SVGs are awesome: they are small, look sharp on any scale, and can be customized without creating a separate file. However, there is something I feel is missing in web standards today: a way to include them as an external file that also retains the format’s customization powers.
For instance, let’s say you want to use your website’s logo stored as web-logo.svg. You can do:
<img src="/images/logo.svg" />
That’s fine if your logo is going to look the same everywhere. But in many cases, you have 2-3 variations of the same logo. Slack, for example, has two versions.
Even the colors in the main logo are slightly different.
If we had a way to customize fill color of our logo above, we could pass any arbitrary color to render all the variations.
Take the case of icons, too. You wouldn’t want to do something like this, would you?
To address this, I have created a library called svg-loader. Simply put, it fetches the SVG files via XHR and loads them as inline elements, allowing you to customize the properties like fill and stroke, just like inline SVGs.
For example, I have a logo on my side-project, SVGBox. Instead of creating a different file for every variation, I can have one file and customize the fill color:
I used data-src to set the URL of SVG file. The fill attribute overrides fill of the original SVG file.
To use the library, the only thing I have to ensure is that files being served have appropriate CORS headers for XHRs to succeed. The library also caches the files locally, making the subsequent much faster. Even for the first load, the performance is comparable to using <img> tags.
This concept isn’t new. svg-inject does something similar. However, svg-loader is easier to use as we only have to include the library somewhere in your code (either via a <script> tag, or in the JavaScript bundle). No extra code is needed.
Dynamically-added elements and change in attributes are also handled automatically, which ensures that it works with all web frameworks. Here’s an example in React:
But why?
This approach may feel unorthodox because it introduces a JavaScript dependency and there are already multiple ways to use SVGs, including inline and from external sources. But there’s a good case for using SVGs this way. Let’s examine them by answering the common questions.
Can we not just inline SVG ourselves?
Inlining is the simplest way to use SVGs. Just copy and paste the SVG code in the HTML. That’s what svg-loader is ultimately doing. So, why add the extra steps to load a SVG file from somewhere else? There are two major reasons:
Inline SVGs make the code verbose: SVGs can be anywhere from a few lines to a few hundred. Inline SVGs can work well if what you need is just a couple of icons and they are all tiny. But it becomes a major pain if they are sizeable or many, because then, they become long strings of text in code that isn’t “business logic.” The code becomes hard to parse.
It’s the same thing as preferring an external stylesheet over a <style> tag or using images instead of data URIs. It’s no wonder that in React codebases, the preferred approach is to use SVG as a separate component, rather than define it as a part of JSX.
External SVGs are much more convenient: Copying and pasting often does the job, but external SVGs can be really convenient. Say you’re experimenting with which icon to use in your app. If you’re using inline SVGs, that means going back and forth to get the SVG code. But with external SVGs, you only have to know the name of the file.
Take a look at this example. One of the most extensive icon repository on GitHub is Material Design Icons. With svg-loader and unpkg, we can start using any of 5,000+ icons right away.
Isn’t it inefficient to trigger an HTTP request for every SVG versus making a sprite?
Not really. With HTTP2, the cost of making an HTTP request has become less relevant. Yes, there are still benefits of bundling (e.g., better compression), but for non-blocking resources and XHRs, the pros are almost non-existent in real-world scenarios.
Here’s a Pen loading 50 icons in a similar fashion as above. (Open in incognito mode as the files are cached by default):
Safari doesn’t even support symbols files hosted on the same domain.
Can we not use a build tool that inlines the SVGs?
I couldn’t find an obvious way to fetch SVGs from a URL and inline them in common bundlers, like webpack and Grunt, although they exist for inlining SVG files stored locally. Even if a plugin that does this exists, setting up bundlers isn’t exactly straightforward. In fact, I often avoid using them until the project has reached acertain level of complexity. We must also realize that a majority of the internet is alien to things like webpack and React. Simple scripts can have a much wider appeal.
What about the <object> tag?
The <object> tag is a native way to include external SVG files that work across all the browsers.:
However, the drawback is we’re unable to customize the SVG’s attributes unless it’s hosted on the same domain (and the <object> tag doesn’t respect CORS headers). Even if the file is hosted on the same domain, we’d require JavaScript to manipulate the fill, like this:
In short, using external SVG files this way makes it ultra-convenient to use icons and other SVG assets. As covered earlier, with unpkg, we can use any icon on GitHub without needing extra code. We can avoid creating a pipeline in a bundler to process SVG files or a component for every icon, and just host the icons on a CDN.
Loading SVG files this way packs a lot of benefits with very little cost.
Just a little post I wrote up over at The Events Calendar blog. The idea is that a set of blocks can be grouped together in WordPress, then registered in a register_block_pattern() function that makes the group available to use as a “block pattern” in any page or post.
Block patterns are becoming upper-class citizens in the WordPress block editor. They were announced without much fanfare in WordPress 5.5 back in August, but have been given prominent real estate in the block inserter with its own tab next to blocks, including 10 or so default ones right out of the box.
Block patterns are sandwiched between Blocks and Reusable Blocks in the block inserter, which is a perfect metaphor for where it fits in the bigger picture of WordPress editing.
What I find interesting is how the blocks ecosystem is evolving. We started with a set of default blocks that can be inserted into a post. We got reusable blocks that provide a way to assemble a group of blocks with consistent content across all pages of posts. Now we have a way to do the same, but in a much more flexible and editable way. The differences are subtle, but the use cases couldn’t be more different. We’ve actually been using reusable blocks here at CSS-Tricks for post explanations, like this:
We drop some text in here when we think there’s something worth calling out or that warrants a little extra explanation.
Any reusable block can be converted to a “regular” block. The styles are maintained but the content is not. That’s been our hack-y approach for speeding up our process around here, but now that block patterns are a thing, previous reusable blocks we’ve been using now make more sense as patterns.
Hard-stop gradients are one of my favorite CSS tricks. Here, Marcel Moreau combines that idea with CSS grid to solve an issue that’s otherwise a pain in the butt. Say you have like a 300px right sidebar on a desktop layout with a unique background color. Easy enough. But then say you want that background color to stretch to the right edge of the browser window even though the grid itself is width-constrained. Tricker.
Talk to anyone who has an active blog and I bet they’ll tell you it’s been valuable to them. Maybe it’s opened doors. Maybe it’s got them a job. Maybe it’s got them a conference invite. Maybe they just like the thrill of knowing people have read and responded to it. Maybe they learned a lot through its creation and maintenance.
In remote work, we communicate primarily through writing. We send messages in Slack. We document projects in Notion. We send meeting invites with a written description of the purpose. We’re writing all the time.
It’s just so damn important for team work of any kind, particularly when you aren’t next to each other physically.
While writing forces people to think clearly, writing also forces teams to think clearly. In my experience, having a clearly written thing makes it easy for folks to collaborate with me. This is because people naturally enjoy poking holes in arguments, adding points that were missed, or mentioning any risks that weren’t taken into account. I’ve found it helpful to use this human tendency to my advantage. Extra opinions and poked holes are hard to surface if you didn’t write something in the first place.