Accessibility Auditing My Portfolio Site - Part 3
Read Part 1 - The Audit and Part 2 - Quick Fixes.
When I made my dark mode toggle using @dailydevtips1's tutorial, I focused on making color themes with sufficient contrast across my site. I chose colors unlikely to cause issues for users with colorblindness. I did not consider making sure both keyboard and screen reader users could use it.
As a result, I got several errors about it during my accessibility audit. I need to make it focusable and add descriptive text. Furthermore, because the visible part of the toggle is a <label>
tied to a <input type="checkbox">
hidden with a display: none;
CSS rule, I need to find a way to put content in the <label>
that adds accessibility and doesn't take away function. Plus, I got an error about the for
attribute in my <label>
not having a valid matching id
attribute in an <input>
.
Let's Focus
My portfolio Github repository has all the toggle component code and the toggle CSS. The structure of the toggle looks like this:
<div className="container--toggle">
{
togClass === "light" ?
<input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked />
:
<input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} />
}
<label htmlFor="toggle" className="toggle--label">
<span className="toggle--label-background"></span>
</label>
</div>
After a bit of reading, I change the CSS hiding the checkbox from display: none;
to opacity: 0;
so that it is focusable.
After a lot of trial and error, I discovered that while you can technically focus a <label>
, it passes its focus to its <input>
. Then, I was under the impression the checkbox was not being focused. There was no focus outline. and I was hitting Enter and nothing was happening. Eventually, I realized I hadn't programmed something to happen on Enter! I added a handleKeypress
function like this:
const handleKeypress = e => {
if (e.key === "Enter") {
if (localStorage.getItem('theme') === 'theme-dark') {
setTheme('theme-light');
setTogClass('light')
} else {
setTheme('theme-dark');
setTogClass('dark')
}
}
}
I originally used e.keyCode === 13
, but nothing was happening. Once I logged the event object in the console, I discovered the keyCode
property was returning as 0 when I hit Enter. No idea why.
Now that my toggle will do something on Enter, I have two options:
- add
onKeyPress={handleKeypress}
andtabIndex="0"
to the<div>
container, which has a inherited default focus outline - add
onKeyPress={handleKeypress}
to the<input type="checkbox">
and try and get a focus outline working around the<label>
I didn't like option #1 because the toggle component is not centered within the <div>
container, and I remember it taking a while to position the container and toggle to look centered within the <nav>
.
It took a fair bit of trail and error, but I managed to get #2 working. I tried setting the CSS property outline
to values like inherit
, but I couldn't get the default blue focus outline showing. Luckily, when designing my <nav>
section, I made sure the button borders that activate on focus and hover had sufficient contrast in both dark and light mode. As a result, I knew I could use that CSS color variable for this outline. I made the toggle outline slightly thicker than the button borders so that it is easier to see.
.toggle--checkbox:focus + .toggle--label {
outline: solid 3px var(--button-border);
}
Labels Within Labels
The first thing I notice is despite the errors, I do have a htmlFor
and id
attributes. I'll have to retest now that the label is not set to display: none;
.
Right off the bat, I added "dark mode toggle" to my <label>
right after the <span>
. It fit nicely within my label, so I messed around for a bit trying to find the best way to make the text invisible. I found out the CSS color
property does not take hsla()
as a valid value - so I can't make it transparent that way. Eventually, I thought "why not just set the color to the same CSS variable as the background?" and voila! ...or so I thought.
The text was hidden, but I noticed one star looked like a rectangle or line instead of a circle. I started moving the text around - putting it before the <span>
and in the <span>
, which started breaking the CSS in a variety of comical ways. Turns out I had accidentally gone with the least breaking option first.
I put the text back after the <span>
and found all I had to do was adjust the value for the width
property in my .toggle--label-background
rule from 4px to 6px.
Finally, I got to work on how a screen reader interacts with the toggle. Ultimately, I want to convey that the component is a dark mode toggle and for the screen reader to notify the user when dark mode is enabled or disabled. I started with a long aria-label
, but the screen reader didn't read the text again after the checkbox state was changed. I started looking into aria-checked
and found role="switch"
. Now the screen reader I'm using clearly says "dark mode toggle" when you focus it, "on" when dark mode is enabled, and "off" when light mode is enabled. Because of the way my CSS worked out, this is actually the opposite of whether the checkbox is checked. Whoops.
<div className="container--toggle">
{
togClass === "light" ?
<input aria-label="dark mode toggle" role="switch" aria-checked="false" onKeyPress={handleKeypress} type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked />
:
<input aria-label="dark mode toggle" role="switch" aria-checked="true" onKeyPress={handleKeypress} type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} />
}
<label htmlFor="toggle" className="toggle--label">
<span className="toggle--label-background"></span>
dark mode toggle
</label>
</div>
You Can't Control Me!
When writing this component, I returned an <input type="checkbox" checked>
or <input type="checkbox">
using a conditional operator based on the theme the user has in their browser's localStorage so that the sun will always show with light mode and the moon will always show with dark mode. I could not get the defaultChecked
attribute to do what I wanted and React will not compile a single controlled component with conditional logic returning the checked
attribute or nothing within the component. Since building this, when the toggle is clicked, I've started getting a warning about how I must "decide between using a controlled or uncontrolled input element for the lifetime of the component."
More research revealed that the defaultChecked
attribute ignores state changes. The game changer was a stackOverflow response showing that you can set the checked
attribute to true or false. Doing this resulted in another error:
"Warning: You provided a checked
prop to a form field without an onChange
handler. This will render a read-only field. If the field should be mutable use defaultChecked
. Otherwise, set either onChange
or readOnly
."
Since onChange
is for recording user input, I added readOnly
and now all the controlled component errors are fixed. Next, I refactored my handleKeypress
and handleOnClick
logic to call changeThemeAndToggle
instead of repeating logic.
Finally, because of the way I wrote the CSS and refactored, I have to add an ariaActive
variable so the screen reader says "on" when dark mode is on and "off" when dark mode is off. Now the component looks like this:
import React, { useEffect, useState } from 'react';
import '../styles/toggle.css';
import { setTheme } from '../utils/themes';
function Toggle() {
// false = dark mode because of the way I wrote the CSS
const [active, setActive] = useState(false)
// the opposite, for screen readers
const [ariaActive, setAriaActive] = useState(true)
let theme = localStorage.getItem('theme')
const changeThemeAndToggle = () => {
if (localStorage.getItem('theme') === 'theme-dark') {
setTheme('theme-light')
setActive(true)
setAriaActive(false)
} else {
setTheme('theme-dark')
setActive(false)
setAriaActive(true)
}
}
const handleOnClick = () => {
changeThemeAndToggle()
}
const handleKeypress = e => {
changeThemeAndToggle()
}
useEffect(() => {
if (localStorage.getItem('theme') === 'theme-dark') {
setActive(false)
setAriaActive(true)
} else if (localStorage.getItem('theme') === 'theme-light') {
setActive(true)
setAriaActive(false)
}
}, [theme])
return (
<div className="container--toggle">
<input aria-label="dark mode toggle" role="switch" aria-checked={ariaActive} onKeyPress={handleKeypress} type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked={active} readOnly />
<label htmlFor="toggle" className="toggle--label">
<span className="toggle--label-background"></span>
dark mode toggle
</label>
</div>
)
}
export default Toggle;
Testing
I've been manually testing with keyboard and screen reader, but it's time I fired back up IBM Equal Access Accessibility Checker.
I really should have retested when I finished the last blog. Immediately, I found out I have two more instances of using "above" and "below" in text that wouldn't make sense without visuals. I already removed one in Accessibility Auditing My Portfolio Site - Part 2 and now I've removed those.
The ARC Toolkit tells me my shiba SVGs need focusable="false"
, so I've added that to both of their code. They wouldn't have been visible long enough to get the errors when I was testing on my live site, so good thing I was testing in local with my lambda functions off. Technically, these and my arrow SVG in my landing page button don't need alt-text because they're decorative, but I'm proud of them. Hopefully screen reader users won't mind hearing about some extra flavor I've added to my portfolio site.
I'm also seeing several errors about the way I've used aria-label
and aria-labelledby
. After even more reading about landmark roles and aria attributes, I've changed all of my content section <div>
s to <sections>
which solves the aria errors and the "multiple <h1>
" warnings in one fell swoop. I now have a couple new things to fix about the blog preview component heading in the next blog in this series.
My required
attributes in my contact form are also causing errors. I ended up adding aria-required="true"
and autoComplete="on"
to the form fields and the ARC Toolkit is now satisfied.
I only get two warnings about the toggle. One is a contrast warning for hiding the text by making it the same color as the background - that makes sense. The sun and moon visuals convey the text meaning, so I'm not concerned. The other says that because I have labelled the component in multiple ways, I need to check how a screen reader interacts with it, which I have done.
Update Based on Feedback
I looked into @inhuofficial's report that the toggle was flashing when you hit Space. Turns out I had accidentally taken out the conditional in handleKeypress()
when I refactored. When there was no conditional, Enter would still trigger the toggle - I speculate because of the HTML. Hitting Space would cause it to flash to the other side and revert back to the original state. I have updated the function to look like this:
const handleKeypress = e => {
if (e.code === "Enter") {
changeThemeAndToggle()
}
}
When I initially changed it, I logged the event object to the console again to verify the code for Space. At that point, I noticed Enter and Space both triggered the toggle perfectly fine. I updated the conditional to if (e.code === "Enter" || "Space")
and Enter worked but Space flashed again! This code is now live on my site and both Enter and Space are working.
Conclusion
Shout out to @overtureweb, who commented on my original dark mode toggle blog with the checked={active}
fix - my apologies for not understanding at the time I responded.
I had a lot of fun with this one. The focus and star fixes were very satisfying, and I'm pleased to have the whole toggle in a much less hacky state.
Read Accessibility Auditing My Portfolio Site - Part 4, where I fix a few things about my blog preview component on the main page.
Read Part 5 - Blog Page Accessibility Deep Dive
In which I find a security vulnerability, write a surprising number of regexes, and this series becomes a thesis.
I fix color contrast issues with the dark mode toggle and speed up its focus outline animation in this one as well.
Stay tuned for Part 6, final testing and thoughts.
Top comments (16)
Great improvements, there are still a couple of things to address though I am afraid!
1. Keyboard controls to activate dark mode
The first one is that because you are using a
<input type="checkbox"
you need to account for Enter and Space. At the moment pressing Space results in some strange reload / flash behaviour.Now I am way out of touch on react but with vanilla JS you can just use a
click
event handler rather than messing with listening for specific keys and it will handle it all for you! That is the great thing about native elements!If you can't just use a
click
handler then add the keycode for space as well and that would be problem solved I would imagine!2. Focus indicator
I like the animated focus indicator but there is a delay before it shows. You may need to speed up the animation a bit or change it to
ease-out
just for theoutline
property.3. The label
There is no need for an
aria-label
as you have a properly associated<label>
.Also "dark mode toggle" isn't quite the correct wording as you should make the text relevant to the current state (if the input is checked I need to know what that means). Something like "dark mode on" makes more sense as when the control is in the
checked
or toggled on state you know that dark mode is activated (dark mode on true effectively), when it is not checked you know it is not activated.Finally you are hiding the text by using the same colour for the text as for the background. A better way is to use a visually hidden class to hide the text by wrapping it in a
<span>
.This is important as if someone uses a custom style sheet (known as a user style sheet) to override your colours the text will show.
Example of hiding text visually but making it screen reader accessible
4. Going that extra mile!
One last thing I would suggest if you can make it work in the design, make the label visible
One thing that often gets forgotten is people with cognitive impairments.
While most people would recognise your toggle switch as a dark and light mode toggle, someone with an association impairment (where abstract graphical representations of real world objects do not get comprehended as an equivalent item - e.g. a triangle with a cross on top for a church vs a picture of a church) may not understand graphics and know what that control is for.
So this point counteracts the above point of hiding the label visually, but instead suggests using the best practice of an always visible label on every control.
This would be my suggestion, place the label just above or to the left of the toggle switch.
Keep going!
You have made some massive improvements already and I am thoroughly enjoying this series.
If you ever need help just @ me!
Great article, have a โค๐ฆ!
Hey InHu,
You can see in 3 codeblocks that I am using React's
onClick
handler, but I'll take a look at space!I left the animation because I liked it. IIRC, the contrast is sufficient down to small text, but I'll consider speeding it up.
Ah - I added the
aria-label
when I was testing before I figured out which element to focus, but I did mention the warning I received about that from an automated tool and testing it in the article. The screenreader I was using did indicate "dark mode toggle, on" or "dark mode toggle, off" with the switch role and label for me, so I'll add both of these to my list to check with multiple screenreaders. I am aware of the visually hidden class, so I'll that to my Github issue for when I figure out the design for my link hover. It doesn't seem like custom/user style sheets are common enough to be a concern.Personally, I would characterize writing almost 5,000 words about accessibility in 6 days as going the extra mile. I am not focused on my site being 100% perfect from this, my first comprehensive audit. I do have at least 2 more blogs planned on the subject. Can you provide more information about an association impairment? I'm not familiar. Also, why not a visual label on hover like is recommended for the external link icon for visual users who may not know what it means? That seems like it would be much better for design, and a second use for the Github issue I linked, so I will add another note to that.
Please consider asking if a person wants feedback like this before giving it. Not everyone would be in a place to respond like this.
I am sorry if this came across in any way as negative, it certainly was not intended to be and was actually meant to help you further your accessibility journey!
I will refrain from saying anything further on your articles and apologise once again that this was taken as anything other than a positive comment.
I look forward to the next instalment(s), once again great article.
Hey InHu,
I'm not sure what implied I was offended. I responded to your points and asked direct questions.
I'm used to getting a lot of unsolicited feedback and love learning more about accessibility. However, that level of feedback would be intimidating for a lot of people, especially beginners to accessibility, who it would seem you want to help.
I will chalk it up to a misunderstanding and phrasing issues between us both then! ๐ That is the problem with the written word!
So to answer your questions:-
point 1
Yeah that might not have been clear, I was referring to vanilla JS
click
handler which interprets Enter and Space as a click on a form control for you.I am guessing React does actually treat
onClick
as just a mouse click or a tap on touch devices.Looks like you found a solve for this one already though so it is all good ๐
Point 2
Nothing wrong with the animation, just how long it takes to show initially.
There is a perceivable delay between tabbing to the element and the outline starting to become visible, so I was just suggesting to see if you can address that if possible.
There are no other issues with the focus indicator as far as I can see!
Point 3
All good here, custom style sheets aren't that common, but they are used in maybe 3-5% (one of those made up statistics...but I did base it on experience ๐คฃ) of accessibility requirements in the sense that people use a lot of plugins and browser extensions to manipulate the page to their preferences.
You are right, it isn't going to affect a large number of people, but the principle of visually hidden text vs same colour text (same colour text is really not a good idea as it can be selected by mistake and can take up space in the layout in other scenarios etc.) is a very good principle to learn for a lot of things and will get you out of a jam when fixing accessibility issues a lot of the time, so I thought it would make great practice.
Point 4
Association impairments are rare, but certain Visual Processing Impairments (the term to search for would be "Visual Processing Disorders") can have this effect (I wish I could remember the exact condition name, but it is eluding me).
I also worked with a young lad with autism and he said that this was something he sometimes struggled with (in fact he was where I got the church example from!), but yet again I would like to point out this is not common.
The "extra mile" part was a poorly phrased way of saying "If you don't mind changing the design slightly, a visible label is always preferable and should be added".
This yet again is a general principle that is really useful to remember as visible labels on forms help in loads of ways.
As for social icons and should you add a label on hover - ideally yes and on focus too.
But bear in mind that this point is that last 1% reaching for perfection, that was why I put "extra mile" as it really is covering every last item and the sort of thing that you would see on a WCAG AAA rated site (of which there are very very few!)
You could ignore this point, just remember visible labels are always better and that will serve you well when making decisions on labelling controls.
Wrapping up
All of the above are just good practices to try and learn early on.
They are things you will come across often (keyboard interactivity problems (common), animations that need adjusting (less common), text just for Assistive Tech (very common) and permanently visible labels (extremely common!).
Although it may seem like a lot of stuff to learn just for a toggle, the principles are some of the core things you will use often fixing accessibility so they are worth practicing and getting used to as it will make the rest of the work a lot easier.
Bonus thing to look at when you have capacity!
Look into
prefers-color-scheme: light
when you have the chance so the site goes into light mode if someone has set that preference in their browser / OS.Everyone always talks about it for
prefers-color-scheme: dark
to switch on dark mode, but as your site starts in dark mode you want to approach it from the opposite angle.This is really minor to be clear, but a great addition once you have done everything else and just something I thought I would throw in as something interesting!
Hope that all helps, any questions just ask! โค
p.s. the fact you have written over 5000 words on accessibility is one of the reasons I reached out in the first place, I think it is fantastic! I also like the approach of telling it as a story of your journey and thought process, rather than as a guide / how to.
p.p.s. There is an article pinned in my profile "101 Digital Accessibility (a11y) tips and tricks" (4th one down), you may find it is a useful reference piece (and it is a reference piece...certainly not something to sit and read in one sitting as you will see if you read it ๐คฃ).
Thought you replied to this but it seems to have disappeared?
Anyway, I will just say the door is open if you do need anything, but I won't write an essay next time as we seem to have gotten off on the wrong foot because of it! ๐คฃ
Followed you on Twitter so I don't miss the next article! โค
Hey InHu,
Yeah, I wrote up why I still disagreed with many of your points but was advised that since it was factual and didn't match your tone, it seemed rude and I don't want to be rude.
For instance, my toggle does take into account user color mode preference using JavaScript instead of CSS - that's the part I refactored in this blog. However, I will take a look to see if the media query would add functionality.
I've incorporated the feedback I do agree with in the blog update yesterday or in the description of the Github issue I'll get to in the future.
Ultimately, I do still want to caution you against giving feedback this in-depth and pushing developers to be 100% perfect/WCAG AAA compliant without asking first. It's possible it might discourage people because perfect is often the enemy of good. Not to mention these issues often have more than one right answer. However, no one can say trying to make the internet more accessible one component at a time is a bad thing. ๐
Right, now we are on the same page there is one thing I will say then, don't worry about your responses to me appearing rude. I was more bothered about offending you so now we both agree that we aren't offended it is all good and I will certainly not be bothered by directness if that was the issue ๐
The disagreement on the toggle colour preference I think is just a misunderstanding then and might need a rephrase on my part!
prefers-color-scheme: light
is purely for first time visitors, if their system setting is prefers light your site doesn't take it into account yet.This is entirely different to storing the preference that they select, which works perfectly.
This really is a minor point, it was purely meant to be a point of interest, it is not a big issue and certainly doesn't need to be addressed right away.
If that is the bit that we are disagreeing on then just ignore this point and do it after everything else you have lined up on the site remediation plan!
That is if you even feel like doing it, it is not essential, just good UX!
As for the offering feedback, we are a million miles apart on that point, most of the people I am friends with on here started because of my essays and this is the first time I have had an even remotely negative response. With that being said your caution has been logged and heard! If I have a similar response in the future then I will take your caution into consideration!
If there were other points you want to discuss fire away, if not then I will wait for your next article and see what you tackle next!
My JS pulls the mode set in their browser's localStorage on load whether or not they've clicked on the toggle and then matches in the toggle and on the page, which I was under the impression would be set by something like that. It'll be interesting to see if I can incorporate the CSS check easily.
Yeah, I just encourage a lot of very new devs to write and a formatted 4 point list is a lot! Plus, even the experts I've heard advise against going for 100% right off the bat because it would be overwhelming, but I understand that this was not solely in the context of me doing a whole overview of my site, just this component. Wasn't meant to be negative, just factual and quick. I have heard the the tone matching message now. ๐
I think React's
onClick
is the equivalent of writing an event handler likeclick
. I think it's<button>
that works on Enter and Space and click automatically.It's not that I don't know the general concepts or want to spend time on things, and I have been practicing since the moment I picked up HTML, but I don't see enough evidence to spend time on user style sheets and am going to lump in making sure the toggle is obvious to cognitively impaired users with my final screenreader tests/that Github issue. Plus, as a developer, I need to spend my limited time on issues that will make the most impact during a big audit fix like this.
I think a label on focus/hover should be sufficient for the extraneous color mode toggle that's not a form and am willing to accept if that makes it just a tad bit less accessible for my pretty design. ๐ญ
Yeah sorry I made a complete hash of that...trying to explain what I was thinking would be more confusing than me just saying "it is fine as it is, ignore me" so I will just say that! ๐คฃ
And as for the points I raised, you are right, they are not important now, just add them to the list!
I think the last thing I should perhaps have clarified earlier on to save us both a lot of too and fro is that the current design is accessible to a stage where it is better than 95%+ of dark mode toggle switches out there! It certainly is good enough, it is production ready, it would pass an audit(see "NECESSARY CAVEAT") and in fact it is more than good enough it is great!
The changes I suggested will not affect WCAG in any way or affect an audit either, they are good practices not compliance points. (WCAG is far from all encompassing).
It certainly is, there is knowing the ideal and working to constraints, it is a fair compromise (I am not militant even if it seemed like it, knowing when you can "cut a corner" and still be accessible is part of the game and what makes accessibility fun!)
NECESSARY CAVEAT - Really prickly auditors might say the lightmode version of the toggle does not have contrast of 3:1 with it's surroundings under SC 1.4.1 Non Text Contrast...if they did say that in a report and given the control design using multiple colours etc. I would call them a **** ****** for being picky, but I can't say it would defo pass without pointing that out!
Ooooh I will check out the color contrast because I am militant about that. ๐ I'm not sure any of the automatic tools caught it, but I'll have to doublecheck.
I don't think automated tools would catch it as you are styling the label rather than the control, the tools are pretty dumb when you start using fancy tricks!
Nice!
Can't believe how much better you even made it!
Really need to step up my a11y game ๐
Thank you! It's such a vast topic, and I was surprised to find out how many screenreader users don't have vision impairment!
Nice ๐ฅ
Dark theming + Accessibility = the future. Actually, the present. Those things are already the standard for any project on the web.
Great article!
Thank you!