GeistHaus
log in · sign up

https://iainbean.com/feed.xml

atom
33 posts
Polling state
Status active
Last polled May 18, 2026 19:56 UTC
Next poll May 19, 2026 22:16 UTC
Poll interval 86400s
ETag "2c1eba561b6af08073321537af6ae8dd-ssl-df"

Posts

Five years of component.gallery
Show full content

This month marks five years since the first deployment of component.gallery, so I thought I’d give an update on the numbers so far and my plans for the site.

The Component Gallery started as a hobby project: an opportunity to learn new tech and something to talk about in job interviews. It was launched with zero fanfare and apart from some ‘dogfooding’[1] by me and a couple of colleagues, it was just another side project. I didn’t expect it to find an audience, but it did. Seeing my work featured on the homepage of Smashing Magazine, being named as the 17th “Hottest Frontend-Tool” of 2021 on CSS-Tricks, and receiving lots of kind messages from strangers on the internet has given me the motivation to keep working on it.

The numbers Data points

Screenshot of the Airtable interface showing a table called Component Examples. Rows include URL, Name, Design system, Type of component.

At the time of writing the site includes:

  • 57 components
  • 94 design systems
  • 2,628 component examples

These figures don’t include the many unpublished systems that I haven’t got round to auditing, nor the nearly 1,000 component examples that don’t fit neatly into one of my 57 component-shaped holes. It also excludes the systems that I’ve had to remove because they’re no longer publicly available.

Visitors

A chart showing visitors over time: Unique visitors 834k, total visits 990k, total pageviews 2m. The chart plots visitors over time starting in 2020 and ending in early 2024. There is a general upward trend with a peak in October 2022.

I use Plausible, which like all client-side analytics, can easily be circumvented by blockers. However, it’s good enough to show trends. The spikes generally correspond with popular social media posts but on an average weekday, the site receives one to two thousand visitors.

A chart with the following columns: Source, Visitors, Bounce rate,	Visit duration. Data: Direct / None, 594k, 57%, 1m 34s; Google, 104k, 63%, 1m 46s; Twitter, 41.7k, 44%, 1m 20s; LinkedIn, 29.2k, 53%, 1m 11s; coliss.com, 7.1k, 36%, 1m 49s; statics.teams.cdn.office.net, 6.8k, 52%, 1m 52s; Facebook, 5.6k, 51%, 1m 16s; css-tricks.com, 2.7k, 32%, 2m 50s; Instagram, 1.8k, 69%, 31s; brunch.co.kr, 1.7k, 39%, 2m 11s;

What’s most interesting to me is the referrer data: direct traffic is the biggest driver of visitors with Google in a distant second. In terms of social networks, Twitter is still the largest source, with LinkedIn not far behind. Instagram is a surprising inclusion in the top 10, considering how much Meta discourage external links. The only conclusion I can draw from the high bounce rate and short visit duration is that Instagram users have much shorter attention spans than the average visitor.

The strangest inclusion to me is the statics.teams.cdn.office.net domain which appears to be a Content Delivery Network (CDN) used by Microsoft Teams or other related Microsoft services. As someone who is lucky enough to very rarely use Microsoft’s enterprise software products, I’m a little baffled why it appears so highly.

Costs

The running costs of the site consist of:

  • Domain: $24.16/year
  • Database: I was initially on the free Airtable plan but I quickly reached the record limit. It now costs me $9/month
  • Hosting: There have been months where I’ve approached the limits of Netlify’s free tier but it’s never cost me a penny: $0
  • Analytics: I’m not a huge fan of analytics in general but Plausible ’s “privacy-focused” approach is far less creepy than most of its competitors: $12/month

While I would love to get paid for my work on it, implementing ads or a paywall for bonus content feels like more hassle than it’s worth. I already have a full-time job, and maintenance costs are low enough that I’m fine to absorb them. I also worry that introducing monetisation could detract from my enjoyment, and make it start to feel like real work.

Out with the old…

Since its first iteration, the site has been built using Gatsby. At the time, Gatsby was a great option thanks to its unified data layer: after installing the gatsby-source-airtable plugin and updating a couple of env vars, I was up and running with a GraphQL API for my Airtable database (a feature which Airtable still don’t offer themselves). As time went on I added more features, which meant more plugins, sources, and transformers. At this point, the simplicity which attracted me to Gatsby had gone – updates became a huge chore as some packages got new versions while others fell by the wayside.

Not only was maintaining and updating a Gatsby site becoming harder, it also appeared to be losing favour as a platform for developers. Since purchasing Gatsby in February 2023, Netlify have turned the GatsbyJS.com homepage into a big ad for their services, laid off most of the team working on it[2], and spun out the clever data layer tech into Netlify Connect, a product seemingly targeted squarely at enterprise customers.

…and in with the new

I'm in the early stages of replatforming the website to Astro. This will coincide with a redesign of the site and some new features. The database for the site will continue to use Airtable.

The biggest barrier stopping me from moving away from Gatsby was the lack of migration path for the Airtable GraphQL API. I first looked at StepZen, a service which promised to let me "Declaratively build GraphQL APIs from backend building blocks". They even had a detailed instruction video on connecting up Airtable as a data source. But when trying to follow the instruction video, I found that all references to Airtable had been purged from the UI. Stranger still, certain links were taking me to a product on the IBM website called “IBM API Connect”. It turns out StepZen was purchased by IBM in 2023 and subsequently rebranded, while the original product appears to be neglected.

I’m currently using BaseQL, a service designed specifically for generating a GraphQL API from Airtable bases (or Google Sheets). It works well so far and has clear, reasonable pricing. I’ve got my fingers crossed that it sticks around (and the likes of IBM or Netlify can keep their grubby hands off it).

Upcoming features

The primary reason for replatforming the site is to make adding new features easier. Here are a few I’ve got planned (in no particular order):

A carousel component

Carousels are bad and you shouldn't use them, but that doesn’t stop me from getting frequent requests to add a component page for them. It would be irresponsible of me to add this without also including a few thousand words arguing against their use.

Differentiate between different types of system

Design Systems !== UI Frameworks !== Component Libraries

The current iteration of the site makes no distinction between these, apart from via the ‘Open Source’ feature badge – this is only an indicator that the code is public, not that it’s intended for use outside the original organisation.

Landing pages for design systems

I've always been reluctant to add pages for individual design systems because I've seen them as duplicating the functionality of the system's own site. However, the existing cards are getting cluttered and that’s before I've added more design system feature badges.

Better dark mode

As someone who actually doesn’t mind light mode, I often forget the site even has the option. It isn’t until I see other users sharing screenshots with dark mode enabled that I think, “that doesn't look very good!”. I’m working on a new dark mode colour scheme that's easier on the eyes.

Automated checking for broken links

I want to relieve users (who I'm very grateful to!) of the burden of finding broken links and shift it onto an automated process.

Thanks

Compared with early 2019 when I began compiling examples for the site, there are now many more sources available for researching components.

Here are some that I keep going back to:

  • The Design Systems News newsletter has always been a great resource which I’ve found indispensible for discovering new design systems.
  • The Storybook component encyclopedia has a great glossary section which includes an image for every single one of its thousands of component examples[3]. It also really helped me when trying to find Storybook links for design systems.
  • UI Guideline covers fewer components and systems than The Component Gallery, but it takes a meticulous approach, with in-depth analysis of naming, grouping, and component anatomy between systems. It looks like v2 has locked some content behind a paywall but v1 of the site has loads of great content, available for free.
  • I'm primarily a developer, not a designer, so the most useful examples for me are code examples. However, design systems are a lot more than code: Design Systems for Figma is great for exploring the Figma files created for big-name design systems.

Finally, I wanted to thank everyone who has contributed to the website: all the people who’ve submitted suggestions via the contact form (I do read them eventually!); those of you who have opened GitHub Issues; and everyone who has sent kind words or shared links on Twitter[4].


  1. The practice in which tech workers use their own product consistently to see how well it works and where improvements can be made. What Is ‘Dogfooding’? – New York Times ↩︎

  2. Tweet by @lekoarts_de 2023-08-20 ↩︎

  3. This is, unfortunately, only possible thanks to the consistent format of storybook stories. It’s a feature I’d love to include on The Component Gallery but I’ve had to park due to the amount of work required. (See the GitHub Issue, Idea: include preview image of each component #43) ↩︎

  4. Apologies if you’ve tried to contact me on LinkedIn – I probably assumed you were either a recruiter or a Large Language Model. ↩︎

https://iainbean.com/posts/2024/five-years-of-component-gallery/
Evaluating packages for accessibility
Show full content

When you’re looking to use someone else’s code in a web project, how do you decide between the alternatives?

You might look at some key numbers:

  • When was it last updated?
  • How many people are using it?
  • How many open GitHub issues does it have?
  • How many other people using it?

To answer these questions you can use the figures for ‘Weekly Downloads’, ‘Last publish’, ‘Issues’, and ‘Dependents’ (the number of other packages on npm that have this package as a dependency) from the npm package page.

You might also want to consider:

  • How big is it?
  • How will it affect my site’s performance?

For these, you can use Bundlephobia to check how a package might increase your bundle size and how long it will take to download.

What about “Is it accessible?”

This one is much harder to distil down to a few numbers. But that hasn’t stopped me from trying.

Measuring accessibility Automated testing

Before building an automated tool, I thought about my manual process for evaluating a package’s accessibility. The first thing I look for is a link to an example of it running in the browser. From here, I’ll do some quick tests: if the package generates markup, I’ll check that it uses semantic HTML; for interactive elements, I’ll check that focus is managed correctly and that everything can be controlled using a keyboard.

If you wanted to automate this step — you could use an automated testing tool like axe, which would give you a figure for the number of accessibility issues. But automated tests only pick up a proportion of all issues and while this could work for some packages, there’s no standard way of linking to examples from an npm or GitHub page.

GitHub issues

After discarding that idea, I decided to look at GitHub issues. These act like a crowd-sourced database of bugs, feedback, and areas for improvement. We can filter for issues that contain certain keywords, such as ‘a11y’ (a common numeronym for ‘accessibility’) or ‘screenreader’ to get those issues we’re interested in. The fundamental assumption: if a maintainer prioritises these kinds of issues and responds to accessibility concerns in a timely manner, it’s likely that they have developed a piece of software with accessibility in mind.

To automate this, I started off by querying the npms.io API. This gives us lots of useful data for each package, including a link to the repository containing the package's source code. If this repository is on GitHub (and the vast majority are) then we can make use of GitHub’s REST API to search issues.

To determine how much a package’s maintainers prioritise accessibility-related issues, I’ve used two metrics:

  1. Score: percentage of issues closed
  2. Average age: median duration between issue open and closed dates

I’ve also found it useful to see a listing of open issues, showing the oldest first. This is handy for highlighting serious red flags — if there’s an issue called something like ‘Add accessibility’ that’s been open for six years, there’s probably something wrong.

Putting it all together

I built a prototype over a year ago which I’ve been using myself. It hasn’t completely changed my workflow but it automates a step that used to take a few minutes.

After deciding that other people might find it useful, I have rebuilt the site in Next.js and published it at IsItAccessible.dev. I’ve also added an autocomplete search, powered by the npms.io API, to make finding packages easier.

Of course, GitHub issues are by no means a perfect way of measuring accessibility:

  • Less popular packages often lack the user base for accessibility issues to be detected.
  • Some packages have the search keywords ‘accessibility’, ‘a11y’, ‘aria’, ‘screenreader’ in their name. This results in a large number of false positives.
  • Lots of packages use monorepos, where multiple packages originate from the same source git repository. This makes working out which GitHub issue corresponds to which npm package difficult.
  • Some maintainers can be trigger-happy closing issues that they don't think are issues. If a maintainer doesn't value accessibility at all, there's nothing to stop them closing accessibility-related issues without any further action.
Roadmap
  • Reduce the number of false positives by filtering issues further based on their content.
  • Give a rough guide to how an accessibility score or average age compares with other packages.
  • Suggest alternative packages which may have fewer accessibility issues.

If you have any ideas on how to improve the site, please get in touch, either by creating a GitHub issue or on Twitter.

https://iainbean.com/posts/2022/evaluating-packages-for-accessibility/
5 steps to faster web fonts
Show full content

In my previous post, I wrote about system fonts and their advantages over web fonts. I encouraged a ‘system fonts first’ approach, arguing that, compared to system fonts, web fonts (a) can negatively impact performance, (b) use more data, and (c) increase your site’s energy consumption. But a web without web fonts would be a far less interesting one — maybe by using web fonts a little more responsibly we can get all their benefits, while minimising the disadvantages.

In part one of this guide, I’m going to cover the five methods for improving web font performance that I think offer the greatest gains for the least effort.

Credit goes to Zach Leatherman, who has written at length about web fonts on his site. All of his articles are worth reading, especially The Font Loading Checklist and A Comprehensive Guide to Font Loading Strategies (which is indeed very comprehensive), both of which came in very useful while I was putting this post together.

In this post, I’m going to be using two terms that are often used interchangeably but which traditionally refer to different things:

  • A typeface is a full family of fonts sharing a common design. A typeface can include any number of weights or styles (and in the days of physical metal or wood type, sizes too). Helvetica is an example of a typeface. You can think of a typeface like a font-family.
  • A font is a single weight and style of a typeface. With physical type, each font would come in its own box containing glyphs of a specific size, weight, and style. e.g. '10-point Helvetica Bold Italic'. The vector-based design of modern digital fonts allows a single font to be scaled up and down infinitely, but you’ll still need a separate file for each weight and style (unless you’re using variable fonts, but that’s a topic for part two).
1. Use the most modern file formats

Web Open Font Format 2.0 (WOFF2) is, at the time of writing, the smallest and most efficient file format for web fonts. When using @font-face at-rules in your CSS, ensure the WOFF2 font appears before older, less efficient, file formats such as TTF. The browser will use the first font in the list it understands, even if it’s a larger file.

@font-face {
font-family: 'Typefesse';
src: url('typefesse.woff2') format('woff2'),
url('typefesse.woff') format('woff');
}

Unless you need to support IE8, you don’t need anything other than WOFF2 and WOFF. If you don’t need to support IE11, you only need WOFF2.

If you only have a TTF file (for example, if you’ve downloaded the font from Google Fonts), you’ll need to convert it using a tool like Online Font Converter. If you’re not using a font with a fully open source license, first check whether the licence permits this.

2. Use the font-display descriptor

There are two acronyms you’ll see a lot when you start delving into font loading strategies:

  1. Flash of Invisible Text (FOIT) is the period of time when text is invisible before the browser has downloaded a web font.
  2. Flash of Unstyled Text (FOUT) is the period of time where text is rendered in a fallback font before the browser has downloaded a web font.

Neither of these are ideal, but if you’re using web fonts, one of them is probably going to happen the first time a user visits your website (hopefully, by the second page load, the browser will be able to serve the fonts from its cache). If we take our font-face at-rule from before and add a font-display descriptor, we can tell the browser which one we’d prefer.

@font-face {
font-family: 'Typefesse';
src: url('typefesse.woff2') format('woff2'),
url('typefesse.woff') format('woff');
font-display: swap;
}

There are five possible values for font-display: the first, auto is the browser’s default behaviour (most browsers favour FOIT). Here are the other four:

swap

A chart showing the behaviour of font-display: swap; (text version below)

swap tells the browser we want it to show text using a fallback font until the web font is loaded (i.e. we’d prefer a FOUT). Whether this takes 5 seconds or 5 minutes, as soon as the font is loaded it will be swapped in. This is a good base because it lets website visitors start reading your content right away, but be sure to choose a similar fallback (we’ll cover fallbacks in part two of this series) to prevent a big layout shift when the fonts are swapped.

block

A chart showing the behaviour of font-display: block; (text version below)

If we’d rather the browser hides text until the web font is loaded (i.e. we’d rather a FOIT), we can use font-display: block. Text won’t remain invisible forever though: if the font doesn’t load within a certain period (usually three seconds), the browser will use the fallback font anyway, swapping in the web font once it has loaded.

If this seems to you like the best option because you think the FOUT looks bad, remember that when text is invisible, your page isn’t useable and your content isn’t readable.

fallback

A chart showing the behaviour of font-display: fallback; (text version below)

fallback is similar to swap with two differences:

  1. It begins with an incredibly small (~100ms) 'block' period where text is hidden, after which it shows the fallback font.
  2. If the web font doesn’t load within a short period (~3s), the fallback font will be used for the rest of the page’s lifetime.

If you’re not fussed whether the user sees your web font the first time they visit your site (chances are they’re not that fussed themselves), fallback is a good choice.

optional

A chart showing the behaviour of font-display: optional; (text version below)

optional is similar to fallback, but it gives the font an extremely short period of time (~100ms) to load, after which it won’t be swapped. It does, however, have an additional feature where it lets the browser decide to abort the font request if the connection is too slow for the font to load.

Each font on your page will have its own FOIT/FOUT period — fonts are swapped individually as they load, not when they’ve all loaded. This can lead to some unfortunate behaviour (see the Mitt Romney Web Font Problem). For full control over font loading, you’ll need to look into JavaScript solutions (which we’ll cover in part 2).

3. Preload your font files

To minimise the FOIT/FOUT period, we want to load our web font files as quickly as possible. Using <link rel="preload"> in our HTML <head>, we can tell the browser to start fetching our fonts earlier. Add the following tag towards the top of your <head> (before any CSS), setting the href attribute to the URL of your font file:

<link rel="preload" href="/typefesse.woff2" as="font" type="font/woff2" crossorigin>

By adding this tag, we’re telling the browser to start loading our font file right away, whereas normally it wouldn’t begin until it’s found a reference to the specific font in your CSS and found a DOM element which uses it.

Browsers are usually smart enough to only download fonts if they’re needed on the current page. Using preload overrides this behaviour, forcing the browser to download a font even if it isn’t used. For this reason, only ever preload a single format of each font (WOFF2 if you have it).

The more fonts you preload, the less benefit you’ll get from this technique, so prioritise those fonts which appear ‘above the fold’ (the first 100vh the user sees without scrolling).

You can read more about preloading in this article by Yoav Weiss: Preload: What Is It Good For?

4. Subset your font files

By subsetting a font, we can generate a new smaller font file which only includes the glyphs (a glyph is an individual character or symbol) we need. I used the Font Subsetter tool on Everything Fonts to subset the font used for headings on this site, Space Grotesk Bold, to only include characters in the ‘Basic Latin’ range. This reduced the filesize of the WOFF2 version from 30kB to just 7kB.

Subsetting is a powerful tool, but it does come with some potential downsides. If you’re building a website that displays user-generated content, people’s names, or place names you should consider characters other than the 26 standard letters, 10 numbers, and handful of symbols common in English writing.

As a minimum, you should think about diacritics: glyphs that appear above or below a character which alter its pronunciation. These are common in languages including French, Spanish, Vietnamese, as well as transliterated (or ‘romanised’) text from alphabets like Greek or Hebrew; they also appear in loanwords (words adopted from another language).

If you subset too aggressively, you could even end up with a mix of fonts in the same word.

A screenshot of the word 'Papier-mâché' in the font Space Grotesk, but the two letters with diacritics are in a different font.
If I wanted to pivot to writing about crafts, I might need to adjust the subset font file I use for headings. Note how the shapes of the ‘â’ and ‘é’ (with diacritics) don’t match the versions of those letters without diacritics.

Fortunately, you don’t have to manually check every page on your site for different glyphs. Glyphhanger is a command line tool, which does two things: firstly, it looks at your webpages and determines the Unicode character ranges used (these ranges correspond to a script or language. e.g. ‘Basic Latin’, ‘Cyrillic’, ‘Thai’); secondly it subsets a font file, outputting a new version containing only the characters in the specified ranges.

It can be a little tricky to get started with Glyphhanger (you’ll need python and pip) — Sara Soueidan’s explains how she got it working here: How I set up Glyphhanger on macOS for optimizing and converting font files for the Web.

As with changing file formats, make sure the licence for your font permits subsetting.

5. Self-host your fonts

This isn’t a universal rule like most of the other points here. There are two good reasons why you might want to use a hosted service like Google Fonts or Adobe Fonts:

  1. They’re often the cheapest or only legal way to use certain typefaces on the web: If you’ve got no choice but to use one of these services, find out if it supports subsetting or adding font-display descriptors.
  2. They’re convenient: Copying and pasting a line of HTML into your site’s <head> is going to be faster than the alternative: downloading font files, converting and subsetting font files, then writing @font-face at-rules for each weight and style.

If you’re still using Google Fonts purely because of the convenience, take a look at google-webfonts-helper. This tool lets you build a custom web font bundle from the complete set of Google fonts, define the weights and character sets you need, then gives you a single download containing all the CSS and font files (in the latest formats) you need.

Web font Myth #1

You may have heard the claim (which is repeated by Google Fonts[1]) that if a user has previously visited a site which loads the same fonts from the same source, the browser doesn’t need to download them again because they’re cached.

This may once have been true, but I can find no evidence that this is a regular enough occurrence to make a real difference. In fact, both Google Chrome and Safari explicitly prevent sharing of cached third-party resources across different domains because of tracking concerns[2].

Here are a list of good reasons not to use a hosted service and self-host your fonts instead:

Performance

Domain lookups take time; you can use preconnect resource hints to mitigate the issue, but there will always be a performance penalty for opening a TCP connection to a new domain. This might be why some of Google’s own sites (including web.dev) now use self-hosted fonts instead of Google Fonts.

Privacy

Paid-for web font services like Adobe Fonts need to detect page views for billing purposes, but they may be collecting more data than is strictly necessary. If you’re given the choice, load your fonts using CSS (<link rel="stylesheet">), instead of JavaScript (<script>), to minimise the amount of data the third-party is able to collect about your users.

Google Fonts doesn’t appear to collect much on website visitors beyond IP addresses and User Agent strings, but Google aren’t acting completely selflessly by providing the service for free. Each one of the fifty trillion[3] page views using Google Fonts is a data point Google wouldn’t have if the websites chose to use self-hosted fonts instead.

Control

With self-hosted fonts you have ultimate control over exactly how you load your fonts, allowing you to serve custom subsets, define font-display settings, and specify how long the browser should cache font files for.

Reliability

Third-party services can suffer slowdowns, outages, or shut down altogether. When self-hosting your fonts, as long as your website is up, your fonts will be available.

Conclusion

Each of these steps can have a benefit on its own, but used together can lead to big improvements. If you decide to implement some of the steps covered in this article, try using a tool like Lighthouse or Web Page Test before and after you make changes, to see the effect of each individual change.

In part two, we’ll cover some more advanced techniques including JavaScript font loading strategies and variable fonts. We’ll also see the importance of choosing the right fallback fonts and introduce a new acronym — FOFT, the Flash Of Faux Text.


  1. Google Fonts FAQ: What does using the Google Fonts API mean for the privacy of my users? ↩︎

  2. Say goodbye to resource-caching across sites and domains, Stefan Judis ↩︎

  3. Figure from Google Fonts Analytics, 2021-05-12 ↩︎

https://iainbean.com/posts/2021/5-steps-to-faster-web-fonts/
System fonts don’t have to be ugly
Show full content

A few weeks ago I discovered this article in the Typewolf newsletter about the new website from design studio, Formafantasma:

Screenshot of the Formfantasma website showing a minimal design: white background, black text set in Times New Roman, and blue links.
The Formfantasma website

Instead of the latest quirky sans-serif, Formafantasma have gone with two of the most ubiquitous typefaces around: Arial and Times New Roman. Whenever I see Arial or Times New Roman in use, I’ll assume that no conscious thought at all has been put into that decision — someone has stuck with the default, the least controversial, the easiest option.

The interesting thing about the Formafantasma site is that they have made a conscious choice, but it isn’t an aesthetic one: using system fonts like Arial and Times New Roman instead of web fonts cuts down on HTTP requests, reducing the amount of data transferred and ultimately reducing CO₂ emissions.

System fonts vs Web fonts

A system font is one that is installed on a computer; these usually come included with Operating Systems, but can also be bundled with certain programs or installed manually. Web fonts are font files stored on a web server, that the browser downloads temporarily for the purpose of rendering a website the way the designer intended. In a single day, you might download hundreds of these files. Some of these might even be the same font but from different websites. The page you’re reading right now loads six web fonts and although some of these have been subset to be smaller than your average web font file, in total, they add over 90kB to the page weight.

Web fonts started gaining popularity with the introduction of services like Typekit in 2009. Before then, if web designers wanted text on their site to look the same across browsers and Operating Systems, they were restricted to a few “web-safe” system fonts.

Fast forward twelve years: web fonts are hugely popular, while system fonts have fallen out of favour, seen by many as boring and unimaginative. But web fonts aren’t free: yes, services like Google Fonts might not cost money, but your users are paying in other ways; improvements in font loading and new font file formats mitigate but don’t solve the fact that web fonts slow down website rendering, use more data, and increase energy usage. That’s why I’d like to suggest a more sustainable approach to choosing fonts I’ll call ‘system fonts first’.

Choosing better system fonts

Not all system fonts are bad; in fact, some are actually pretty good. Here I’ve listed my five favourite system fonts for body text, giving an example font stack with similar-looking fallbacks for Operating Systems that don’t support a specific font-family. I’ve tested compatibility on the five different devices I had to hand using this CodePen; different versions of each Operating System may differ from the platform information I’ve provided here.

Some operating systems may have non-standard behaviour. For example, newer android devices will swap Georgia or Palatino for Noto Serif; some Linux distributions include open-source copies of popular typefaces that behave in the same way. Remember: your website doesn’t need to look exactly the same on every browser and every device.

Georgia

An example of the typeface Georgia showing a heading, normal weight, bold weight, and italic style text.

Platforms: Windows, Mac, iOS

font-family: Georgia, Times, Times New Roman, serif;

Georgia is a serif typeface designed in 1993 specifically to look good on the low-resolution screens of the era. If you look closely, you’ll see that it eschews some of the finer detail seen in serif typefaces designed for print. Unlike other typefaces designed for the same purpose (e.g. Verdana), it still looks fresh on the HiDPI screens of today.

Charter

An example of the typeface Charter showing a heading, normal weight, bold weight, and italic style text.

Platforms: Windows, Mac, iOS, Ubuntu

font-family: Charter, Bitstream Charter, serif;

This is my personal favourite. Designed in 1987 by Matthew Carter, who would later design Georgia for Microsoft, Charter was donated by Bitstream to the XConsortium under a permissive licence that allows copies to be freely downloaded and modified. Charter was designed for low-resolution 300dpi printing, so it also works well on screen.

Palatino

An example of the typeface Palatino showing a heading, normal weight, bold weight, and italic style text.

Platforms: Windows, Mac, iOS

font-family: Palatino, Palatino Linotype, Palatino LT STD, Book Antiqua, Georgia, serif;

Named after the 16th-century calligrapher Giambattista Palatino, Palatino is influenced by type from the Italian Rennaisance. In an attempt to improve readability and legibility when printing on low-quality paper, it was given larger proportions than those designs on which it is based. This gives it the added benefit of also having good legibility on screen.

Hoefler Text

An example of the typeface Hoefler Text showing a heading, normal weight, bold weight, and italic style text.

Platforms: Mac, iOS (but missing some advanced features)

font-family: Hoefler Text, Baskerville Old Face, Garamond, Times New Roman, serif;

Hoefler Text is an old-style serif, designed in 1991 and included in every version of Apple’s OSX (now MacOS) since version 7.5. It is, along with other typefaces including Garamond and Sabon, inspired by the work of 16th-century engravers such as Claude Garamond. What sets Hoefler Text apart from most other system fonts is its rich feature set: it supports old-style figures, small-caps, ligatures and even decorative swashes. If you think you can’t do quality typography with system fonts, try using Hoefler Text.

The ‘system font stack’

Platforms: All

An example of the typeface San Francisco showing a heading, normal weight, bold weight, and italic style text.
San Francisco on MacOS 10.14
An example of the typeface Segoe UI showing a heading, normal weight, bold weight, and italic style text.
Segoe UI on Windows 10
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
/* Only supported on Chromium-based browsers and Safari */
font-family: system-ui;

The goal behind the system font stack is to match the typeface on your website with the operating system UI. Here we make good use of the cascading nature of font-family declarations; the first font name recognised will be used, with others being ignored. For example, MacOS and iPhone will use -apple-system (an alias for San Francisco); Windows users will get Segoe UI, and Android users, Roboto.

Are web fonts really that bad?

I’d be remiss if I didn’t point out that, although they’re often one of the main culprits, in most cases, web fonts aren’t the biggest source of bloat on a website. As an example, the Formafantasma site may not load any web fonts but it does load JavaScript: 221kB worth (gzipped), including four tracking libraries with a combined size of over 160kB.

Screenshot of the Chrome Dev Tools network tab showing four requests totalling 166kB: three to Google Tag Manager and one to Google Analytics.
Imagine how many web fonts you could get for 166kB

Byte-for-byte, JavaScript is more expensive for the browser to process than the equivalently sized web font[1]. When prioritising what to spend your performance budget on, remember that web fonts contribute to the beauty of your site. Tracking JavaScript does not.

When building my own site, I made a choice to prioritise typography over JavaScript. I’ve also become pretty attached to the fonts I’m using. But with new projects, I’ll be using system fonts first and only reaching for web fonts if I really think they add something. This gives me more room in my performance budget for other things including images, CSS, and maybe even some JavaScript.

Further reading
  1. The Cost of JavaScript, Addy Osmani ↩︎

https://iainbean.com/posts/2021/system-fonts-dont-have-to-be-ugly/
Simply write better
Show full content

If I asked you to compile two lists: one containing examples of good documentation and another with examples of bad documentation, I suspect the second list would be longer. I think this is because writing good documentation is hard and documentation can be bad for plenty of reasons: maybe it’s incomplete or out-of-date, or just maybe it uses unnecessary adverbs.

An adverb is a word that describes a verb, adjective or another adverb. Examples include ‘simply’ (‘simply drag and drop’), ‘easily’ (‘you can easily edit the config’), and ‘obviously’ (‘you obviously need this package installed for it to work’). If you’re overusing them, you’re not only upping your word count with filler, but you could also be alienating your readers.

Simply the worst

Jim Fisher’s 2018 talk at Write the Docs Prague in 2018, ‘Don’t say “simply”,’ gives four reasons not to use the word ‘simply’ in your documentation:

  1. It’s hopelessly subjective
  2. It makes people angry and ashamed
  3. It’s lazy and unimaginative
  4. It’s filler

Jim created a script to generate Pull Requests (PRs) against popular open source repos, removing occurrences of the word ‘simply’. While there was a negative response from some maintainers, others welcomed the changes – Taylor Otwell, the creator of the PHP framework Laravel, merged this PR.

But documentation is an ever-changing thing; since that PR was merged, 32 more occurrences of the word have been added to the docs.

Other ‘Inflammatory’ words

‘Simply’ is one of many ways to introduce your opinion into documentation when it’s not needed. While recently setting up a fresh Laravel project, I encountered this line:

“To make this a cinch, Laravel utilizes the DotEnv PHP library.”[1]

Not only is ‘to make something a cinch’ a oddly informal, idiomatic phrase but it’s also completely unnecessary — I don’t care that the author finds environment configuration ‘a cinch’, I want to know how to do it.

In his talk, Jim lists some other words and phrases which can be similarly ‘inflammatory’ (not all of these are adverbs):

  • Naturally
  • Merely
  • Of course
  • Just
  • Boom, you’re done[2]
  • Obviously
  • Trivially

Chris Coyier also covers the topic in the CSS Tricks article, Words To Avoid in Educational Writing. As well as some of the above, he also adds:

  • Basically
  • Clearly
  • Everyone knows
  • Easy

Look at these two lists and strike these words from your technical writing vocabulary. They don’t add to your writing; in fact they’re probably detracting from it. I like this quote from Chris’s article:

“In educational writing, being clear and direct is valuable and appreciated. Skip the filler words and save the casual vibe for a time when you aren’t trying to explain something complicated.”

It's expected for documentation authors to express an opinion about the preferred way of doing something — this can be helpful — but it’s not their job to trivialise a task. The person writing a post or article on a topic is going to be far more familiar with that topic than a newcomer. If your reader finds something challenging, you’re not helping them by describing it as easy.

Cutting down on adverbs

If you’re already in the habit of using some of these words like I am, you might find it difficult to stop. Luckily, there are some useful automated tools which can pick up these mistakes. One of my favourites is write-good, a linter for english prose, designed to detect amongst other things:

  • Adverbs (like ‘simply’) which can weaken meaning
  • “Weasel words”: that make a claim sound vague or ambiguous. e.g. “Some people say…”
  • Cliches
  • Wordy phrases and unnecessary words

I use the write-good VS Code extension to lint my markdown posts, but it’s also available as a Chrome extension for use in Google Docs.

You could also try Hemingway App: a minimal editor which gives you a readability score as you type. It highlights adverbs and difficult to read sentences. It will even suggest simpler alternatives to overly complicated phrases.

You may have used or seen an ad for Grammarly. While it can be useful for detecting certain grammatical errors (and has a handy VS Code extension), it’s really designed for business communications and can struggle with technical language. It can also strip your writing of personality, so I’d steer clear of using it for your personal blog. I only enable it when I want to do one final check before putting a post live.

The output of automated tools should be seen as a guide: you’re bound to get some false positives. That’s why, as well as technical solutions like those above, I also try to follow a few rules:

  • Be aware of ‘the curse of knowledge: If you are incredibly familiar with a topic, it can become difficult to remember that other people aren’t.
  • Acknowledge the possibility of failure: not only do people make mistakes, but machines are set up in different ways. If you can’t make a process less error-prone, give guidance on how to recover from common errors.
  • And finally, write what you’d want to read.

  1. Laravel Docs: Configuration ↩︎

  2. I thought this one was too bad to be true but I’ve found multiple examples with this exact phrasing, and plenty more with variations including “BOOM, you've got some HTML!” and “BOOM! You now have your site hosted on [redacted] with blazing fast speeds, powerful CDNs, simple management, and ironclad security.” ↩︎

https://iainbean.com/posts/2021/simply-write-better/
Flexible components in Eleventy with Nunjucks macros
Show full content

Out of the box, Eleventy gives you a choice of ten different templating languages. Two of these: Nunjucks and Liquid bear more than a passing resemblance to Twig, the templating language used by Craft CMS. When I was migrating this site from Craft to Eleventy, I tried liquid but eventually settled on Nunjucks, simply because it seems to be more popular in the Eleventy community. However, I quickly encountered a problem: include in Nunjucks doesn’t work like include in Twig. Whereas the following code works fine in Twig, Nunjucks doesn’t support passing variables to includes:

{# Works in twig, doesn’t work in nunjucks #}
{% include 'template.html' with {'foo': 'bar'} %}

Fortunately, there is a solution and it comes in the form of macros: one of Nunjucks’ most powerful features. I like to think of macros like React function components. Here are a few ways in which they are similar:

  1. Like in JavaScript, imported macros have a separate scope, meaning they don’t have access to any variables from the parent.[1]
  2. If you want to use variables from outside, you need to explicitly pass them in.
  3. You can set default values for props/arguments to be used if they are not defined.

At first glance, some of these might seem like downsides, but if you’re used to React then you can probably see how they might help to make your code cleaner and reduce bugs.

An example component using macro

Let’s take the following example of the postCard component, used on this site for showing a summary of each blog post, and break it down. Here is the template, src/_includes/macros/post-card.njk:

{% from 'macros/tags.njk' import tagsList %}

{% macro postCard(post, class = 'w-full', cardClass = '', element = 'div', headingElement = 'h2') %}
<{{ element }} class="{{ class }}">
<div class="{{ cardClass }}">
<{{ headingElement }}>
<a href="{{ post.url or post.data.linkUrl }}">
<span>{{ post.data.title }}</span>
</a>
</{{ headingElement }}>
<time datetime="{{ post.date | w3date }}">{{ post.date | date }}</time>
<p>{{ post.data.metaDesc }}</p>
{% if post.data.tags | length %}
{{ tagsList(post.data.tags) }}
{% endif %}
</div>
</{{ element }}>
{% endmacro %}

And here is how we include it in our blog listing template, src/_includes/layouts/blog.njk:

{% from 'macros/post-card.njk' import postCard %}

<ol reversed="reversed">
{% for post in collections.blog %}
{{ postCard(post, element = 'li', class = '') }}
{% endfor %}
</ol>

When calling a macro, you can pass arguments in the order they are defined in the macro (post is first in this case), use their name (as we’ve done here for element and class), or a mix of the two. All arguments not passed will use the default if set.

Import statement
{% from 'macros/tags.njk' import tagsList %}

The first line in our macro file is an import statement. Because we want to use the tagsList component to show a list of tags on each card, we need to import it. This works similarly to a JavaScript import statement: like JavaScript functions, if you want to use a macro from outside the current file, you’ll need to import it or else you’ll get an error. Note that in Eleventy, the file path is relative to the root of your templates directory (which for me is src/_includes). You can also use relative paths: ./ for the template’s directory, or ../ for the template’s parent directory.

Passing arguments
{% macro postCard(post, class = 'w-full', cardClass = '', element = 'div', headingElement = 'h2') %}

Next up, we have the opening macro tag. Here we declare the name of the macro and the arguments. post is the only one of these variables without a default value – that’s because the post card is meaningless without post data. The rest of the arguments are optional, so we define default values.

If you’re familiar with React, this is equivalent to the following code (we’re destructuring the props object here and setting some default values – you could also define default prop values with prop-types):

const PostCard = ({
post,
class = 'w-full',
cardClass = '',
element = 'div',
headingElement = 'h2'
}) => {
// rest of the component here…
Contextual heading levels

The headingElement variable is used so we can be more flexible about where the component appears and make sure it correctly fits in with the heading hierarchy[2]. For example on the homepage we call our postCard macro like this:

{{ postCard(post, class = 'mt-0', cardClass = 'max-w-xs', element = 'li', headingElement = 'h3') }}

Notice how the headingElement is set to 'h3' instead of the default of 'h2'. This is because the parent heading 'Posts' is an <h2>. Compare this with the blog archive page – here the parent heading is 'Blog', an <h1>, so we can let the macro fall back to using the default value: 'h2':

{{ postCard(item, element = 'li', class = '') }}

The rest of the template should be pretty familiar if you’ve used Nunjucks before. We’re using the arguments passed to the macro the same we would any other template variable.

An alternative using include

If, instead of a macro, I was to use an include for my postCard example, it would look something like this:

src/_includes/partials/post-card.njk

{# Set some default values #}
{% set class = class if class else 'w-full' %}
{% set cardClass = cardClass if cardClass else '' %}
{% set element = element if element else 'div' %}
{% set headingElement = headingElement if headingElement else 'h2' %}

<{{ element }} class="{{ class }}">
{# The rest of the component is omitted as it’s the same as the macro example #}
</{{ element }}>

Here we’re using Nunjucks’ rather wonky looking ternary syntax to set some default values. This is equivalent to let class = class ? class : 'w-full' in JavaScript.[3]

src/_includes/layouts/archive.njk

<ol reversed="reversed">
{% for post in collections.blog %}
{% set element = 'li' %}
{% set class = '' %}
{% include "partials/post-card.njk" %}
{% endfor %}
</ol>

As mentioned before, Nunjucks doesn’t let you pass variables along with include. You need to define them beforehand. Here we’re setting the post variable as we loop through items in the collection and setting two more variables, element and class. These will all be accessible inside the post-card.njk partial.

Another example

One benefit of macros is that they avoid problems where inherited variables either need to be reset, or cause unexpected consequences. But if you’re aware of the risks and want even more flexibility, you can give them access to the parent context using with context. Consider the following example using include; here we have two calls-to-action – the first comes from markdown Front Matter and the second from a global data file (located at src/data/globalCta.json):

src/_includes/partials/cta.njk

{# The variable 'globalCta' comes from global data. This is the default value for cta #}
{% set cta = globalCta %}

{# Use ctaContent if defined #}
{% if ctaContent %}
{% set cta = ctaContent %}
{% endif %}

<a href="{{ cta.linkUrl }}">{{ cta.linkText }}</a>

src/_includes/layouts/home.njk

{# Use data from 'pageCta' defined in the Front Matter #}
{% set ctaContent = pageCta %}
{% include "partials/cta.njk" %}

{# Reset ctaContent back to its default #}
{% set ctaContent = globalCta %}
{% include "partials/cta.njk" %}

Notice that we need to reset a variable back to its default value; if we didn’t, the second call-to-action would also use pageCta. With a macro this resetting wouldn’t be necessary — we’d set a default value for cta of globalCta (this only works because we’re importing the macro with context) and we could replace the whole of our cta.njk file with the following:

{% macro cta(cta = globalCta) %}
<a href="{{ cta.linkUrl }}">{{ cta.linkText }}</a>
{% endmacro %}

This also allows us to simplify the code in our home.njk template. Note how we’ve added with context to our import statement, which allows us to use the global data file, globalCta.json inside our macro:

{% from 'macros/cta.njk' import cta with context %}

{{ cta(pageCta) }}
{{ cta() }}
Conclusion

I’m sure that a lot of my reasoning for using macros over includes is personal preference — the mental model of scoped function components is something I’m familiar with from React.

Neither includes or macros are perfect for every situation. I still use includes for simpler components — things like the site header and footer — but the isolated, flexible nature of macros make them ideal for building complex, maintainable UIs. If you’re building a library of reuseable components for your Eleventy site, give macros a try.


  1. I know this isn’t always the case when using var, but it is if you’re following the modern ‘best practice’ of using let and const to declare variables. ↩︎

  2. Heydon Pickering gives a comprehensive overview of the problem of handling heading levels in design systems in the article, Managing Heading Levels In Design Systems. ↩︎

  3. Or let class = class || 'w-full' if you want it even more concise. ↩︎

https://iainbean.com/posts/2020/flexible-components-in-eleventy-with-nunjucks-macros/
An opinionated guide to accessibility testing
Show full content

Identifying (and fixing) accessibility issues is an essential part of any front end developer’s skillset, but it can sometimes be difficult to pick out the useful tools and techniques from the not so useful ones. There are also plenty of misconceptions out there, so I thought I’d make a post covering the tools and techniques that I use when testing web accessibility. To get the most from this exercise, I encourage you to follow along at home.

First things first, choose a website to test: if you’ve got a website of your own or work for a company with a website, you could use that, or if you’re looking for something inaccessible, you’re bound to find plenty of examples on Awwwards or Product Hunt.

0. First impressions

I’ve called this Step 0 because it’s entirely optional: it won’t detect anything you can’t test through other means. We won’t be looking at any source code or developer tools yet; this is all about the feeling you get when first visiting a site. It should take no more than a minute.

This step is based on the assumption that you can get a good idea of how much a site values accessibility by looking at the design choices made. Design is about solving problems and if the solution to that problem excludes a group of people, it’s often the result of an ableist culture at a company. If you find browsing a site unpleasant or frustrating, it’s almost certain that you’re not alone.

Here are some of the things I look for:

  • Readability: How readable is the text? Can you read all the text at your usual distance from the screen or do you have to strain your eyes?
  • Labels: Are there interactive elements like buttons or links represented only with icons?
  • Video: Are there autoplaying videos? is there a way to stop them? are videos containing speech captioned?
  • Animations: Are there an obnoxious number of animated elements? Do they make you feel nauseous?

These are early indicators that a site hasn’t been built with accessibility in mind. In the next steps we’ll get a bit more scientific and start identifying some common issues.

1. The Tab key

The next thing I’ll do is hit the Tab key. Not everyone navigates webpages using a mouse/trackpad/touchscreen — some rely on a keyboard. HTML supports keyboard navigation by default, provided it’s used properly, and this has been the case since the beginning of the web. Somewhere along the way, a sizeable proportion of developers seem to have either forgotten, or never learned this.

It’s important to note that not all browsers behave in the same way. In Google Chrome and Microsoft Edge on MacOS, and all the most common browsers on Windows, the tab key cycles through all types of interactive element: links, buttons, and form inputs. On MacOS there are two exceptions:

Firefox on MacOS

In Firefox, while inputs and buttons receive focus, links do not. In System Preferences → Keyboard, click the Shortcuts pane; at the bottom, you’ll have either an “All controls” radio or a “Use keyboard navigation to move focus between controls” checkbox, depending on your MacOS version — check this (you may need to restart Firefox).

The Keyboard preferences pane in MacOS version 10.12.
On older versions of MacOS you’re given the option to choose between either 'Text boxes and lists only' or 'All controls'. Choose 'All controls'.
Safari on MacOS

Safari is the same as Firefox, in that it does not highlight links by default. You’ll need to go to the Safari menu in the top Menu Bar, select Preferences…, navigate to the Advanced pane, then under Accessibility, check the “Press Tab to highlight each item on a web page” checkbox.

The Advanced preferences pane in Safari.
An example of a skip link, with the text 'Skip to main content' in the header of the GOV.UK homepage.
The first element in the tab order on GOV.UK is a skip link, ‘Skip to main content’, usually a good indicator that at least some thought has been given to accessibility

When using the keyboard to navigate a site, you’re not restricted to just the Tab key. You can also use: Shift + Tab to go back in the tab order; Enter to open a link; Space to scroll the screen down (or interact with a button if focused); and the arrow keys (↑ / ↓ / → / ←) to go between radio and checkbox inputs.

If you find yourself reaching for your mouse or trackpad, drag this bookmarklet onto your bookmarks bar. Pressing it will hide your cursor completely.

Hide cursor

Manuel Matuzovic offers a great breakdown of all the things that you can test with the Tab key. Here are the three I think are most important to check:

  1. As you tab through the page, can you see which item has focus? It’s common for developers (possibly under pressure from designers or managers) to remove default focus styling for ‘aesthetic reasons’
  2. Is everything interactive focusable? Developers often use the wrong element (e.g. a <div> with an onclick event attribute) when there’s a better element available (like a <button>)
  3. In cases where JavaScript is used to modify the DOM (e.g. if a site uses client-side routing, lazy-loads more list items as you scroll, or uses modal windows), is focus handled correctly?

Considering that issues one and two can be prevented by following two simple rules: a) don’t remove :focus styling and b) use the correct semantic HTML element, it’s amazing how often these issues occur. In the article, I Used The Web For A Day With Just A Keyboard, Chris Ashton uses the following example of a span which should be a link:

<span onclick="window.location = 'https://google.com'">Click here</span>

The above element might be styled to look exactly like a link, but a keyboard user would be unable to use it because a span isn’t a focusable element.

“Keyboard-users are standards-reliant users, whereas the able, sighted demographic is privileged enough to be able to interact with the element despite its non-conformance.”

2. Automated testing tools

The main benefit of automated tools is their convenience: they give you a quick, reproducible result. They’re great for finding ‘low-hanging fruit’ — those accessibility issues that are easiest to fix. If you only follow one step in this post, this is probably the one that gives the most results for the least effort.

If I already have an idea that something specific is wrong, the first thing I like to do is to click the tota11y bookmarklet. This brings up an overlay which has different options to highlight individual elements causing accessibility issues:

  • Labels each element with its colour contrast ratio, marking any which do not pass the AA standard in red.
  • Shows images with missing alt text
  • Shows inputs without labels
  • Highlights links with missing or unclear link text

This is the point where I usually run Lighthouse. If you’re using Google Chrome, you can find it in the browser dev tools. If you prefer a different browser, you can run it from a bookmarklet or use a hosted service like Lighthouse Metrics. Lighthouse measures more than just accessibility: it has sections for performance, best practices and SEO. Don’t ignore these other scores — poor performance also impacts accessibility and a slow site can be frustrating for anyone.

Lighthouse catches many common accessibility violations, but you’ll find that different tools detect different issues. The Web Accessibility Initiative (WAI) lists 155 different Web Accessibility Evaluation Tools. Here are a few worth trying:

Automated tools can’t detect every issue: figures range from 25–35% of issues[1] to 71% when combining results from different tools[2]. To find the remaining issues we need to think more about how disabled people actually use the web. In the next step, we’ll cover assistive technologies and start using one.

3. Screen reader testing

Assistive technology is any device or system which helps support and assist people with disabilities to perform specific tasks that might otherwise be difficult or impossible. Today we’ll focus on screen readers, programs which read on-screen content out loud. Examples include VoiceOver on Macs and iOS devices; JAWS and NVDA on Windows; and TalkBack on Android devices. Not all users with visual impairments use screen readers — some may use a screen magnifier like 'Zoom' on MacOS or Magnifier on Windows. There are also Refreshable braille displays which work by electronically raising and lowering different combinations of pins to form braille characters dynamically (as these range in cost from $3,500 to $15,000 I will forgive you if you don’t go out and buy one immediately).

Each screen reader behaves slightly differently, so it makes sense to test using as many different programs as possible, on both desktop and mobile devices. For the sake of convenience, it’s worth getting familiar with a screen reader on your development machine. I’ve included a couple of brief guides to get you started — one for Windows users and another for those on a Mac:

NVDA on Windows

Windows comes with a screen reader, Narrator, but most screen reader users on Windows opt to install a third-party program, the most popular being NVDA and JAWS. JAWS will set you back $90/year but NVDA is free and open source, so that's what I'd suggest you start with.

Once you’ve downloaded and installed NVDA, you can open it at any time using Ctrl + Alt + N.

A screenshot of the 'Welcome to NVDA' window showing some basic instructions and some options.

When you first open NVDA, you’re shown a dialog with Options, including a setting for Keyboard layout: most guides online seem to favour desktop, so choose that even if you don’t have a number pad unless you’re planning to use more advanced features.

Now if you go back to your browser (I recommend either Firefox or Chrome), you should be able to navigate the webpage using the same keys as in Step 1, but now NVDA will announce the content of each focused element (this is called 'focus mode'). If you want to hear non-focusable elements you can use the up and down arrow keys (this is called 'browse mode'). NVDA also announces the content of elements as you hover the mouse cursor over them.

Most NVDA-specific keyboard commands consist of the NVDA modifier key pressed in combination with other keys. By default, the NVDA modifier key is Insert, but you can also configure it to use Caps Lock (which makes entering certain key combinations one-handed, much easier). The NVDA modifier key will be referred to as NVDA for the rest of this article.

NVDA has lots of commands, I’ll only be covering those I’ve found most useful:

  • Ctrl stops speech.
  • Shift pauses speech. Press Shift again to continue.
  • H navigates to the next Heading. Use Shift and H to go to the previous heading.
  • NVDA + Q switches NVDA off
  • NVDA + N will bring up the NVDA menu. For sighted users learning NVDA, there are a few useful tools in here:
    • In PreferencesSettings…Vision, check 'Enable Highlight' to show a box around the currently focused element.
    • ToolsSpeech viewer brings up a window displaying all the text NVDA is currently speaking.
Resources VoiceOver on MacOS

If you use a Mac, you’ll already have VoiceOver installed. To start (or stop) VoiceOver, press Cmd + F5 or if you have a Mac with a Touch Bar, hold Cmd + tap the Touch ID / Power button three times.

When using VoiceOver, the best browser to test in is Safari.

To navigate with VoiceOver, the 'VoiceOver modifier' keys (referred to as VO for the rest of this article) are used in combination with other keys. The default VO keys are either:

  • Ctrl + Option (aka ⌥ or alt)
  • ⇪ Caps Lock

Remember these because every other shortcut uses them.

You can press Ctrl at any time to pause VoiceOver. To resume speaking, press it again.

VoiceOver draws a dark rectangle around the area where it’s focused. This rectangle is called the “VoiceOver cursor.” You can continue to use the same keys as in Step 1 to move between the standard focusable elements (e.g. links, buttons and inputs) and the VoiceOver cursor will follow. To move the cursor through all elements (not just those focusable), use VO + → / ←. VoiceOver will announce the text of each element and if it’s interactive, how to interact with it.

Sometimes when navigating a page, you’ll come across an interactive element with elements inside it. e.g. toolbars, lists, tables etc. Press VO + Shift + ↓ to start interacting with the current element — think of this as moving your cursor down into the element. You can then cycle through its children using VO + → / ←; the VoiceOver cursor will stay within the parent element. Press VO + Shift + ↑ to bring the cursor back up out of the element.

You can bring up the VoiceOver help menu at any time you’re using VoiceOver with the shortcut: VO + H

Resources

Here are some things to check for when testing with a screen reader:

  • Do all images have meaningful alt text?
  • Do links make sense out of the context of the content around them? Text like "Read more" or "Click here" doesn’t tell you anything about what you’re clicking on.
  • Does the visible content match what you hear? Well-meaning developers may add overly-long aria-labels to an element, or hide visible elements from screen readers using aria-hiddenNot all screen reader users are blind and any mismatch between what’s visible and what’s spoken can be confusing.

If you want to really understand how screen reader users navigate the web, I highly recommend watching the video How A Screen Reader User Surfs The Web with Léonie Watson.

4. Next steps

At this point, you should have uncovered the majority of issues on your site, but when it comes to accessibility, you can always do more. I cannot stress this enough: don’t wait for users of your site to report problems — most users who find your site inaccessible won’t get in touch to complain, they’ll leave and won’t come back. Accessibility isn’t something you can mark as done — it’s an ongoing task. A proactive approach to accessibility is always better than a reactive approach:

  • Get familiar with Web Content Accessibility Guidelines (WCAG) — these are a set of recommendations for making web content more accessible and are widely used as the basis for accessibility laws and policies[3].
  • Pay an accessibility professional to test your website.
  • Use a tool like Speedlify to periodically test your site so you can catch regressions early. Speedlify runs both Lighthouse and axe and can be integrated into your Continuous Integration (CI) workflow.
Conclusion

How have you done? If you’ve found zero accessibility issues, Congratulations! sites with no accessibility issues are incredibly rare — in a survey of one million website homepages, WebAIM found that 98.1% of had detectable WCAG 2.0 failures and that study used an automated tool (WAVE, mentioned earlier), so I’d expect the real chance of finding a site with no WCAG failures is close to zero.

If you’ve found major accessibility issues, it’s usually due to either: a lack of knowledge; ‘aesthetics’ trumping accessibility[4]; or developers being overruled by another department (e.g. marketing). If you still need convincing, or need to convince someone else, why accessibility is important, I’d recommend a11y.coffee for a good introduction. If it’s not your site, call them out publicly — ignoring web accessibility is a legal issue and in 2019, 2,256 web accessibility lawsuits were filed in the US[5].

I hope that you’ve found this exercise a useful introduction to web accessibility testing. I encourage you to try different tools and technologies and assemble an accessibility testing toolkit which works best for you. I expect that automated testing tools like Lighthouse will be an essential part of that toolkit, but they’ll only get you so far — eventually, you’ll need to roll up your sleeves and get your hands dirty with some manual testing.


  1. The WebAIM Million: An annual accessibility analysis of the top 1,000,000 home pages ↩︎

  2. What we found when we tested tools on the world’s least-accessible webpage, GOV.UK, Accessibility in government blog ↩︎

  3. The Web Accessibility Initiative (WAI) lists forty Web Accessibility Laws and Policies, 25 of which are based on WCAG ↩︎

  4. The so-called aesthetics vs accessibility debate isn’t one I want to go into here. Let me just say it’s in no way a dichotomy — you can make an aesthetically pleasing site that’s also very accessible and anyone who says you can’t is severely lacking in imagination and/or talent. ↩︎

  5. The Curve Has Flattened for Federal Website Accessibility Lawsuits, Seyfarth Shaw ↩︎

https://iainbean.com/posts/2020/an-opinionated-guide-to-accessibility-testing/
The shady world of Google Analytics proxying
Show full content

I was looking at Google’s new Eleventy starter, eleventy-high-performance-blog, the other day, when one specific feature jumped out at me:

Supports locally serving Google Analytics's JS and proxying it's hit requests to a Netlify proxy (other proxies could be easily added).

As someone who is all too familiar with the flaws in Google Analytics — it’s something I’ve written about before — this struck me as exceedingly clever, but also rather sneaky. To recap, I had three main issues with Google Analytics:

  1. It’s not accurate
  2. It’s bad for performance
  3. It’s bad for privacy

This technique of locally serving Google Analytics's JavaScript and proxying its hit requests has the potential to almost entirely solve the first two issues:

  • By disguising the purpose of the JavaScript library and requests to Google’s tracking API, we can bypass ad blockers and other technology designed to prevent tracking, improving accuracy.
  • Hosting the JavaScript ourselves (rather than on Google’s servers) allows us to load it how, and when, we want, to minimise performance impact.

In this post, I’ll start with a brief explanation of CNAME cloaking, another method commonly used to conceal the nature of tracking JavaScript. I’ll then cover how Google Analytics proxying differs, and see how it improves upon other ways of loading Google Analytics. I’m also going to cover its privacy implications and explain why, despite its obvious benefits for site owners, I’d strongly advise against using it.

CNAME cloaking

While exploring methods to hide tracking JavaScript from ad-blockers, I found this post by Roger Comply, on the topic of Plausible Analytics (a service which I also covered in my previous post), which mentions CNAME cloaking — a technique which exploits CNAME records to make third-party resources seem like first-party resources.

A CNAME record is a type of DNS record which maps an 'alias' name to a true or 'canonical' domain name. Say your website lives on my-site.com and you load some tracking JavaScript from tracking-site.com. Like google-analytics.com, tracking-site.com is known to the developers of ad blockers as a tracking service, so any request to it is blocked. If you add a CNAME record for nothing-suspicious-here.my-site.com (a subdomain of your site, the alias), and point it to tracking-site.com (the canonical domain) you can load the tracking code from your alias domain, making it much harder for ad blockers to identify its true purpose.

The Scooby Doo 'Let’s See Who This Really Is' meme where Fred removes the ghost mask, overlaid with the text 'nothing-suspicious-here.my-site.com', to reveal the text 'tracking-site.com'

But this technique is not guaranteed to bypass ad blockers — since version 1.25.0, uBlock Origin can now 'CNAME-uncloak' network requests to determine the canonical domain and block it if appropriate.

What is Google Analytics Proxying?

The basic idea behind Google Analytics proxying is to divert any traffic that would normally go directly between the browser and Google through an intermediary. This intermediary, known as a reverse proxy, sends data to and from Google, hiding the data's true destination/source from the browser — if you open up the Network tab in your browser's developer tools, instead of google-analytics.com, you'll see the URL of your proxy.

A diagram demonstrating reverse proxying, showing from left to right: an illustration of a desktop monitor, labelled 'web browser'; a server labelled 'proxy' and another server labelled 'Google'. Between, each item there are arrows pointing back and forth, representing the flow of data.

When setting up a Google Analytics proxy, there are two distinct types of request which would usually go directly to Google, which we want our server to handle:

  1. Loading the analytics.js library
  2. Sending pageviews and other tracking events
Self-hosting analytics.js

The first requirement of this technique is hosting analytics.js somewhere other than Google. You could use the method from this article by Stefano Chiodino whereby your proxy loads the most up-to-date analytics.js library directly from Google, rewriting any hardcoded URLs on the fly. However, there is a simpler approach: copy analytics.js from google-analytics.com to your own server, editing any hardcoded URLs to point to your proxy instead of google-analytics.com. You won’t receive any updates to the analytics.js library, but unless you want to use the newest features, it’s unlikely to cause any noticeable problems.

A screenshot from the Network tab in Firefox developer tools, showing the contents of a file named cached.js which is a copy of analytics.js

eleventy-high-performance-blog uses this second approach: this file is named 'cached.js' but when you open it up, you’ll see a modified version of analytics.js with any references to Google URLs replaced.

Proxying tracking requests

The second task, tracking events, is a little more complex. Here’s an article on the technique from 2017 which suggests using a Node.js server to act as your proxy, but for sites built with Static Site Generators (SSGs), this is an increased layer of complexity and potentially an extra cost. eleventy-high-performance-blog uses Netlify functions instead — these are a way of running backend logic without needing to maintain a server, which is why they’re commonly called 'serverless' functions. The code for Netlify functions lives in your site’s git repository and gets automatically deployed along with your site. There's the added benefit that you get a quota of 125k free function calls per month on Netlify’s free tier. The function, which you can view here, is triggered by any request to yoursite.com/.netlify/functions/ga.

This function does more than just forwarding the request directly to google-analytics.com — Google Analytics determines a lot of information from the origin of the request, not just its content, so when forwarding these requests you need to add a few extra parameters to the URL:

  • If you want accurate IP addresses in your data, you’ll need to set the uip URL parameter with the client’s IP address, or else all of your hits will appear to come from the IP of the server running your Netlify function (which is likely in an AWS data center somewhere).
  • If you want accurate browser and device info, you'll also need to pass the User Agent or ua param.

The end result is that, in your Google Analytics dashboard, you should see the exact same data you would have if you weren't using a proxy, but with one important difference — you’re likely to see more visitors and more pageviews; which brings me onto the first key difference of this technique, when compared with non-proxied analytics: accuracy.

How does it differ from regular Google Analytics? Accuracy

One of, if not the biggest causes of inaccurate analytics data is the tracking protection features in browsers and browser add-ons. When using my usual ad blocker, uBlock Origin, analytics.js served from Google and any requests to google-analytics.com are blocked automatically. What does this mean for visitor and pageview numbers? These users are effectively invisible to client-side trackers, resulting in a big gap in analytics data sets; but when Google Analytics is served through a proxy, all of these requests get through.

To find a figure for the size of this gap, you can start by looking at ad blocker installs as a proportion of web users. This 2020 survey by AudienceProject, shows that 36% of respondents in the UK answered that they use an ad blocker (down from 47% in 2020). Unfortunately, there are ad blockers which don’t even block all ads, let alone tracking JavaScript. Things are further complicated by the fact that many ad blockers which don’t block analytics by default, have an option to enable it. e.g. to disable analytics trackers in the most widely-used ad blocker, AdBlock Plus, you have to navigate to the extension options and tick the 'Block additional tracking' box, something I’d expect most users to ignore.

A screenshot of the settings page, for AdblockPlus, showing an unchecked checkbox labelled 'Block additional tracking'

The website of rrreGAIN, a company whose entire business model seems to be based on CNAME cloaking and proxying Google Analytics, says "30% of your web traffic is invisible", with the caveat that this could range "from 5% to as high as 60%", depending on the type of traffic your site attracts. But I’d be more inclined to believe this article from December 2017, which compared client-side hits (which can be blocked) with server-side hits (which can’t be blocked). It gives a percentage of only 8% of users blocking Google Analytics.

Performance

If you’re loading analytics.js from Google, there’s only so much you can do to improve performance, but hosting it somewhere you control can help to mitigate these issues:

  • It reduces DNS requests: if you’re hosting the file on your own site, that’s one less domain that the browser has to look up.
  • It allows you to leverage the browser cache more effectively: Google set the cache-control header so that their analytics.js can only be cached for 2 hours; if you host it on your own server, you can set this much longer so your users will need to re-download it far less frequently.
Privacy

First of all, as with any collection of personal data, you’ve got the law to think about. “I’m not collecting any personal data!” you might say, but under privacy laws like the European Union’s General Data Protection Regulation (GDPR), IP addresses can be personal data, and you need either a good reason for collecting them[1], or explicit consent from your website visitors. Google Analytics gives you the option to anonymise IP addresses, only storing part of the IP address, which may be enough to be GDPR-compliant[2] (I’m not a lawyer in case you hadn’t guessed).

That said, your Google Analytics proxy doesn’t collect any more data than regular Google Analytics; in fact, because you’re using your own domain, there’s no opportunity for Google to set third-party cookies. Although unless you’re running ads, Google Analytics itself only uses first-party cookies.

Above all, the best argument not to disguise your tracking code as a first-party resource is that it’s fundamentally dishonest; and this kind of deception is far more widespread than you might think. The fact that so-called 'privacy-friendly' analytics services like Plausible offer CNAME cloaking shows just how normalised this type of behaviour has become. Isn’t privacy about respecting people’s right to choose how much they share? Perhaps it’s time site owners stopped inventing new tracking methods for the sake of a single-digit improvement to the accuracy of analytics data, and instead thought about why people choose to block trackers in the first place.


  1. What is personal data?, the Information Commissioner’s Office ↩︎

  2. Google Analytics IP Anonymization and GDPR Compliance, Data Driven ↩︎

https://iainbean.com/posts/2020/the-shady-world-of-google-analytics-proxying/
Google Analytics: A luxury your users are paying for
Show full content

Since its launch in 2005, Google Analytics (GA) has become so widely used that it now appears on 86% of the top 100,000 websites in the United States[1]. How did it become so popular? For two main reasons: its convenience and its cost. Once you’ve signed up (for free), you can install it by pasting a script tag and four lines of JavaScript into your site’s HTML. You now have the power to track whenever anyone visits your website, where they came from (both virtually and geographically), what pages they looked at, and for how long they stuck around. But this power doesn’t come for free: you may not be spending any money, but your users are paying in other ways.

Graph showing percentage of global page loads tracked by certain companies: Google 64.4%, Facebook 28.8%, Comscore 12.2%, Twitter 11%, Amazon 10.5%, Yandex 8%, Criteo 6.5%, New Relic 5.9%

Of course, Google isn’t the only company involved in the analytics business, but they collect more data from more users than anyone else. According to this graphic from 2017, Google were tracking 64.4% of all page loads across the whole web[2]; three years later, that figure is now probably even higher.

After recently deciding to remove all the client-side JavaScript from my own website, I had a decision to make: either stop tracking visitor numbers entirely, or look for an alternative to GA. Fortunately, there are a number of other ways to measure the popularity of my site that have far less (or even zero) impact on end users. In this post, I’ll cover the main problems with GA and explore some alternatives.

The downsides

Let’s look at some of the frequently mentioned downsides of using GA and determine their impact:

It’s not accurate

A quick search gives me a figure from 2018 of 27%[3] of web users using an ad blocker. A more recent study suggests that this figure is 40% on desktop and 22% on mobile[4]. Some ad blockers such as uBlock origin block all tracking JS by default (including GA) and even those that don’t block GA by default can often be configured to do so. There are now also privacy controls built into browsers like Safari and Firefox which break certain tracking features by disabling cross-site-tracking cookies (these are what allow ads to follow you around the web after you’ve looked at a product). What does this mean for your analytics numbers? It means you’re going to miss a significant amount of visitors and if your site is one with a tech-savvy audience (e.g. a blog about web development), that number is probably even higher.

This one is a definite yes: the numbers you get from GA are wrong and it’s difficult to say exactly how wrong they are.

It’s bad for performance

If you’re using the method currently recommended by Google to install GA on your site, each of your users will be downloading the gtag.js library which is 34.72kB of (compressed) JavaScript[5] and analytics.js which is 18.4kB[6]. For comparison, that’s larger than many popular JavaScript libraries including Vue (22.8kB) or jQuery (30.4kB)[7], and over twice as large as the previously recommended method of loading analytics.js on its own.

I’ve seen it argued that this isn’t a big deal because of the way browsers cache files between requests. To confirm whether this assumption is correct, I opened a new incognito window in Chrome, opened the network tab and then navigated to five different websites which I knew used GA: while gtag.js appeared as a 34.72kB download on the first site, and analytics.js showed up as being 18.4kB, for the next four sites both files showed up as '(disk cache)', meaning the locally downloaded version was being used instead. However, Google sets a Cache-Control max-age header of only two hours for analytics.js and only fifteen minutes for gtag.js, meaning that after that time the version in the cache is invalid and the file must be downloaded again.

There was a time when websites using Google Analytics were unable to score 100/100 on Google’s own PageSpeed benchmark (the predecessor to Lighthouse) because of this short cache period[8], leading certain performance-minded developers to host the analytics.js library on their own server with a longer cache period. However, this negates the benefit mentioned above, where the file can be cached between different sites.

I’ll mark this one as a yes, but with the caveat that for some visitors it won’t be an issue: you’re still forcing those visitors without a cached version to download multiple libraries which provide them with zero perceptible benefits. It’s also worth mentioning that GA often isn’t the only third-party tracking JavaScript installed on a site. A 2018 study by Pingdom, showed that the average news site loads over 40 trackers and the impact of all those trackers is an increase in the average page load time of 6.77 seconds[9].

It’s bad for privacy

By default, GA harvests a wide variety of data from users, including IP addresses, regardless of whether you have your users’ permission to do so. Efforts by governments, like the EU’s General Data Protection Regulation (GDPR), have attempted to reduce this kind of harvesting of personal information without good reason; but big tech companies are always looking for loopholes which allow them to carry on these practices. If you’ve ever been overwhelmed by the options on a cookie consent dialog, that’s intentional — these are usually designed to either trick you or bore you into sharing more data than you’re comfortable doing, with as many third parties as possible.

Here’s an excerpt from ‘How Google uses information from sites or apps that use our services’:

Google uses the information shared by sites and apps to deliver our services, maintain and improve them, develop new services, measure the effectiveness of advertising, protect against fraud and abuse, and personalize content and ads you see on Google and on our partners’ sites and apps.[10]

When you install GA on your site, you’re basically giving Google free rein to use any data they collect for whatever purpose they choose. One of these purposes Google is somewhat open about is 'Ad personalization' — this is where data collected from multiple sources is combined to build an ‘ad profile’ containing your demographic data and interests. This is then used ‘to make your ads more useful for you’. These personalised, ‘more useful’ ads are shown with the aim of increasing conversions (i.e. clicks and sales), based on the idea you’re more likely to engage with something that’s relevant to your interests. A higher conversion rate means Google can charge higher fees to advertisers for the same ad space.

This is another definite yes: the negative impact on privacy caused by GA is undeniable and is something you should be open with your visitors about. The Google Analytics Terms of Service include the following line: “You must post a Privacy Policy and that Privacy Policy must provide notice of Your use of cookies…” — if you’re using GA without making this clear in your privacy policy (you do have a privacy policy, right?), you’re not only violating GA’s terms and privacy regulations, but also your users’ trust.

The alternatives Minimal Analytics

When Google Analytics runs in the browser it sends tracking events to an API. This means that you don’t have to use Google’s JavaScript: you can instead write your own or use one of many existing GA-compatible libraries to track only the things you need. A great example of this is the Minimal Google Analytics Snippet, a 1.5kB library that is small enough to paste directly into your website’s HTML with no external dependencies. For websites without complex requirements such as AdWords, this is just as effective (if not more effective, as it should load quicker) as gtag.js or analytics.js, but at a fraction of the size.

This mostly solves the potential performance issue, however, even though we’re not using Google’s JavaScript, we’re still sending data to their servers. This does nothing to fix the privacy issue and most ad blockers will be smart enough to intercept these requests.

Client-side alternatives

If you want the features of GA but want to make a more ethical choice, there are plenty of offerings which claim to offer better privacy than GA. I won’t cover all of the available options here, but I will recommend this post by Josef Strzibny in which he covers both open source and hosted alternatives. These generally have much smaller file sizes and collect far less data than GA, but are still liable to be blocked by ad-blockers.

A graph from Plausible analytics, showing the number of visitors during each hour of a single day

I’ve recently replaced GA with Plausible on the Component Gallery: it’s open source, lightweight (835 bytes), it captures the bare minimum of data, and it even comes with a 30 day free trial (after that, it costs $6 a month or $4 if paying annually). I can’t say whether it’s the best privacy-focused client-side analytics service because I haven’t tried them all, but I’d tend to favour those services that have made their code open source over those which haven’t. Open source analytics programs have the benefit that you can choose to host the program yourself without paying a subscription fee. You can also inspect the source code to get a better idea of how much data they’re collecting.

For hosted services I’d be more inclined to choose a service which charges a fee. While no company should be taken at their word without a healthy pinch of scepticism, it’s easier to understand the business model of a service which charges a subscription fee than one that gives it away for free — with a free service, you could be paying with your users’ data. That said, it’s unlikely that any service is doing anything with data on the scale of Google. If you’re strongly against paying a subscription fee for analytics, GoatCounter offers a free tier (donations recommended, non-commercial use) for up to 100,000 pageviews per month.

Other sources of analytics data

Even without client-side analytics, users leave a trail when using the internet: if you make a search using Google, or click a link in a tweet, those platforms are recording that action. No single tool can give you a definitive measurement of how users arrive on your website, but there are still ways you can build up a picture from data that already exists: sites including Google search and Twitter provide a subset of the data they collect for you to use for your own purposes.

A graph from Google Search console showing the number of clicks and impressions for the last 3 months

Google Search Console (shown above) allows you to track the terms used when visitors see your website in search results as well as the proportion of users who saw those links and went on to click them. It also lets you know which pages have been successfully crawled and which caused crawler errors. Unlike with GA, Google already has this data, so I personally don’t see this as anywhere near as harmful as actively collecting more data on Google’s behalf.

Google Search Console isn’t a replacement for analytics, in fact Google push you to link it with your GA account so the datasets from each platform can be combined. But if you don’t want to feed more data into Google’s ad revenue generating machine, using Search Console on its own may be enough to give you a good idea of how users are arriving on your site and what they’re looking for.

Netlify analytics

If you’re hosting your website on Netlify you can enable Netlify Analytics for $9 per site, per month. That might sound steep for a service you can get elsewhere for free, but Netlify’s server-side tracking is an entirely different offering from the client-side services previously mentioned:

  1. It doesn’t impact privacy. If you’ve ever explored the GA sidebar, you’ll have seen that GA collects a baffling array of location, demographic and acquisition data from your site users. GA can collect this because it runs custom JavaScript in the client’s browser. Netlify Analytics data is compiled from server request logs: it can’t use anything other than the data provided to it by regular HTTP requests from the browser.
  2. It doesn’t impact performance. All client-side analytics libraries mean forcing extra JavaScript on users when there’s no benefit to them, whereas code running only on the server will have zero impact on users.

Compared to GA, the set of features is fairly minimal, but for small sites like my own personal blog, I’ve found it refreshingly simple to find the information I want. All the data for your site is shown on a single dashboard screen including graphs of visitor numbers over time, a list of top pages, and something I’ve found especially useful: a list of resources which returned the most 404 errors (something that server-side analytics is able to handle far better than client-side analytics).

A screenshot from Netlify Analytics showing a list of resources and the number of 404 errors caused by each

There are some issues worth pointing out, perhaps the biggest of which is Netlify’s accuracy: compared to GA which seems to generally underestimate visitor numbers, because Netlify uses raw access logs it treats traffic from bots and aggregators (which don’t tend to run JavaScript) the same as genuine users, meaning your visitor numbers are artificially inflated with non-human visitors. While I appreciate how clean and free of configuration Netlify Analytics is, there are some glaring omissions: firstly, I’d like an option to filter out known bots based on user-agent string; I’d also like to see a method for viewing or exporting data older than 30 days — the lack of historical data makes it hard to do any serious data analysis.

If you aren’t using Netlify, there are other server-side analytics but most require some form of subscription fee. If you’re not afraid of some manual set up you could try AWStats, an open-source program written in Perl, which parses your server log files and builds an interface which you can use to explore your data.

What if I don’t have a choice?

Google Analytics is now so common that for many it’s become synonymous with the word, ‘analytics’. If you build websites for clients, chances are they will expect to be able to log in to ‘analytics’ using their Google account and see the familiar reporting interface. You may have other, more pressing battles you’d rather fight with a client: maybe they insist on using animated GIFs everywhere, or have a brand palette with only inaccessible colour combinations.

I’m not saying this data can’t be used for good: there are plenty of "top 10 benefits of Google Analytics" articles out there explaining how you can use GA data to make targeted improvements to your site. For example, you could try to improve the content on your most visited pages, or if the majority of your visitors are on mobile devices, you can focus your design on smaller screen sizes. If you’ve got to have it, at least use it to make your site better, but if you aren’t using it to drive improvements to your site, you’re better off without it.

I've found that for my own blog, a combination of Google Search Console, Twitter analytics and Netlify analytics provide me with enough information that I haven’t needed to look further. I’m lucky enough to be able to afford the $9/month fee but I understand that many people, including clients, will be hesitant to pay for something they can get for free elsewhere. It isn’t up to me to tell you whether the privacy of your website users is worth money out of your pocket, but I’d encourage you to ask yourself that question.

Google don’t give away analytics for free as an act of kindness: they’re still, primarily, an advertising company[11] and the more data they can feed into their ad-targeting algorithms, the more money they can make from selling ads. By giving up your users’ data voluntarily, you’re doing Google a favour, but doing your users a disservice.


  1. Figures from BuiltWith ↩︎

  2. Infographic from Statista ↩︎

  3. Figure from Adblocking penetration rate in selected countries worldwide as of February 2018 ↩︎

  4. Figures from Survey shows US ad-blocking usage is 40 percent on laptops, 15 percent on mobile — Marketing Land ↩︎

  5. Size of the file, https://www.googletagmanager.com/gtag/js, measured 2020-08-21 ↩︎

  6. Size of the file, https://www.google-analytics.com/analytics.js, measured 2020-08-21 ↩︎

  7. Figures from Bundlephobia, GZipped ↩︎

  8. PageSpeed Insights 99/100 because of Google Analytics - How can I cache GA? — Stack Overflow ↩︎

  9. How 3rd Party Trackers Impact the Performance of the World’s Top News Sites, Pingdom ↩︎

  10. How Google uses information from sites or apps that use our services ↩︎

  11. Over 80% of Google’s revenue comes from advertising ↩︎

https://iainbean.com/posts/2020/google-analytics-a-luxury-your-users-are-paying-for/
The most accessible JavaScript framework
Show full content

In my post about why I chose Eleventy over Gatsby when building my new website, I used a statistic from the WebAIM million survey that homepages using React had 5.7% more accessibility errors than the average page. At the time of writing that post, WebAIM hadn’t published the results for Gatsby because its usage share fell under the 0.5% threshold for reporting. I pointed to the value from React to imply that sites using Gatsby could have a similar rate of issues.

Gatsby have since published the statistics from the WebAIM million survey for Gatsby and the results are pretty surprising — home pages built with Gatsby had 34.3 detectable errors on average, 44% fewer than the average[1]. The article includes the following quote from WebAIM:

“Gatsby corresponded with the fewest detectable errors of any common technology or page characteristic analyzed! In fact, Gatsby home pages had about half as many detectable errors as home pages built with React alone.”

This rather unexpected outcome prompted me to explore a couple of subjects further: firstly, to look at the possible reasons for Gatsby doing so much better than other technologies; and secondly whether automated testing is accurate enough to conclusively say if a website is accessible or not.

Enforcing accessibility through developer ‘guide rails’

If I had to choose one reason for Gatsby pages doing so much better than other pages using React, I’d point to the developer 'guide rails' Gatsby puts in place to prevent certain common mistakes.

The second most frequently occurring error in the WebAIM million results is img elements missing alternative text. If an image lacks alt text, assistive technologies such as screen readers have no way (apart from the filename) to communicate its content. In cases where an image is purely decorative the alt attribute is still necessary, but should be empty. In the following example, whereas the second example would be detected by WAVE as an accessibility violation, the third would not be:

<!-- 1. GOOD: alt text describes the image -->
<img
src="/IMG_20200222_101247.jpg"
alt="A tropical beach with palm trees and a sailboat in the distance."
/>

<!-- 2. BAD: No alt attribute -->
<img src="/IMG_20200222_101247.jpg" />

<!-- 3. POTENTIALLY OK, but only if this image is purely decorative -->
<img src="/IMG_20200222_101247.jpg" alt="" />

The popular first-party plugin for Gatsby, gatsby-image aims to prevent missing alt attributes by outputting an empty alt attribute (alt="") when the alt text prop is not defined.

You can emulate this behaviour in React using the PropTypes package. In the following example, I’ve created a basic component that outputs an img element. The alt prop is not required, but will be set to an empty string if not defined:

Image.propTypes = {
src: PropTypes.string.isRequired
};

Image.defaultProps = {
alt: ''
};

const Image = ({ src, alt }) => <img src={src} alt={alt} />;

While this is definitely better than leaving the alt attribute off entirely, if you are a developer who doesn’t understand the need for alt text in the first place, it’s unlikely that you’ll learn anything from this — just because an automated test doesn’t complain, empty alt text on an image containing important information is still an accessibility violation.

Brad Frost suggests a less forgiving approach in his article, Enforcing Accessibility Best Practices with Component PropTypes, making the alt prop required. If you forget it, you’ll see a warning in the browser console.

Image.propTypes = {
src: PropTypes.string.isRequired,
alt: PropTypes.string.isRequired
};

In my opinion, this is a far better solution as it prompts action from the developer, reducing the chance they’ll repeat the same mistake again. The approach of gatsby-image helps to make sites using gatsby-image more accessible, but Brad Frost’s approach could teach developers a valuable lesson that they can apply to any site, even if it doesn’t use React.

Teaching accessibility by example

I’d also like to point out the work the Gatsby team have done to encourage accessibility through their documentation, live streams and events. Unlike some frameworks which regularly include inaccessible examples in their documentation, Gatsby documentation is consistently written with accessibility in mind.

This tutorial on using images in Gatsby contains the following code snippet, showing how to use the gatsby-image component:

<Img
fluid={data.file.childImageSharp.fluid}
alt="A corgi smiling happily"
/>

Note the nice descriptive alt prop on this image. Compare it with an example from the docs for Craft, a popular PHP-based CMS: here the alt text is missing, a serious violation of accessibility guidelines.

<img src="{{ asset.getUrl(thumb) }}" width="{{ asset.getWidth(thumb) }}" height="{{ asset.getHeight(thumb) }}">

Adding a tiny bit more markup to make your examples more accessible isn’t overcomplicating things, it’s encouraging best practices. Developers, especially beginners, copy and paste code from documentation when they need to get something working — it’s a phenomenon which even has its own Wikipedia page. Why not make that code accessible?

The limits of automated accessibility testing

The results of the WebAIM million analysis come with some pretty large caveats. Accessibility errors were detected using WAVE: an automated accessibility testing tool. WebAIM, the developers of WAVE and organisers of the survey, acknowledge that like all automated tools, it can only detect 25% to 35% of possible WCAG[2] conformance failures. This remaining 65% to 75% allows you to build an incredibly inaccessible site that passes accessibility audits, as brilliantly demonstrated by Manuel Matuzovic when he built a site to trick Google’s automated testing suite, Lighthouse.

Performance is accessibility

While front end performance testing has come on leaps and bounds in the last few years, with tools like Lighthouse becoming widely used, automated accessibility tests still have huge gaps in what they can detect. In a recent article, Scott Jehl points to the accessibility tree as one area currently overlooked by automated tests — the accessibility tree or Accessibility Object Model (AOM) is a subset of the Document Object Model (DOM) generated by the browser to communicate with assistive technology such as screen readers. Both the initial DOM and any updates triggered by JavaScript need to be accurately reflected in the accessibility tree and it can’t be used until it’s fully in sync[3].

The lighthouse score for this website, showing separate 100/100 scores for Performance, Accessibility, Best Practices, and SEO

Current DOM-based tests don’t measure whether front end JavaScript slows the time taken to build the accessibility tree. As shown above in the Lighthouse report for this website, performance is measured separately to accessibility, when in reality it can have a real impact on users if downloading and parsing a large JavaScript bundle delays the time until a site is usable. I have concerns that Gatsby’s method of sending static HTML to the browser then "hydrating"[4] it with React components could have a noticeable negative effect on the browsing experience for those using assistive technology. If you want a better idea of the impact of performance on assistive tech I’d encourage you to watch this talk by Léonie Watson in which she explores in-depth the APIs used by screen readers when communicating with the browser.

Manual accessibility testing

While there’s no way to get a quantitative measure of this ‘time-to-a-usable-accessibility-tree’ (a term I just made up, the equivalent of time-to-interactive but for assistive tech) using current automated tests, you can gain a real feeling for whether there’s an issue by manually testing with assistive tech. I’d encourage you to put yourself in the position of the people actually using these programs in their day-to-day lives. While you won’t get a definitive measurement of how accessible your site is, you’re bound to learn something and maybe gain a little empathy in the process.

I’d first start by unplugging your mouse and forcing yourself to use a keyboard to navigate your website instead: do all interactive elements have an obvious focus state? do you need to tab through fifty links before you get to the content?

Next, install a screen reader: if you use a Mac, you already have VoiceOver, if you use Windows, NVDA is free. Get familiar with the basic controls — do all of your images have meaningful alt text? How long does it take before you can start to interact with the page? Is it faster if you switch off JavaScript? If you want to really understand accessibility, instead of treating it as a checklist of chores, try using the same tech as your users.

Conclusion

The fact that an average of 34.3 detectable accessibility errors on a single page is considered good, is a damning indictment on the state of the web in 2020. Poor accessibility is a problem which seems to be deeply ingrained in the web industry as a whole — either developers are not aware of how to make a website accessible; they genuinely don’t care; or they’re not given the time, support or permission by their employer to make their work accessible. Gatsby’s attempts to make things accessible by default appear to be paying off. However I think that hiding this away behind layers of components, queries and client-side JavaScript dilutes the educational value. There’s no truly accessible frontend framework as long as those developers using the framework have the freedom to write inaccessible code.

Perhaps we shouldn’t then be looking to frameworks to fix accessibility, but to companies such as Google and Facebook who have the resources to improve automated accessibility tests. Instead much of this work seems to be left to specialist accessibility consultancies, non-profits and volunteers[5]. The fact that accessibility testing lags so far behind performance testing demonstrates the priorities of the large corporations who control much of the web. Improving performance has been shown to increase conversions, ultimately making more money for shareholders, but for many companies the only motivation to improve accessibility is the threat of lawsuits. This narrow-minded view completely ignores the fact that people with disabilities also spend money online. Gatsby remain rare in the world of VC-funded tech startups in that they do genuinely seem to care about accessibility because it’s the right thing to do, not just for monetary reasons.

Until web accessibility is something that can be fully automated, developers will need to put it on themselves to understand accessibility. I’d like to see screen readers up there with browsers and text editors as one of the core tools of front end developers. If we want to make the web more accessible for everyone, automated test results aren’t going to change people’s attitudes. Ultimately it’s empathy for users that should be the foundation of any accessible website.


  1. If you, like me, were wondering where Eleventy might sit in these results, it’s unlikely to ever show up on this survey even if it does pass the 0.5% threshold to appear in the results. Why? because there’s no way to see if a site is using Eleventy by looking at the HTML — it’s a tool to generate pages and doesn’t leave any trace in the front end code. ↩︎

  2. Web Content Accessibility Guidelines, the global standard for web content accessibility ↩︎

  3. An archived email conversation between accessibility professionals (GitHub gist) ↩︎

  4. A process whereby React attempts to match elements in the existing HTML markup to its own components and state, effectively re-running the logic which rendered the static HTML file at build time, but in the client. ↩︎

  5. Google’s Lighthouse uses a subset of tests from axe, a testing framework built by Deque, an accessibility software and development company. WebAIM is a non-profit company. ↩︎

https://iainbean.com/posts/2020/the-most-accessible-javascript-framework/
Your blog doesn’t need a JavaScript framework
Show full content

At the beginning of this year, I decided it was finally time to port my website from a PHP-based CMS[1] to a JavaScript-based Static Site Generator (SSG). This was for a few reasons:

  1. After starting out as a ‘full-stack’ developer, I now work solely on the front end: If I need to write custom functionality, I don’t want to write it in PHP when I could be writing it in JavaScript.
  2. I don't need the layers of abstraction or the complexity of a CMS — markdown files are where I'm most comfortable writing content and I’d be happy never to touch a MySQL database or wysiwyg editor again.
  3. I want to improve my site’s performance: static HTML files are, 99 times out of 100, going to be faster than pages built on the fly.
  4. Lastly, there’s the cost benefit: A LAMP stack[2] server costs money every month; Netlify’s free tier (300 build minutes/month) should comfortably cover a personal blog at zero cost.

Once I'd decided I want a Static Site Generator and it needed to use JavaScript (which ruled out Jekyll and Hugo), I narrowed the list down to two very different contenders:

“I heard Gatsby was good”

According to the official site, “Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps”. It has a data layer powered by GraphQL and it outputs everything to static files, allowing you to host it pretty much anywhere.

When I first heard that I could write React and use this cool new GraphQL thing, while still outputting static pages that work without JavaScript, I was keen to give it a try. ‘This sounds like Progressive Enhancement but without the effort’ I thought. Unfortunately, as with most things that sound too good to be true, after some investigation, there turned out to be a catch. Here's what happens the first time a user visits a Gatsby site:

  1. The user requests a page
  2. The server sends the statically-generated HTML document to the user’s browser and the browser starts rendering the page.
  3. Now that the HTML document has arrived, the JavaScript bundle (including the React library and any other JavaScript required to render the page) begins downloading, parsing and compiling in the background
  4. The JavaScript is ready to run — the whole DOM gets “hydrated”[3] with React components

There’s something not quite right here — Gatsby forces you to load the same page again, but as React components; until that extra step is complete, none of the elements that require JavaScript (e.g. buttons, menus, custom inputs) are actually interactive.

If your site doesn’t have any interactive elements (excluding links, they work without JavaScript, even in Gatsby), your users still have to download this JavaScript anyway, for the sole purpose of turning your site into a Single-Page Application (SPA), which has its own drawbacks as we'll soon discover.

This extra bloat seems to go against one of the main reasons for moving to a SSG: making pages faster. Your flashy Gatsby site might be fast on a $2,000 MacBook, but for someone using a budget smartphone on a 3G connection, it's visible yet unresponsive, for 15 seconds while the user waits for the JavaScript to load. It’s also unnecessarily draining their battery and data allowance.

If the browser needs to parse 296kB of JavaScript[4] to show a list of blog posts, that’s not Progressive Enhancement, it’s using the wrong tool for the job. To use the rather fuzzy website/web app distinction — React is for building web apps: interactive UIs that need to respond to user input or fetch data in real time — your blog is just a website and that’s fine.

Accessibility in single-page applications

A single-page application is a website that forgoes the traditional method of navigation on the web, i.e. loading new content by loading a new HTML document; instead it uses JavaScript features like AJAX and the History API to swap in new content without triggering a page load. The goal is to increase perceived performance and make the website feel more like a 'native' app (something you’d download from an app store). The problem with getting rid of full-page reloads is that browsers and assistive technology use page loads as a signal to trigger certain useful behaviours, including announcing the title of the new page or resetting keyboard focus to the beginning of the document.

If you’re an accessibility-conscious developer building a single-page application, you may try to emulate the browser’s behaviour using JavaScript. Gatsby attempts to handle this for you by including a RouteAnnouncer component. This uses an ARIA live region to announce the title or h1 of the page, alerting users of screen reader software to navigation changes. This approach, however, is not without its problems: there are still outstanding issues around configuration and localisation.

We’ve seen that the Single-page applications have inherent accessibility issues around navigation, but it’s worth bearing in mind that using a front-end framework can make other aspects of accessibility harder too. In a February 2020 survey of one million homepages, WebAIM found that those using React had 5.7% more accessibility errors than average; while those using Vue had 25% more[5]. This doesn’t necessarily mean that the frameworks caused these errors, but there’s a strong correlation between more JavaScript and worse accessibility.

Does a blog even need JavaScript?

Chances are, the first webpage your ever built performed far better than some of the pages you've built since — it consisted of an HTML file and some CSS; maybe it had a few unoptimised images but they didn’t stop the page from loading. If you're anything like me, the moment you started adding JavaScript was the moment the performance of your webpage took a nosedive. Not all bytes are made equal: an image is going to take far less time to decode and render to the screen than the equivalently sized JavaScript file would take to parse, compile, and execute[6].

JavaScript is a powerful language that can do some incredible things, but it’s incredibly easy to jump to using it too early in development, when you could be using HTML and CSS instead. Consider the rule of least power: Don’t use the more powerful language (JavaScript) until you’ve exhausted the capabilities of less powerful languages (HTML). To me, it feels that turning a blog into a JavaScript Single-page application is introducing unnecessary complexity.

I don't want this post to come across as a specific attack on Gatsby. There are some clever people behind it, who’ve acknowledged many of the issues mentioned in this article and are trying to solve them. A statically rendered and hydrated page is still infinitely better than a fully client-side rendered React app (like those generated by create-react-app) which is useless without JavaScript. I do have issues with the way that its JavaScript-heavy, Single page app approach is advertised, including in Gatsby’s own marketing, as appropriate for any kind of website. Client-side JavaScript has a cost and developers should be aware of it.

Building a Gatsby site with less JavaScript

This brings me to a dilemma: developing sites with Gatsby is an all-round great experience; but Developer Experience (DX) should always be secondary to User Experience (UX) — how do I build a Gatsby site without the issues inherent in Gatsby’s JavaScript-heavy approach? By removing as much of it as possible, of course. Fortunately, there are some fantastic efforts going on within the Gatsby community to build faster, more lightweight sites:

Firstly, you could save a few kilobytes by swapping React for Preact, with gatsby-plugin-preact. I use this on the Component Gallery and it works seamlessly, instantly knocking ~30kB off the JavaScript payload.

If you want to take a more drastic approach, there's a plugin which removes all the Gatsby JavaScript from your Gatsby site. Now we’re getting somewhere! You can continue to write react components and GraphQL, even use a CSS-in-JS library (as long as it outputs CSS or inline styles), without sending any JavaScript to the browser. Most of the problems with Gatsby can be solved by throwing out all of its client side JavaScript. Gatsby Starter Low Tech Blog uses the no-javascript plugin, as well as some other techniques including converting all images to greyscale, to help you create an incredibly lightweight and energy-efficient blog.

Starting from scratch with Eleventy

At this point I felt like something was wrong — using a framework that heavily pushes client-side JavaScript, but removing all of that JavaScript seemed like a pretty convoluted way to build a website. I wanted to see whether I could build a fully-featured blog with zero client-side JavaScript and instead of using a plugin to remove it, I wasn’t going to add it in the first place. This is where my other contender comes in:

Eleventy encourages you to build your site how you want to. You use the technology you're most comfortable with and it sticks to generating your pages. Eleventy gives you the option of mixing and matching ten different templating languages, including markdown, nunjucks, and liquid; this meant I could copy and paste my old twig templates from Craft, change the file extension, and with some minor tweaks have them running in Eleventy. Instead of adapting my front-end build process to a new bundler I could just drop in my existing webpack file and src folder. Using the concurrently package I can run my build script at the same time as Eleventy’s serve process.

Like Gatsby, Eleventy also has an ecosystem of plugins (which is small, but rapidly growing). I’ve picked out a few that I've used which have enabled me to add features without adding client-side JavaScript:

When displaying code snippets in a post, it’s common to include language-specific syntax highlighting. There are a few JavaScript libraries out there that can do it, the most popular of which seems to be Prism — normally you’d run this in the client, but because we’re using a JavaScript SSG, we can run it at build time and bake the HTML elements and CSS classes required for syntax highlighting directly into the document — this removes the need to download the library in the browser.

eleventy-plugin-embed-tweet is another example of this approach of running the JavaScript at build time instead of in the client. Twitter’s default embed code forces the user to download vast quantities of JavaScript just to render a tweet. By fetching and rendering the tweet at build time, the user gets the bare minimum of HTML and CSS required to render the tweet and no extra JavaScript at all.

As with any new technology, Eleventy is lacking some of the features of other more well-established tools. For example, there’s no elegant method for generating responsive images in Eleventy; compare this with Gatsby’s excellent gatsby-image plugin, which can generate lazy-loading, responsive picture elements and smoothly transition from a low resolution or SVG version of the image once the full-resolution file is loaded. There are also a few things I’ve found confusing: I struggled to understand the pagination feature for a while, thinking it was solely for paginating posts into groups of a specified size, before realising it could generate entirely new pages dynamically; I also find myself mixing template languages within the same file: there’s nothing to stop you including nunjucks tags in a markdown file, or swapping out yaml-based frontmatter for JavaScript, but this can break syntax-highlighting, linting and autoformatting.

Conclusion

If you do decide to go with Gatsby I don’t blame you — sometimes it’s nice to use an opinionated framework and if you want to get something up and running quickly, it’s a solid solution. Just be aware of the performance costs and potential impact on accessibility associated with all that JavaScript.

I've chosen to use Eleventy to build my website, but I understand starting with a blank canvas is not for everyone — having the freedom to build something exactly how you want can be daunting. But you don’t have to approach it the way I have — similar to Gatsby, Eleventy has lots of starter projects that you can use as a base. Some of these, such as Andy Bell’s Hylia starter kit can be up and running in a matter of minutes; it even comes pre-configured with Netlify CMS so you can edit your site’s content without touching any code.

So what have I learned? Eleventy makes it easy to build a blog without JavaScript, but there will always be some features that need client-side JavaScript:

  • I’ve had to remove Google Analytics from my site, but analytics was never really good for users in the first place, so I’m not sad to see it go — fortunately, there are server-side alternatives which I will cover in another post.
  • I’ve used the loading="lazy" attribute for lazy loading images, but browser support is patchy and until the native browser implementation improves, there’s no way to gradually fade in images as they load.
  • A dark mode toggle — while I could probably hack this together using only CSS, without access to cookies or local storage, I’d have no way of persisting the value between pages.

Am I going to add JavaScript to my site any time soon? Probably not: the features I’ve listed above are nothing more than nice-to-haves. I’m not seriously suggesting that everyone reading this opens up their website and deletes every single JavaScript file, but from now on, when building websites, I will try to think of JavaScript as an optional extra, not a fundamental part of the experience. I encourage you to do the same.


  1. Craft CMS, it’s one of the better ones ↩︎

  2. Linux, Apache, MySQL, PHP: a common hosting setup for cheap shared servers ↩︎

  3. A process whereby React attempts to match elements in the existing HTML markup to its own components and state, effectively re-running the logic which rendered the static HTML file at build time, but in the client ↩︎

  4. The amount of JavaScript loaded on the homepage of Gatsby Starter Blog (99kB gzipped), measured 2020-04-11 ↩︎

  5. The WebAIM Million: An annual accessibility analysis of the top 1,000,000 home pages ↩︎

  6. The Cost of JavaScript, Addy Osmani ↩︎

https://iainbean.com/posts/2020/your-blog-doesnt-need-a-javascript-framework/