Text zoom issues are the worst, I made it better

To comply with the Web Content Accessibility Guidelines (WCAG) 2.2, we need to make sure that our website is usable at 200% text zoom. As a web developer, these A11Y issues are often the most annoying to fix! Just last week, the euphoria of wrapping up a website project was short-lived when my colleague, Caitlin de Rooij at Level Level, played the bearer of bad news - we had a 200% text zoom debacle on our hands. Not again! To tackle this problem once and for all, I came up with a solution that makes dealing with text zoom issues a lot easier.

Text zoom media queries

To make a website responsive we often use media queries. These media queries are used to change the layout of the website depending on the screen size. When dealing with text zoom issues I always wanted to use some sort of media query to change the layout depending on the text zoom level. However, this is not possible. ... "argh". But! I found a way to make this possible using my best friend JavaScript. As JavaScript isn't able to detect the text zoom level, we need to use some little tricks to make this possible.

How to implement the text zoom hack

Our text zoom hack consists of two parts. First we need to create a point of reference. Second we need to create some sort of media query that uses this point of reference to detect the text zoom level.

Creating a point of reference

We can do this by creating a <div> with a fixed width of 1em, as 1em is the current root font-size. We need to make sure that this div is always the first element in our body and is completely hidden for the user. Your HTML And CSS should look something like this:

html
<body>
    <div
        id="text-zoom-reference"
        aria-hidden="true"
        class="text-zoom-reference"
    ></div>
</body>
css
.text-zoom-reference {
    width: 1em;
    position: absolute;
    top: 0;
    left: 0;
    z-index: -1;
    pointer-events: none;
}

Creating a custom media query

With a reference point established, it's time to delve into some JavaScript to identify the text zoom level. The strategy here is to compare the width of our reference element with 16px, which is the default root font size. The JavaScript code for this logic would appear as follows:

js
const referenceElement = document.getElementById('text-zoom-reference');
let fontSize;

// Add more levels to this array if different zoom levels are needed
const zoomLevels = [
    { factor: 1.75, class: 'text-zoom-175' },
    { factor: 1.5, class: 'text-zoom-150' },
    { factor: 1.25, class: 'text-zoom-125' },
    { factor: 1, class: 'text-zoom-100' },
];

const resizeObserver = new ResizeObserver(entries => {
    for (const entry of entries) {
        const currentSize = entry.target.offsetWidth;

        if (currentSize === fontSize) {
            return;
        }

        fontSize = currentSize;
        const zoomFactor = fontSize / 16;

        zoomLevels.forEach(zoomLevel => {
            document.body.classList.remove(zoomLevel.class);

            if (zoomFactor >= zoomLevel.factor) {
                document.body.classList.add(zoomLevel.class);
            }
        });
    }
});

resizeObserver.observe(referenceElement);

I want to take some time to explain this code line by line, so you can get a real grip on what happens here.

  1. First we get the reference element from the DOM. We use this element to compare the width of the reference element with the default font size.
  2. We define a fontSize variable that will be changed according to the current font size.
  3. We create an array of zoom levels. This array contains the zoom factor and the class that should be added to the body when the zoom factor is reached.
  4. We create a ResizeObserver that observes changes of the reference elements dimensions.
  5. Inside the "for loop", We get the current width of the reference element and check if it is different from the previous font size.
  6. If needed, we update the font size.
  7. We calculate the zoom factor by dividing the current font size by the default font size.
  8. We loop over the zoom levels array and remove all text zoom classes from the body.
  9. We check if the zoom factor is larger than the zoom factor of the current zoom level. If so, we add the corresponding text zoom class to the body.
  10. Finally, we have to tell the resizeObserver to observe the referenceElement.

How to use it

Now that we have our text zoom hack in place, we can use it to fix our text zoom issues. We can use the text zoom classes to change the layout of our website depending on the text zoom level. For example, we can use the following CSS to change the layout of our element when the text zoom level is 150%:

css
.element {
    display: flex;
    flex-direction: row;
}
.text-zoom-150 .element {
    flex-direction: column;
}

Demo

I've found this hack to be particularly useful with grid layouts. For instance, in a 4-column layout, increasing text zoom retains the four columns, which can look cramped and hinder readability. With this hack, you can transition to a three- or two-column layout at certain text zoom levels, significantly enhancing readability. Below is a demo illustrating this, allowing you to experience the layout alteration as you tweak the text zoom (not the page zoom!).

Dancing with Colorful Umbrellas

Join us on a whimsical journey through rain and sunshine, where umbrellas dance in vibrant hues. Delight in tales of polka-dotted skies and the art of twirling parasols.

The Secret Life of Garden Gnomes

Dive into the mysterious world of garden gnomes. Discover their midnight parties, the intricacies of hat selection, and why they prefer fishing in imaginary ponds.

The Great Sock Conspiracy

Ever wonder where that missing sock really goes? Unravel the hilarious truths behind sock disappearances and the secret sock society hiding in your laundry room.

Moon Cheese and Other Space Delicacies

Take a gastronomic tour of the galaxy. Sample the famed Moon Cheese, Martian marshmallows, and Saturn’s rings cereal. A culinary adventure beyond the stars awaits.

Supercharging with TailwindCSS

Being a TailwindCSS aficionado, I wanted to leverage this text zoom hack within its ecosystem, leading to the birth of a plugin. Here’s how you can integrate it into your tailwind.config.js. Just paste this code in the plugins array:

js
plugin(function ({ addVariant, e }) {
    const zoomVariants = {
        'text-zoom-md': 'text-zoom-125',
        'text-zoom-lg': 'text-zoom-150',
        'text-zoom-xl': 'text-zoom-175',
    };

    Object.keys(zoomVariants).forEach(key => {
        addVariant(key, ({ modifySelectors, separator }) => {
            modifySelectors(({ className }) => {
                return `.${zoomVariants[key]} .${e(
                    `${key}${separator}${className}`
                )}`;
            });
        });
    });
});

You can modify the zoom levels to your liking by changing the zoomVariants object. The key is the name of the variant, and the value is the class that should be added to the body when the zoom level is reached. Make sure to require 'plugin' at the top of your tailwind.config.js:

js
const plugin = require('tailwindcss/plugin');

Now you can use the text zoom modifiers within your TailwindCSS code:

html
<div class="grid grid-cols-4 text-zoom-lg:grid-cols-2"></div>

Wrapping Up

While the demo predominantly showcased layout alterations, the potential applications are boundless. You could adjust padding, margins, and much more—unleash your creativity! Just bear in mind that hiding content using this hack is off-limits as it could result in a WCAG violation. I hope this article will make your websites more accessible and your life as a web developer a little easier.