Add Less

When you’re about to start a new website, what do you think first? Do you start with a library or framework you know, like React or Vue, or a meta-framework on top of that, like Next or Nuxt? Do you pull up a speedy build tool like Vite, or configure your webpack?

There’s a great tweet by Phil Hawksworth that I bookmarked a few years back and still love to this day:

Your websites start fast until you add too much to make them slow. Do you need any framework at all? Could you do what you want natively in the browser? Would doing it without a framework at all make your site lighter, or actually heavier in the long run as you create or optimize what others have already done?

I personally love the idea of shipping less code to ultimately ship more value to the browser. Understanding browser APIs and what comes “for free” could actually lead to less reinventing the wheel, and potentially more accessibility as you use the tools provided.

Instead of pulling in a library for every single task you want to do, try to look under the hood at what they are doing. For example, in a project I was maintaining, I noticed that we had a React component imported that was shipping an entire npm package for a small (less than 10-line) component with some CSS sprinkled on top (that we were overriding with our own design system). When we re-wrote that component from scratch, our bundle size was smaller, we were able to customize it more, and we didn’t have to work around someone else’s decisions.

Now, I’m not saying you shouldn’t use any libraries or frameworks or components out there. Open source exists for a reason! What I am saying is to be discerning about what you bring into your projects. Let the power of the browser work for you, and use less stuff!


Add Less originally published on CSS-Tricks

Empathetic Animation

Animation on the web is often a contentious topic. I think, in part, it’s because bad animation is blindingly obvious, whereas well-executed animation fades seamlessly into the background. When handled well, animation can really elevate a website, whether it’s just adding a bit of personality or providing visual hints and lessening cognitive load. Unfortunately, it often feels like there are two camps, accessibility vs. animation. This is such a shame because we can have it all! All it requires is a little consideration.

Here’s a couple of important questions to ask when you’re creating animations.

Does this animation serve a purpose?

This sounds serious, but don’t worry — the site’s purpose is key. If you’re building a personal portfolio, go wild! However, if someone’s trying to file a tax return, whimsical loading animations aren’t likely to be well-received. On the other hand, an animated progress bar could be a nice touch while providing visual feedback on the user’s action.

Is it diverting focus from important information?

It’s all too easy to get caught up in the excitement of whizzing things around, but remember that the web is primarily an information system. When people are trying to read, animating text or looping animations that play nearby can be hugely distracting, especially for people with ADD or ADHD. Great animation aids focus; it doesn’t disrupt it.

So! Your animation’s passed the test, what next? Here are a few thoughts…

Did we allow users to opt-out?

It’s important that our animations are safe for people with motion sensitivities. Those with vestibular (inner ear) disorders can experience dizziness, headaches, or even nausea from animated content.

Luckily, we can tap into operating system settings with the prefers-reduced-motion media query. This media query detects whether the user has requested the operating system to minimize the amount of animation or motion it uses.

Screenshot of the user preferences settings in MacOS, open to Accessibility and displaying options for how to display things, including one option for reduce motion, which is checked.
The reduced motion settings in macOS.

Here’s an example:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

This snippet taps into that user setting and, if enabled, it gets rid of all your CSS animations and transitions. It’s a bit of a sledgehammer approach though — remember, the key word in this media query is reduced. Make sure functionality isn’t breaking and that users aren’t losing important context by opting out of the animation. I prefer tailoring reduced motion options for those users. Think simple opacity fades instead of zooming or panning effects.

What about JavaScript, though?

Glad you asked! We can make use of the reduced motion media query in JavaScript land, too!

let motionQuery = matchMedia('(prefers-reduced-motion)');

const handleReduceMotion = () => {
  if (motionQuery.matches) {
    // reduced motion options
  }
}

motionQuery.addListener(handleReduceMotion);
handleReduceMotion()

Tapping into system preferences isn’t bulletproof. After all, it’s there’s no guarantee that everyone affected by motion knows how to change their settings. To be extra safe, it’s possible to add a reduced motion toggle in the UI and put the power back in the user’s hands to decide. We {the collective} has a really nice implementation on their site

Here’s a straightforward example:

Scroll animations

One of my favorite things about animating on the web is hooking into user interactions. It opens up a world of creative possibilities and really allows you to engage with visitors. But it’s important to remember that not all interactions are opt-in — some (like scrolling) are inherently tied to how someone navigates around your site.

The Nielson Norman Group has done some great research on scroll interactions. One particular part really stuck out for me. They found that a lot of task-focused users couldn’t tell the difference between slow load times and scroll-triggered entrance animations. All they noticed was a frustrating delay in the interface’s response time. I can relate to this; it’s annoying when you’re trying to scan a website for some information and you have to wait for the page to slowly ease and fade into view.

If you’re using GreenSock’s ScrollTrigger plugin for your animations, you’re in luck. We’ve added a cool little property to help avoid this frustration: fastScrollEnd.

fastScrollEnd detects the users’ scroll velocity. ScrollTrigger skips the entrance animations to their end state when the user scrolls super fast, like they’re in a hurry. Check it out!

There’s also a super easy way to make your scroll animations reduced-motion-friendly with ScrollTrigger.matchMedia():


I hope these snippets and insights help. Remember, consider the purpose, lead with empathy, and use your animation powers responsibly!

Add a Service Worker to Your Site

One of the best things you can do for your website in 2022 is add a service worker, if you don’t have one in place already. Service workers give your website super powers. Today, I want to show you some of the amazing things that they can do, and give you a paint-by-numbers boilerplate that you can use to start using them on your site right away.

What are service workers?

A service worker is a special type of JavaScript file that acts like middleware for your site. Any request that comes from the site, and any response it gets back, first goes through the service worker file. Service workers also have access to a special cache where they can save responses and assets locally.

Together, these features allow you to…

  • Serve frequently accessed assets from your local cache instead of the network, reducing data usage and improving performance.
  • Provide access to critical information (or even your entire site or app) when the visitor goes offline.
  • Prefetch important assets and API responses so they’re ready when the user needs them.
  • Provide fallback assets in response to HTTP errors.

In short, service workers allow you to build faster and more resilient web experiences.

Unlike regular JavaScript files, service workers do not have access to the DOM. They also run on their own thread, and as a result, don’t block other JavaScript from running. Service workers are designed to be fully asynchronous.

Security

Because service workers intercept every request and response for your site or app, they have some important security limitations.

Service workers follow a same-origin policy.

You can’t run your service worker from a CDN or third party. It has to be hosted at the same domain as where it will be run.

Service workers only work on sites with an installed SSL certificate.

Many web hosts provide SSL certificates at no cost or for a small fee. If you’re comfortable with the command line, you can also install one for free using Let’s Encrypt.

There is an exception to the SSL certificate requirement for localhost testing, but you can’t run your service worker from the file:// protocol. You need to have a local server running.

Adding a service worker to your site or web app

To use a service worker, the first thing we need to do is register it with the browser. You can register a service worker using the navigator.serviceWorker.register() method. Pass in the path to the service worker file as an argument.

navigator.serviceWorker.register('sw.js');

You can run this in an external JavaScript file, but prefer to run it directly in a script element inline in my HTML so that it runs as soon as possible.

Unlike other types of JavaScript files, service workers only work for the directory in which they exist (and any of its sub-directories). A service worker file located at /js/sw.js would only work for files in the /js directory. As a result, you should place your service worker file inside the root directory of your site.

While service workers have fantastic browser support, it’s a good idea to make sure the browser supports them before running your registration script.

if (navigator && navigator.serviceWorker) {
  navigator.serviceWorker.register('sw.js');
}

After the service worker installs, the browser can activate it. Typically, this only happens when…

  • there is no service worker currently active, or
  • the user refreshes the page.

The service worker won’t run or intercept requests until it’s activated.

Listening for requests in a service worker

Once the service worker is active, it can start intercepting requests and running other tasks. We can listen for requests with self.addEventListener() and the fetch event.

// Listen for request events
self.addEventListener('fetch', function (event) {
  // Do stuff...
});

Inside the event listener, the event.request property is the request object itself. For ease, we can save it to the request variable.

Certain versions of the Chromium browser have a bug that throws an error if the page is opened in a new tab. Fortunately, there’s a simple fix from Paul Irish that I include in all of my service workers, just in case:

// Listen for request events
self.addEventListener('fetch', function (event) {

  // Get the request
  let request = event.request;

  // Bug fix
  // https://stackoverflow.com/a/49719964
  if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') return;

});

Once your service worker is active, every single request is sent through it, and will be intercepted with the fetch event.

Service worker strategies

Once your service worker is installed and activated, you can intercept requests and responses, and handle them in various ways. There are two primary strategies you can use in your service worker:

  1. Network-first. With a network-first approach, you pass along requests to the network. If the request isn’t found, or there’s no network connectivity, you then look for the request in the service worker cache.
  2. Offline-first. With an offline-first approach, you check for a requested asset in the service worker cache first. If it’s not found, you send the request to the network.

Network-first and offline-first approaches work in tandem. You will likely mix-and-match approaches depending on the type of asset being requested.

Offline-first is great for large assets that don’t change very often: CSS, JavaScript, images, and fonts. Network-first is a better fit for frequently updated assets like HTML and API requests.

Strategies for caching assets

How do you get assets into your browser’s cache? You’ll typically use two different approaches, depending on the types of assets.

  1. Pre-cache on install. Every site and web app has a set of core assets that are used on almost every page: CSS, JavaScript, a logo, favicon, and fonts. You can pre-cache these during the install event, and serve them using an offline-first approach whenever they’re requested.
  2. Cache as you browser. Your site or app likely has assets that won’t be accessed on every visit or by every visitor; things like blog posts and images that go with articles. For these assets, you may want to cache them in real-time as the visitor accesses them.

You can then serve those cached assets, either by default or as a fallback, depending on your approach.

Implementing network-first and offline-first strategies in your service worker

Inside a fetch event in your service worker, the request.headers.get('Accept') method returns the MIME type for the content. We can use that to determine what type of file the request is for. MDN has a list of common files and their MIME types. For example, HTML files have a MIME type of text/html.

We can pass the type of file we’re looking for into the String.includes() method as an argument, and use if statements to respond in different ways based on the file type.

// Listen for request events
self.addEventListener('fetch', function (event) {

  // Get the request
  let request = event.request;

  // Bug fix
  // https://stackoverflow.com/a/49719964
  if (event.request.cache === 'only-if-cached' && event.request.mode !== 'same-origin') return;

  // HTML files
  // Network-first
  if (request.headers.get('Accept').includes('text/html')) {
    // Handle HTML files...
    return;
  }

  // CSS & JavaScript
  // Offline-first
  if (request.headers.get('Accept').includes('text/css') || request.headers.get('Accept').includes('text/javascript')) {
    // Handle CSS and JavaScript files...
    return;
  }

  // Images
  // Offline-first
  if (request.headers.get('Accept').includes('image')) {
    // Handle images...
  }

});

Network-first

Inside each if statement, we use the event.respondWith() method to modify the response that’s sent back to the browser.

For assets that use a network-first approach, we use the fetch() method, passing in the request, to pass through the request for the HTML file. If it returns successfully, we’ll return the response in our callback function. This is the same behavior as not having a service worker at all.

If there’s an error, we can use Promise.catch() to modify the response instead of showing the default browser error message. We can use the caches.match() method to look for that page, and return it instead of the network response.

// Send the request to the network first
// If it's not found, look in the cache
event.respondWith(
  fetch(request).then(function (response) {
    return response;
  }).catch(function (error) {
    return caches.match(request).then(function (response) {
      return response;
    });
  })
);

Offline-first

For assets that use an offline-first approach, we’ll first check inside the browser cache using the caches.match() method. If a match is found, we’ll return it. Otherwise, we’ll use the fetch() method to pass the request along to the network.

// Check the cache first
// If it's not found, send the request to the network
event.respondWith(
  caches.match(request).then(function (response) {
    return response || fetch(request).then(function (response) {
      return response;
    });
  })
);

Pre-caching core assets

Inside an install event listener in the service worker, we can use the caches.open() method to open a service worker cache. We pass in the name we want to use for the cache, app, as an argument.

The cache is scoped and restricted to your domain. Other sites can’t access it, and if they have a cache with the same name the contents are kept entirely separate.

The caches.open() method returns a Promise. If a cache already exists with this name, the Promise will resolve with it. If not, it will create the cache first, then resolve.

// Listen for the install event
self.addEventListener('install', function (event) {
  event.waitUntil(caches.open('app'));
});

Next, we can chain a then() method to our caches.open() method with a callback function.

In order to add files to the cache, we need to request them, which we can do with the new Request() constructor. We can use the cache.add() method to add the file to the service worker cache. Then, we return the cache object.

We want the install event to wait until we’ve cached our file before completing, so let’s wrap our code in the event.waitUntil() method:

// Listen for the install event
self.addEventListener('install', function (event) {

  // Cache the offline.html page
  event.waitUntil(caches.open('app').then(function (cache) {
    cache.add(new Request('offline.html'));
    return cache;
  }));

});

I find it helpful to create an array with the paths to all of my core files. Then, inside the install event listener, after I open my cache, I can loop through each item and add it.

let coreAssets = [
  '/css/main.css',
  '/js/main.js',
  '/img/logo.svg',
  '/img/favicon.ico'
];

// On install, cache some stuff
self.addEventListener('install', function (event) {

  // Cache core assets
  event.waitUntil(caches.open('app').then(function (cache) {
    for (let asset of coreAssets) {
      cache.add(new Request(asset));
    }
    return cache;
  }));

});

Cache as you browse

Your site or app likely has assets that won’t be accessed on every visit or by every visitor; things like blog posts and images that go with articles. For these assets, you may want to cache them in real-time as the visitor accesses them. On subsequent visits, you can load them directly from cache (with an offline-first approach) or serve them as a fallback if the network fails (using a network-first approach).

When a fetch() method returns a successful response, we can use the Response.clone() method to create a copy of it.

Next, we can use the caches.open() method to open our cache. Then, we’ll use the cache.put() method to save the copied response to the cache, passing in the request and copy of the response as arguments. Because this is an asynchronous function, we’ll wrap our code in the event.waitUntil() method. This prevents the event from ending before we’ve saved our copy to cache. Once the copy is saved, we can return the response as normal.

/explanation We use cache.put() instead of cache.add() because we already have a response. Using cache.add() would make another network call.

// HTML files
// Network-first
if (request.headers.get('Accept').includes('text/html')) {
  event.respondWith(
    fetch(request).then(function (response) {

      // Create a copy of the response and save it to the cache
      let copy = response.clone();
      event.waitUntil(caches.open('app').then(function (cache) {
        return cache.put(request, copy);
      }));

      // Return the response
      return response;

  }).catch(function (error) {
      return caches.match(request).then(function (response) {
        return response;
      });
    })
  );
}

Putting it all together

I’ve put together a copy-paste boilerplate for you on GitHub. Add your core assets to the coreAssets array, and register it on your site to get started.

If you do nothing else, this will be a huge boost to your site in 2022.

But there’s so much more you can do with service workers. There are advanced caching strategies for APIs. You can provide an offline page with critical information if a visitor loses their network connection. You can clean up bloated caches as the user browses.

Jeremy Keith’s book, Going Offline, is a great primer on service workers. If you want to take things to the next level and dig into progressive web apps, Jason Grigsby’s book dives into the various strategies you can use.

And for a pragmatic deep dive you can complete in about an hour, I also have a course and ebook on service workers with lots of code examples and a project you can work on.

Strut Your Stuff With a Custom Scrollbar

The first time I had my breath taken away by a humble scrollbar was on this very site. When CSS-Tricks v17 rolled out with its FAT CHONKY BOI, my jaw dropped.

I didn’t know you could do that on a professional site. And it would look… good?!

I appreciated so much about it—the gentle gradient, the reckless rounding, the blended background, the sheer satisfying CHONKINESS that dares you to click and wiggle it up and down just to marvel in its tactile heft. How bold! How avant-garde! What sheer, accessible, gracefully degrading delight!

Of course, because fun doesn’t last, the current CSS Tricks scrollbar is more grown-up and muted, light gray on black. Still on brand, still flexing subtle gradient muscle, but not so distracting that it detracts from the reading experience. In our ultra-functional world of MVPs and 80/20 rules, maximizing efficiency and hacking productivity, custom scrollbars evince something about craftsmanship. It says with no words what you can’t in a hundred.

Thanks to some standardization (with more on the way), the API is simple: seven pseudo-elements and eleven pseudo-classes that target (almost) every imaginable component and state of the trusty (and often overlooked) scrollbar. Sounds like a lot, but you can go very far with just three of them:

body::-webkit-scrollbar {
  /* required - the "base" of the bar - mostly for setting width */
}
 
body::-webkit-scrollbar-track {
  /* the "track" of the bar - great for customizing "background" colors */
}
 
body::-webkit-scrollbar-thumb {
  /* the actual draggable element, the star of the show! */
}

From here, it works like any other selected element, so bring your full bag of single div CSS tricks! Media queries work! Background gradients work! Transparency works! Margins with all manner of CSS units work! (Not everything works… I’d love to style cursor on my scrollbars for that authentic GeoCities look). I tried it out on my site with Lea Verou’s stash of CSS background gradients (my stash of stashes is here) and ended up with an atrocious combo of stripy barber pole (💈) for the thumb element and transparent hearts for the track. But it was most definitely mine—so much so that people have taken to calling it the “swyxbar” when I implemented a subtler version at work.

Every front-end developer should take this too far at least once in their careers. Live dangerously! Break the rules! Rage against the user agent! And maybe don’t ship scrollbars that break user expectations on a mass-market product (like Google Wave did back in the day)!

Remember You Are Not the User

One thing people can do to make their websites better is to remember that you are not representative of all your users. Our life experiences and how we interact with the web are not indicative of how everyone interacts with the web.

We must care about accessibility.

Some users rely on assistive technology to navigate web pages. 

We must care about multi-language support.

Layouts that make sense to me, as a native English speaker (a left-to-right language) don’t necessarily make sense in Arabic (a right-to-left language) and can’t simply be swapped from a content perspective.

We must care about common/familiar UX paradigms.

What may be obvious to you may not be obvious to other users.

Take the time to research your key user markets and understand how these users expect your website or product to function. Don’t forget accessibility. Don’t forget internationalization. And don’t forget that you are not the representation of all users.

Make Joyful Things

Everything kind of sucks right now. Things—generally—feel bad. Setting aside the broader realities of a global pandemic and rampant social injustices, we’re watching the identity of the web platform, an intrinsically free and open medium of creative expression, co-opted for the “Web 3.0” grift built on artificial scarcity and an accelerating climate crisis.

Websites (mostly) look kind of the same. The proliferation of frameworks, vector design tools, data-driven design and an imperative to maximize business value (oh dear, it was capitalism all along) has led to a status quo of design by default. Websites having their own, consistent, portable UX language that helps users quickly comprehend new sites is great, but embracing that language is a trade-off, and design doldrums is the cost.

What if it didn’t have to be this way? What if we decided to create joy with our work? After all, in a profoundly dark time for many, making someone smile or laugh with your work is a special thing. Shouldn’t we try to do that if we can?

“Okay, you got me, but how?” I hear you ask.

Well, this is how:

The web community is bursting with folks who create joy with their work. Let’s look at how they do it.

Unexpected interactions

Did you know that the web is an interactive medium? I know, weird, right? People get an “oooh neat” feeling when something responds to them. Cats knock stuff off shelves, people hover and click on stuff. It’s simple biology (probably; I’m not a biologist).

Cassie Evans puts the “virtuoso” in SVG (the V stands for virtuoso) (it does not) with this animated portrait on her personal site. I absolutely adore how it tracks the cursor in a natural-feeling way, and I love how her expression changes when you hover an interactive element.

I loved this little hover interaction from Josh Comeau’s site. I thought that there might be a hidden interaction in this 3D illustration, but I was surprised to see the background react to my cursor. As far as I can tell, it doesn’t serve a “purpose” and it cost me a few precious moments I could have spent Engaging With Content, but it made me smile.

This little pop-up book-esque pull tab on Lynn Fisher’s site is lovely. I’m sure a click or scroll would be easier to implement and get the job done, but this is special. It’s different, it’s unexpected. It’s fun. Every single iteration of her site is excellent, so I encourage you to check out her archive.

Animation

We’re finally at the place where motion is baked into design systems and considered (nearly) as important as your typefaces and colors. Despite this, we can still use animation to elevate an interaction from neat to joyful.

Another banger from Josh Comeau: this little interaction on his blog’s Like button. It could just be a ❤️ icon, sure, but it’s full of delightful personality, with lots of appeal and secondary action. The bouncy easing curve and the exuberant expression creates appeal, while the +1 and confetti at the end add some welcome secondary action. This even reinforces the experience because it encourages folks to hit that little button. Go try it out, there are also some wonderful little sound effects!

3D

Between WebGL and CSS 3D, pushing the web beyond two dimensions has been possible for ages. Good news is that it’s still a specialized skill and sometimes a pain in the ass, so it’s still rare. Even after “3D illustration” became all the rage, interactive 3D elements are still a great way to inject a little personality.

lepuzz.com is a joyful website throughout, but I especially love the spinning product images you get on hover.

Internet cryptid “Henry” “Desroches” adds a bit of personality with these animated 3D flags that appear when hovering over his case studies. He uses a noise texture to displace vertices on a plane, which creates this lovely, soothing effect.

Don’t turn this into a listicle, let’s wrap this up

  • Break the rules!
  • Use weird colors!
  • Put animations and interactions where they don’t belong!
  • Use sound effects!

The web is as much a cultural artifact as it is a platform for tricking people into looking at ads. We have the power to make people happy with our work; to share the love in a literal sense.

Understand that prioritizing joy will have a cost. Like everything else involved with making things, its a tradeoff. It might make someone’s computer fans turn on which will make some people—who are not in the pocket of Big Fan—extremely upset. It might be slower, and less usable for folks on slower devices. Creating joy will take time that you can’t spend on maximizing key metrics and synergizing upward overflow; mitigating the negative impacts of these tradeoffs will eat yet more time. There’s a reason that almost all of the above examples are from personal sites and not big Brands, after all.

What if making somebody smile is worth it though? What if it’s worth the cost? In a lot of ways, we’re living in shitty times. It’s worth giving up a little to take your shot at making someone’s day.

Maybe Nothing

What’s one thing we can do to make our site better? Maybe nothing at all!

Our websites keep getting bigger and bigger! When we have a team with so many exciting ideas and such interesting technology, it can be easy to get swept up and not as easy to prioritize all those ideas.  

However, there are only so many hours in a day, only so many resources, and only so much our visitors want in an interface without their focus being lost. How do we balance it all?

There are many prioritization tools out there. Recently, my favorite has been to ask yourself (and/or your team) when deciding to add something new to your site: What happens if I don’t do this right now? 

When we add new things to our site, sometimes we jump right into the weeds and discuss:

  • KPIs/OKRs: What kind of return will this feature have?
  • Performance: Will this new code help or hurt our performance metrics?
  • Accessibility: Will all of our customers be able to utilize this new feature?
  • Maintenance: Do we have the resources to maintain this new component?
  • User Experience: How will this affect our visitors?

Conversations around these factors can be necessary, but overwhelming sometimes. Before we even dig too deep into any of these areas, first asking what happens if we don’t do it, can help us really determine the priority and necessity of the task. 

Let’s say we brainstorm three ways to improve our modals:

Example 1: Lazy load our modal content

What happens if we don’t remove our modals from the initial page load right now? Our visitors are downloading content they might not ever request, perhaps even blocking other critical content. We should add this to our priorities and investigate paths forward.

Example 2:  Add an animation on modal close

What happens if we don’t add this animation right now? The close experience might be a bit jarring for our customers without an animation, but maybe not? Will it change conversion? Will it make our readers not come back to our site? Probably not. Let’s keep our priority hats on and move this to our non-urgent/non-important column and revisit someday if we have the bandwidth. 

Example 3: Our close buttons are missing aria-labels and we don’t retain keyboard focus within the modal. 

What happens if we don’t address this right now? Um, what are you doing here? Stop reading this article and go do it! 

There are many ways to plan your website roadmap and improve our current experiences. This is just one question to ask upfront to help drive discussions around priorities and focus.

Bonus: Audit your site and ask: what happens if we don’t keep this component? Does this SVG animation really spark joy? (That’s a trap… of course, it does!)

This is by no means an invitation to say “no” to every ask that comes in. If we didn’t iterate and try new things, our websites would grow stale, slow, and outdated. But it’s another way to open communication with your team (or your inner monologue) and explore the impact of new code on your beautiful, fast, accessible site. 

Consistent, Fluidly Scaling Type and Spacing

When Chris first sent me this prompt, I was thinking about writing about progressive enhancement, but that subject is so wide-reaching to be one thing and all too predictable, especially for those already familiar with my writing. Saying that, what I’m going to outline in this article also isn’t just one thing either, but the day I meet a writing prompt exactly will be the day pigs start flying. This one group of things, though, will change how you write CSS.

I personally think this group of things lets a lot of sites down—especially in a responsive design sense. The things in this group are typography and spacing. So often, I see inconsistent spacing—especially vertically—that makes content hard to scan and creates this subtle, disjointed feeling. The same goes for type: huge headings on small viewports, or heading hierarchies that visually have no contrast in size, rendering them useless in a visual sense.

There is a pretty easy fix for all of this using a sizing scale and fluid type, and I promise it’ll make your websites look and feel heaps better. Let’s get into it.

What the heck is a sizing scale?

A sizing scale is a uniform progression of sizes based on a scale—or, more accurately, a ratio.

Screensjhot of the type-scale.com type scale tool. It displays eight variations of font sizes in black on a white background starting from largest to smallest vertically. To the left of the examples are options to configure the output, including base font size, type of scale, Google font selection, and preview text.

In that screenshot of type-scale.com, I’ve selected a “Perfect Fourth” scale which uses a ratio of 1.333. This means each time you go up a size, you multiply the current size by 1.333, and each time you go down a size, you divide by 1.333.

If you have a base font size of 16px, using this scale, the next size up is 16 * 1.333, which is 21.33px. The next size up is 21.33 * 1.333, which is 28.43px. This provides a lovely curve as you move up and down the scale.

CSS clamp() and type fluidity

For years, if you were to say, “Hey Andy, what’s your favorite CSS feature?” I would immediately say flexbox, but nah, not these days. I am a clamp() super fan. I wrote about it in more detail here, but the summary of clamp() is that it does clever stuff based on three parameters you give it:

  • a minimum value
  • an ideal value
  • a maximum value

This makes for a very useful tool in the context of fluid typography and spacing, because you write CSS like this:

.my-element {
  font-size: clamp(1rem, calc(1rem * 3vw), 2rem);
}

This tiny bit of CSS gives us full responsive text sizes based on the viewport width with handy locks to make sure sizes don’t get too big or too small.

It’s really important to test that your text is legible when you zoom in and zoom out when using clamp. It should be very obviously larger or smaller. Because we’re using a rem units as part of our fluid calculation, we’re helping that considerably.

Putting it all together

Right, so we’ve got a size scale and CSS clamp() all set up—how does it all come together? The smart people behind Utopia came up with the simplest, but handiest of approaches. I use their type tool and their spacing tool to create size scales for small and large viewports. Then, using clamp(), I generate a master size scale that’s completely fluid, as well as a Sass map that informs Gorko’s configuration.

$gorko-size-scale: (
  '300': clamp(0.7rem, 0.66rem + 0.2vw, 0.8rem),
  '400': clamp(0.88rem, 0.83rem + 0.24vw, 1rem),
  '500': clamp(1.09rem, 1rem + 0.47vw, 1.33rem),
  '600': clamp(1.37rem, 1.21rem + 0.8vw, 1.78rem),
  '700': clamp(1.71rem, 1.45rem + 1.29vw, 2.37rem),
  '800': clamp(2.14rem, 1.74rem + 1.99vw, 3.16rem),
  '900': clamp(2.67rem, 2.07rem + 3vw, 4.21rem),
  '1000': clamp(3.34rem, 2.45rem + 4.43vw, 5.61rem)
);

This snippet is from my site, piccalil.li, and the typography is super simple to work with because of it.

You could also translate that into good ol’ CSS Custom Properties:

:root {
  --size-300: clamp(0.7rem, 0.66rem + 0.2vw, 0.8rem);
  --size-400: clamp(0.88rem, 0.83rem + 0.24vw, 1rem);
  --size-500: clamp(1.09rem, 1rem + 0.47vw, 1.33rem);
  --size-600: clamp(1.37rem, 1.21rem + 0.8vw, 1.78rem);
  --size-700: clamp(1.71rem, 1.45rem + 1.29vw, 2.37rem);
  --size-800: clamp(2.14rem, 1.74rem + 1.99vw, 3.16rem);
  --size-900: clamp(2.67rem, 2.07rem + 3vw, 4.21rem);
  --size-1000: clamp(3.34rem, 2.45rem + 4.43vw, 5.61rem);
};

This approach also works for much larger sites, too. Take the new web.dev design or this fancy software agency’s site. The latter has a huge size scale for large viewports and a much smaller, more sensible, scale for smaller viewports, all perfectly applied and without media queries.

I’m all about keeping things simple. This approach combines a classic design practice—a sizing scale—and a modern CSS feature—clamp()—to make for much simpler CSS that achieves a lot.

Remove Trackers

Earlier this week, I tried out a starter theme for a blog platform. The theme had loads of nice default features: pretty typography, fancy navigation, dark mode widget… and a couple of default trackers I really don’t want just sitting there in a header component, waiting for me to add my account information.

As web development has become increasingly complex, more starters, frameworks, and embeddable tools have been created to simplify our developer experience. Just paste this one line of code into the <head> of your site, and you’ll be a 10× full stack developer in no time. Sometimes we’ll pull out a feature we don’t want or the code we don’t need, but who has the time for a line-by-line review? If you got a feature for free, you might as well use it!

Over-simplifying our setup is risky. When we don’t fully understand what we’ve embedded on our site, we give up control of that feature to an unknown third party. We assume the maintainer knows best because the repository has a load of stars on GitHub or because a big name uses that same script on their site. Somebody must have checked this package is legit, right?

The malicious risk

Malicious scripts for password jacking and other nefarious purposes are sometimes found in popular npm packages. Cryptojacking, where crypto miners are installed on your site without your knowledge, are more common. Just recently, Alibaba Cloud services were targeted to mine the Monero cryptocurrency. If we’re a customer of a hacked service, we might hope our provider lets us know if our site is hacked in a timely fashion. If it’s an open source package, we’ve just got to hope we’re online when someone discovers the vulnerability so we can get our sites updated quickly.

The reliability risk

Many third parties we rely on for critical features are just incompetent or unreliable. We make jokes when some big internet infrastructure goes down and leaves us with no choice but to take it easy at work, but it’s not all fun and games for sites providing vital utilities and information for people.

For many years, we’ve been sold on third-party solutions to improve performance issues on our sites. And, occasionally, you will find a service that genuinely serves your site faster and in more locations across the globe than you can throw together yourself. But the majority of the most popular sites on the web have way more than one third-party script embedded on their site. From my work on Better Blocker, I can tell you that around ten third-party scripts is low, and as many as thirty on one homepage is common, especially on news sites. Does that many third-party scripts on one page have a positive impact on performance?

The privacy risk

Whether or not the third-party feature we’ve installed on our site is nefarious, incompetent, unreliable, or does its job, it’s always a privacy risk for our site visitors.

In a world where developer experience is often the priority, it’s too easy to forget we’re using these tools to build experiences for other people. And we have a responsibility to build experiences that don’t put our site’s visitors at risk.

Any third-party script, or any resource that can log visitor information, can be considered a tracker. At best, it has tracker potential. Your analytics, fonts, iframes, content delivery networks, CAPTCHAs — they all have the potential to collect information about your site’s visitors. What information, how much, and how often depends on what the feature does and the access you’ve provided it. That information collected about an individual could be used to sell them ads, build sellable profiles of them or even be used to discriminate against them.

I know privacy isn’t a popular topic in the web community. It feels like we’re already near the bottom of the slippery slope… and sometimes it’s easier to give up than to address how reliant we are on privacy-exploiting funding models. But there are small changes we can make to protect our visitors, even if we simply start with our own personal projects.

1. Review the third-party tools you use

Do you really need two different analytics scripts on your site? Can you embed that font locally instead? Reviewing the tools you already use gives you a manageable way to improve the privacy of your project a little at a time. And you’ll get a bonus when your site’s performance improves.

2. Use privacy-respecting alternatives

Over the last few years, privacy-respecting alternatives to mainstream technology have become more popular. One of my favorite sites is switching.software, which helps you find alternatives to the popular tools you use every day. Good Reports is another one that explains the reasoning behind each recommendation.

Privacy isn’t as hard as giving up the defaults

In this post, I decided not to go into the legal issues around privacy on the web. Making sites that adhere to laws and regulations around respecting rights is essential, but we’re more likely to make great experiences for the people using our sites if we care about their privacy, rather than worrying about what we can get away with on the legal side.

Privacy is not as hard as giving up the defaults, the tools that save us time, or uncritically copying our colleagues’ approaches. But removing one tracker at a time, we can make a difference.

Test Your Product on a Crappy Laptop

There is a huge and ever-widening gap between the devices we use to make the web and the devices most people use to consume it. It’s also no secret that the average size of a website is huge, and it’s only going to get larger.

What can you do about this? Get your hands on a craptop and try to use your website or web app.

Craptops are cheap devices with lower power internals. They oftentimes come with all sorts of third-party apps preinstalled as a way to offset its cost—apps like virus scanners that are resource-intensive and difficult to remove. They’re everywhere, and they’re not going away anytime soon.

As you work your way through your website or web app, take note of:

  • what loads slowly,
  • what loads so slowly that it’s unusable, and
  • what doesn’t even bother to load at all.

After that, formulate a plan about what to do about it.

The industry average

At the time of this post, the most common devices used to read CSS-Tricks are powerful, modern desktops, laptops, tablets, and phones with up-to-date operating systems and plenty of computational power.

Granted, not everyone who makes websites and web apps reads CSS-Tricks, but it is a very popular industry website, and I’m willing to bet its visitors are indicative of the greater whole.

In terms of performance, the qualities we can note from these devices are:

  • powerful processors,
  • generous amounts of RAM,
  • lots of storage space,
  • high-quality displays, and most likely a
  • high-speed internet connection

Unfortunately, these qualities are not always found in the devices people use to access your content.

Survivor bias

British soldiers in World War I were equipped with a Brodie helmet, a steel hat designed to protect its wearer from overhead blasts and shrapnel while conducting trench warfare. After its deployment, field hospitals saw an uptick in soldiers with severe head injuries.

A grizzled British soldier smiling back at the camera, holding a Brodie helmet with a large hole punched in it. Black and white photograph.
Source: History Daily

Because of the rise in injuries, British command considered going back to the drawing board with the helmet’s design. Fortunately, a statistician pointed out that the dramatic rise in hospital cases was because people were surviving injuries that previously would have killed them—before the introduction of steel the British Army used felt or leather as headwear material.

Survivor bias is the logical error that focuses on those who made it past a selection process. In the case of the helmet, it’s whether you’re alive or not. In the case of websites and web apps, it’s if a person can load and use your content.

Lies, damned lies, and statistics

People who can’t load your website or web app don’t show up as visitors in your analytics suite. This is straightforward enough.

However, the “use” part of “load and use your content” is the important bit here. There’s a certain percentage of devices who try to access your product that will be able to load enough of it to register a hit, but then bounce because the experience is so terrible it is effectively unusable.

Yes, I know analytics can be more sophisticated than this. But through the lens of survivor bias, is this behavior something your data is accommodating?

Blame

It’s easy to go out and get a cheap craptop and feel bad about a slow website you have no control over. The two real problems here are:

  1. Third-party assets, such as the very analytics and CRM packages you use to determine who is using your product and how they go about it. There’s no real control over the quality or amount of code they add to your site, and setting up the logic to block them loading their own third-party resources is difficult to do.
  2. The people who tell you to add these third-party assets. These people typically aren’t aware of the performance issues caused by the ask, or don’t care because it’s not part of the results they’re judged by.

What can we do about these two issues? Tie abstract, one-off business requests into something more holistic and personal.

Bear witness

I know of organizations who do things like “Testing Tuesdays,” where moderated usability testing is conducted every Tuesday. You could do the same for performance, even thread this idea into existing usability testing plans—slow websites aren’t usable, after all.

The point is to construct a regular cadence of seeing how real people actually use your website or web app, using real world devices. And when I say real world, make sure it’s not just the average version of whatever your analytics reports says.

Then make sure everyone is aware of these sessions. It’s a powerful thing to show a manager someone trying to get what they need, but can’t because of the choices your organization has made.

Craptop duty

There are roughly 260 work days in a year. That’s 260 chances to build some empathy by having someone on your development, design, marketing, or leadership team use the craptop for a day.

You can run Linux from a Windows subsystem to run most development tooling. Most other apps I’m aware of in the web-making space have a Windows installer, or can run from a browser. That should be enough to do what you need to do. And if you can’t, or it’s too slow to get done at the pace you’re accustomed to, well, that’s sort of the point.

Craptop duty, combined with usability testing with a low power device, should hopefully be enough to have those difficult conversations about what your website or web app really needs to load and why.

Don’t tokenize

The final thing I’d like to say is that it’s easy to think that the presence of a lower power device equals the presence of an economically disadvantaged person. That’s not true. Powerful devices can become circumstantially slowed by multiple factors. Wealthy individuals can, and do, use lower-power technology.

Perhaps the most important takeaway is poor people don’t deserve an inferior experience, regardless of what they are trying to do. Performant, intuitive, accessible experiences on the web are for everyone, regardless of device, ability, or circumstance.

Ain’t No Party Like a Third Party

I’d like to tell you something not to do to make your website better. Don’t add any third-party scripts to your site.

That may sound extreme, but at one time it would’ve been common sense. On today’s modern web it sounds like advice from a tinfoil-hat-wearing conspiracy nut. But just because I’m paranoid doesn’t mean they’re not out to get your user’s data.

All I’m asking is that we treat third-party scripts like third-party cookies. They were a mistake.

Browsers are now beginning to block third-party cookies. Chrome is dragging its heels because the same company that makes the browser also runs an advertising business. But even they can’t resist the tide. Third-party cookies are used almost exclusively for tracking. That was never the plan.

In the beginning, there was no state on the web. A client requested a resource from a server. The server responded. Then they both promptly forgot about it. That made it hard to build shopping carts or log-ins. That’s why we got cookies.

In hindsight, cookies should’ve been limited to a same-origin policy from day one. That would’ve solved the problems of authentication and commerce without opening up a huge security hole that has been exploited to track people as they moved from one website to another. The web went from having no state to having too much.

Now that vulnerability is finally being closed. But only for cookies. I would love it if third-party JavaScript got the same treatment.

When you add any third-party file to your website—an image, a stylesheet, a font—it’s a potential vector for tracking. But third-party JavaScript files go one further. They can execute arbitrary code.

Just take a minute to consider the implications of that: any third-party script on your site is allowing someone else to execute code on your web pages. That’s astonishingly unsafe.

It gets better. One of the pieces of code that this invited intruder can execute is the ability to pull in other third-party scripts.

You might think there’s no harm in adding that one little analytics script. Or that one little Google Tag Manager snippet. It’s such a small piece of code, after all. But in doing that, you’ve handed over your keys to a stranger. And now they’re welcoming in all their shady acquaintances.

Request Map Generator is a great tool for visualizing the resources being loaded on any web page. Try pasting in the URL of an interesting article from a news outlet or magazine that someone sent you recently. Then marvel at the sheer size and number of third-party scripts that sneak in via one tiny script element on the original page.

That’s why I recommend that the one thing people can do to make their website better is to not add third-party scripts.

Easier said than done, right? Especially if you’re working on a site that currently relies on third-party tracking for its business model. But that exploitative business model won’t change unless people like us are willing to engage in a campaign of passive resistance.

I know, I know. If you refuse to add that third-party script, your boss will probably say, “Fine, I’ll get someone else to do it. Also, you’re fired.”

This tactic will only work if everyone agrees to do what’s right. We need to have one another’s backs. We need to support one another. The way people support one another in the workplace is through a union.

So I think I’d like to change my answer to the question that’s been posed.

The one thing people can do to make their website better is to unionize.

Test Your Site With Real Users

A few years ago, there was this French book publisher. They specialize in technical books and published an author who wrote a book about CSS3, HTML5 and jQuery. The final version, however, a glaring typo on the cover where “HTML5” was displayed as “HTLM5.” Read that twice. Yes. “HTLM5.” (Note that it was also missing the capitalized “Q” in jQuery in one version.)

Image of the book containing the typo. It has three cartoonish figures on it dressed as superheroes, then a product description of the book to the right of the cover.

I don’t know how many people are involved in publishing and printing a book. I bet quite a few. Yet, it looked like none of the people involved saw the typo. It made it to the printer, after all.

And this kind of thing happens all the time on projects. One of my favorite French expressions is avoir la tête dans le guidon. A literal translation is “having your head in the handlebar.” (The English official version is having your nose in the grindstone.) It comes from cycling. When cyclists are trying to win a race, at some point, they end up with their nose so close to the handlebar that nothing else around them matters. They are hyper focused on the road ahead. They can’t see anything else around anymore.

Photo of a cyclist in a black helmet and red jacket on a black and blue racing bike riding through a busy intersection with a blurry backdrop indicating a fast speed.
Credit: Max Bender via Unsplash

And this is exactly what happens to us quite often on projects. We and our teams are so focused at some point on shipping the site (or printing the book) that we get blindfolded and fail to see little (or big) details anymore. This is how you ship a book about “HTLM5” and a website with navigation issues and dead ends in user flows, or features no one needs.

Gaining an external view with user testing

If you want to avoid these sorts of things, you need an external view of your site, product or service. And the best way to gain that view is to test it with people who are not on the team. We call this usability or user testing. I have to confess that I’m biased here since part of my job is to perform user testing on websites. So, I have to say that, ideally, you want to test with your target audience — the people who actually use your website, product, or service. But, if (and this is a big if) you can’t find any users, at least have a first round of tests with people who did not work directly on the project.

You also want to test with people with different impairments to make sure the end result is as accessible as possible.

When should I start testing my project?

In a perfect world, you test as soon and as often as possible. Testing prototypes built in design tools before starting development is cheaper. If the concept doesn’t work, at least you did not invest three months of development into an ineffective feature.

You can also test HTML/CSS/JavaScript prototypes with fake data built for the tests — or test once the feature or website is developed. This does mean, though, that any changes are more complex and expensive.

Define what you want to test

The first step is to define what specific tasks or activities you want to test. Usually, you want a set of different actions with a user goal at the end. For example:

  • an account creation process
  • a whole checkout process
  • a search process from the homepage to the final blog post, etc.

List the tasks and activities the user needs to accomplish in the form of questions. We call this a creating a test script. You can find an example here from 18F.

Be careful not to bias users. This is the tricky part. For example, if you want to test an account creation flow and the button says “Sign up,” then avoid asking your test users to “sign up” because the first thing they will do is search for a button with the same verb on the screen. You could ask them to “create an account” instead and gain better insights.

Screenshots of Axure and Word side by side.
Example of a protype build in Axure and a test script

Then prepare the prototype you want to test. As mentioned before, it can range from mockups with a click-through prototype to a fully-developed prototype with real data. It’s totally up to you and how far you are in the project. Just make sure it works (technically, I mean).

Recruit participants

You know who your users are on most of your projects. The question is: how can you reach out to them? There’s plenty of ways. You might go through support or salespeople with lists of possible participants. If it’s a broad target audience, you could recruit testers right where they are. Working on an e-commerce website that sells plants? Why not try visiting actual physical shops, online communities for gardeners, community gardens, Facebook groups, etc.

You can use social media to recruit participants as long as you recruit the right people who are prospective users of the site. This is why UX professionals use screeners. A screener is a set of questions you while recruiting (and when starting the test), to make sure you are working with someone who is in the target audience.

Note that participants are usually compensated for their time. It can be gift cards, maybe getting of your product, some really nice chocolate — something that encourages people to spend time with you in a way that thanks them.

If you struggle recruiting and have a budget, you can use professional user research recruitment websites like userinterviews.com or testingtime.com.

Schedule, set up, prepare

Once you successfully recruit participants for testing, schedule a meeting with them, including the testing date, time, and place. The test can be remote or face to face. I won’t detail the logistics here, but at some point, you will need help to set up an actual room or a virtual space for the testing. If it’s a physical room, make sure it’s calm and accessible for your users. If it’s remote, make sure the tools are accessible and people can install them if needed on their computers.

Schedule some emails in advance to remind participants the day before the test, just in case.

Last but not least: do a dry run of your test using people from your team. This helps avoid typos in the scripts and prototypes. You want to make sure the prototype works properly, that there are no technical issues, etc. You want to avoid anything that could bias the test.

Facilitate the test

You need two testers to conduct a usability test. One person facilitates. The other takes care of the logistics and notes.

Welcome the participant. You can find a lot of templates for usability testing over at usability.gov, including consent forms, email template examples, and much more.

Start the recording, but only if they give you permission to do so, of course. Explain that you are testing the site, not them, and that there are no right or wrong answers. Encourage them to think out loud, and to tell you exactly what they do, see, and think.

Put them at ease by starting with a few soft questions to get them to talk. Then follow your script.

The most important thing: don’t help users accomplish the tasks. I know, this is hard. We don’t like to see people struggle. But if you help them, you will bias the results. Of course, if they struggle for five minutes and you need them to accomplish the task to go to the next one, you can unlock them. Mark that particular task as “failed.”

Once testing is finished, thank the test user for their time and offer them the compensation (or tell them how to get compensated if it was a remote test).

Get the recording, upload it somewhere in the cloud so there is a backup. Same for your notes. Trust me on that, there’s nothing worse than losing some data because the computer crashed.

Analyze and document the results

After the test, I usually like to put together a quick “first draft” of the analysis for a given participant because the testing is still fresh in my mind.

Some people do this in shared documents or Excel sheets. My favorite method is using the actual screens that were used for testing in a Miro board. And I put digital sticky notes on them with the test’s main findings. I use different colors for different types of feedback, like a user comment, feature request, usability issue, etc.

When multiple users give the same feedback or experience the same issue, I add a small dot on the note. This way, I have a visual summary of everything that happened during all the tests.

Screenshot of mockup screens in Miro with notes attached to various areas of the screens. There are 13 total screens, each with different layouts and content.

And then? Learn, iterate, improve.

We don’t test for the fun of testing. We test to improve things. So, the next step is to learn from those tests. What worked well? What can be improved? How might we improve? Of course, you might not have the time and budget to improve everything at once. My advice is to prioritize and iterate. Fix the biggest issues first. “Big” is a relative term, of course, and that depends on your project or KPIs. It could mean “most users have this issue.” Or it could mean, “if this doesn’t get fixed, we will lose users and revenue.” This is when it becomes again, a team sport.

In conclusion

I hope I’ve convinced you to test your site soon and often. This was just a small introduction to the world of testing with real users. I simplified a lot in here to give you an idea of what goes into user testing. Proper professional usability testing is more complex, especially on large projects. While I always favor hiring someone dedicated to user research and testing, I also understand that it might be complicated for smaller projects.

If you want to go further, I recommend checking out the following resources:

Embrace the Unpredictable

In nature, no two things are ever the same. Life is imperfect, unpredictable, and beautiful. We can walk through the same forest every day and see differently colored leaves. We can look up at the clouds every minute and watch a whole new formation. The physical world is transient and ever-changing. What if our designs were a little more like this?

Often, we spend hours, weeks, even months carefully crafting our websites/applications, sculpting every last pixel until they are just right. Then, we set them free into the world — a perfectly formed, yet static snapshot of something that once was a living, evolving thing.

There is (of course!) nothing wrong with this way of working. But what if we let go of the idea that there can be only one final version of a design? What if our interfaces were free to take more than one form?

I could write forever about this stuff, but I think it’s best to show you what I mean. Naturally, here’s a Pen:

Try clicking the “Regenerate” button above. Notice how the interface changes just a little every time? By parameterizing aspects of a design, then randomizing those parameters, we can create near-infinite variations of a single idea. For those familiar with generative art — art made using a system that includes an element of autonomy — this is likely a familiar concept.

For makers (particularly perfectionists like me!), this approach to design can be incredibly liberating.

For the folk who use the things we make, it creates an experience that is truly individual. In randomizing carefully chosen aspects of our interfaces, they become ephemeral, and to me, this is kind of magical. No two people will ever see the same version of our work.

The web can be a cold, sterile place. By embracing the unpredictable, we can add a joyful, organic touch to our creations — to me, this is the essence of generative UI design, and I would love if you gave it a try! SVG, Canvas, and CSS/Paint API are all excellent mediums for generative work, so pick the one that is most familiar and experiment.

Just remember: apply carefully, and always be mindful of accessibility/UX. Magically evolving designs are great, but only if they are great for everyone.

Exactly What You Want

What is one thing people can do to make their website better?

Exactly what you want to build!

Ask yourself:

  • What drew you to development in the beginning?
  • Is there an experimental API that you’ve been wanting to try out?
  • What could you spend all night hacking away at, just for the fun of it?

Your personal site is a statement of who you are and what you want to do. If you showcase your favorite type of work, you’ll get more requests for similar projects or jobs — feeding back into a virtuous cycle of doing more of what you love.

Like stage performances, you can tell when love and excitement went into creating a website. One of my favorite examples is Cassie Evans’ website. She added so many fun flourishes (including an adorable SVG self-portrait). The joy baked into her work has (at least partially) led to her current role, bestowing animation superpowers at GreenSock!


So, go forth, and create a trailing mouse cursor. Or a confetti component! A real-time drawing pad, or some hardware to show the current state of your coffee machine. Really, anything that gets you excited to build!