Accessibility no-brainers

I recently had a great discussion in a Kirby partners call. We talked about how important accessibility is, but also how there’s not always a big enough budget for it in projects. Personally, I don’t want to work on ā€œinaccessibleā€ websites anymore because this topic gives me a lot of purpose in my job. But sometimes, for smaller projects, you have to make compromises.

So I’ve collected some habits that require little to no effort but have a huge impact on accessibility.

I should clarify some things upfront: I’m talking about projects where you don’t (have to) care about meeting specific WCAG levels. True accessibility requires designers, content editors, and developers working together, with a big enough budget to make it happen. This article is about making real improvements with little effort.

Use the right elements

One of the easiest accessibility improvements: using the correct HTML elements. The browser does so much work for you when you choose the right element — keyboard navigation, screen reader announcements, focus management — all without writing a single line of extra code.

Buttons

The ⁠<button> element is the simply best. It comes with so many built-in features: they’re focusable, they get recognized as interactive elements by screen readers, they can be ā€œclickedā€ with the space bar or return key, … — all for free. Not using it can make people very sad.

For a long time I felt like the default styles were really hard to override across different browsers. Nowadays the solution is quite simple though. Just use the all property to reset all styles and then restore the focus outline (and optionally box-sizing):

button {
    /* Unset all the styles in one line 🤯 */
    all: unset;
    /* Restore some important stuff */
    box-sizing: border-box;
    &:focus-visible {
        outline: revert;
    }
}

That’s it! Now you can use the button element for pretty much every interactive element on your website (that’s not a link).

I’ve seen so many custom ā€œlinkā€ components built from div or span or button elements with JavaScript click handlers. But when you build your own links out of other elements, you need to manually implement so many things yourself: focus states, browser history integration, opening in new tabs (some people even hold ⌘?!), proper semantics for screen readers, …

That’s a lot of work for something the browser gives you for free with ⁠<a> tags.

Dialog

The ⁠<dialog> element is another gem. It handles (among others):

  • Focus management and trapping
  • ESC key to close
  • Proper modal semantics
  • Backdrops

Before ⁠<dialog>, we had to implement all of this ourselves with JavaScript. Now it’s built right in. Isn’t this amazing?

Similar to the button element, you might not be happy with the default styles. In this case it’s mostly just the padding and border you can easily override. The ones related to centering the dialog can be kept most of the time.

After you’ve adjusted the styling, you’re not only making your mobile menu or other dialogs more accessible, it even saves you time. Learn more about the dialog element on MDN

Other semantic elements

Elements like ⁠<nav>, ⁠<header>, ⁠<main>, ⁠<aside>, and ⁠<footer> automatically create landmarks for screen reader users. These landmarks make it much easier to navigate a page for them. I learned a lot about landmarks from Sara Soueidan’s Practical Accessibility course.

Since you now saved some characters by switching from <div class="navigation"> to <nav>, let’s use them for something useful: when you have multiple instances of the same landmark, add an ⁠aria-label to differenciate them for screen reader users:

<nav aria-label="Main navigation">...</nav>
<nav aria-label="Footer links">...</nav>

Use ARIA attributes as selectors

For the longest time, I thought ARIA attributes always had to be extra markup I needed to add on top of my classes. I hated the redundancy.

But then it clicked — why not use these attributes instead of classes? Instead of writing this:

.menu-item {
    &.active {
        font-weight: bold;
    }
}

Do this:

.menu-item {
    &[aria-current="page"] {
        font-weight: bold;
    }
}

This approach is quite neat because it connects the visual state directly to its semantic meaning. It doesn’t only apply to CSS of course — the same goes for selecting elements in JavaScript.

There’s an ARIA attribute for almost every UI state you can think of, it’s great! The naming is also surprisingly self-explanatory. Here are my top 3:

  • ⁠aria-current indicates the active/current item within a set (navigation, pagination, steps)
  • ⁠aria-pressed shows whether a button is pressed or not (great for filter tags)
  • aria-expanded communicates if an expandable element like a dropdown is open or closed

Learn more about ARIA attributes on MDN

Add a lang attribute to html

This is such a no-brainer, but I’m surprised how often it’s missing (on some of my older websites šŸ™ˆ):

<html lang="en">

Screen readers use this attribute to determine pronunciation rules. Without it, they might read your English content with a pronunciation similar to the German politician Günter Oettinger. It takes two seconds to add and makes a big difference.

focus-visible + hover = ā¤ļø

In 99% of cases, ⁠:focus-visible and ⁠:hover should be treated as inseparable buddies. To stick with my theme of linking to old German YouTube videos, here’s a real classic: Gute Freunde kann niemand trennen (Good friends can never be separated).

Anyways, these two belong in the same selector:

.button:where(:hover, :focus-visible) {
    background-color: var(--color-secondary);
}

This ensures that keyboard users get the same visual feedback as mouse users. It’s such a simple change but makes a huge difference for keyboard navigation.

Learn more in my article about beautiful focus outlines.

Set aside your mouse

As developers, we’re right at home using a keyboard for everything anyways. Why switch to a mouse for testing then?

You’ll immediately notice if focus indicators are invisible or if important elements can’t be reached. One downside of this approach: you’ll get used to it and realize how frustrating it is when other websites don’t work with the keyboard.

Separate heading style from heading level

Accessible page structure requires proper heading hierarchy — don’t skip levels, include exactly one ⁠<h1> per page, and so on. But this semantic structure doesn’t always match the visual design you get. At least that’s the case for me.

The solution? Separate styling from semantic levels. In my CSS I create ⁠.h1, ⁠.h2, etc. classes. Here’s a simplified example:

/* Part of my CSS reset */
h1, h2, h3, h4, h5, h6 {
  font-size: inherit;
}

/* Where the magic happens */
.rich-text :where(h1),
.h1 {
  font-size: var(--font-size-xxl);
}
.rich-text :where(h2),
.h2 {
  font-size: var(--font-size-xl);
}
.rich-text :where(h3),
.h3 {
  font-size: var(--font-size-l);
}
.rich-text :where(h4, h5, h6),
.h4, .h5, .h6 {
  font-size: var(--font-size-m);
}

This approach gives me flexibility to maintain proper heading hierarchy while still matching the design. It’s been particularly helpful when working with designers who aren’t familiar with accessibility requirements. I can easily implement their visual hierarchy without compromising the semantic structure.

The ⁠.rich-text class in the example above is something I use for any user-generated content areas. It ensures that semantic headings get proper styling without me having to manually add classes to each heading.

Wrap animations in media query

Some users can get physically ill from certain animations on websites. That’s something I didn’t fully appreciate until my wife told me she’s one of them. Nothing like seeing someone you love feel nauseated by your cool animations.

Make it a habit to wrap animations and transitions in a media query. With native CSS nesting, this is easier than ever:

.my-fancy-element {
    @media (prefers-reduced-motion: no-preference) {
        transition: transform 0.5s;
    }
}

Technically, ⁠prefers-reduced-motion only needs to affect movement animations, but sometimes when you combine movement with fades or other effects, it makes sense to include those too.

You can also use the inverted way of removing it with ⁠prefers-reduced-motion: reduce:

.my-fancy-element {
    transition: transform 0.5s;
    @media (prefers-reduced-motion: reduce) {
        transition: none;
    }
}

Personally, I try to focus on adding stuff, not taking stuff away.

Remove harmful meta viewport tags

Make sure you’re not using viewport tags that prevent zooming:

<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">

This is okay:

<meta name="viewport" content="width=device-width, initial-scale=1.0">

I remember why I used that in the past: because Safari would zoom in on form inputs, and I thought it looked unprofessional. In hindsight, that was a terrible idea — I was preventing users from zooming altogether šŸ¤¦ā€ā™‚ļø

Make missing alt text visible

Want to make sure your content editors are adding proper alt text to images? In content management systems like Kirby, you can easily add a warning directly in files fields/sections to remind them:

files:
  info: '{{ file.alt.or("āš ļø No alt text") }}'
Screenshot of a Kirby files field. Two of the images have a warning icon and the text No alt text next to it.

This adds a clear warning in the backend whenever an image is missing alt text, making it impossible for editors to miss. This, plus educating the editors about the importance of alt text, is a great way to keep a website accessible in the long run. The files field with the alt info is now part of my starter kit since quite a long time.

Final thoughts

As you might have noticed, several of these tips build on having a good CSS reset in place. Other times, simply using the right element or a different attribute can make a huge difference.

Of course there are so many other things you can do to improve accessibility dramatically: labelling interactive elements, implementing skip links, custom focus outlines, and so many more. It was honestly quite hard for me to restrict myself to the actual ā€œno-brainersā€ that don’t require any or little effort here.

I’d love to hear about easy wins I’ve missed. Let me know on Mastodon!