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).
Links
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") }}'

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!