Go back to fundamentals: HTML label
Published on
Rediscover the power of the HTML label element by looking at how we can use it and its impact on accessibility.
Chances are you know <input>
HTML elements should be accompanied by a <label>
. That’s part of semantic HTML. I learned about that when I built my first HTML forms using vanilla PHP in 2015—understand hand-made procedural PHP 😅.
At that time, I had to write quite evolved HTML code to submit forms to my backend.
html
<form method="POST" action="/user/signup"><input type="hidden" name="csrf" value="..." /><label for="username">Username</label><input id="username" type="text" name="username" /><label for="password">Password</label><input id="password" type="password" name="password" /><button type="submit">Submit</button></form>
Nowadays, when you build a website using JavaScript-first frameworks, it feels less necessary to write semantic HTML. Few people use actual <form>
elements to create forms because that’s not required when JavaScript manages inputs’ states.
Frameworks are going back to more native solutions and the new React documentation does an excellent job for promoting semantic HTML.
If I would like people to re-discover an HTML element, it would be the <label>
tag. It’s one of these HTML elements that convey meaning by themselves and were given superpowers by the browsers.
A <label>
must be linked to an <input>
. When the <label>
is focused or clicked on, the focus moves to the input. Screen readers will use the content of the <label>
to describe the input. Simple. Let’s see how we can wire these two elements.
permalinkLink a <label>
and an <input>
with an id
The first and most common way to attach a <label>
to an <input>
is to use a unique id
.
html
<!-- ✅ Perfect! --><label for="unique-identifier">Username</label><input id="unique-identifier" />
The <label>
and the <input>
can appear in any order. It should work the same. However, reading the <label>
before the <input>
is more understandable and should be favored.
html
<!-- ⚠️ Works but isn't recommended --><input id="unique-identifier" /><label for="unique-identifier">Username</label>
Many <label>
elements can refer to a single <input>
. The mdn documentation about the <label>
element gives this example:
html
<!-- ✅ Also valid! --><label for="username">Enter your username:</label><input id="username" name="username" type="text" /><label for="username">Forgot your username?</label>
permalinkIn React world
If you use React, I recommend using the new useId
hook, if you don’t have to set a specific id.
tsx
import { useId } from 'react';function Form() {const inputId = useId();return (<><label htmlFor={inputId}>Username</label><input id={inputId} /></>);}
You may have several inputs in the same component, and calling the useId
hook many times might feel redundant. We can solve it using a single generated id as the prefix for all ids.
tsx
import { useId } from 'react';function Form() {const inputRootId = useId();return (<><label htmlFor={`${inputRootId}-username`}>Username</label><input id={`${inputRootId}-username`} /><label htmlFor={`${inputRootId}-password`}>Password</label><input id={`${inputRootId}-password`} /></>);}
permalinkWrap the <input>
inside the <label>
That may seem strange, but that’s totally valid HTML. Doing so removes the need to wire elements with a unique identifier.
html
<!-- ✅ Great and even shorter --><label>Username<input /></label>
You must adapt your CSS code, but that’s a great solution working without much effort.
permalinkCombine both solutions
You don’t need to use this option often, but I recently found a use case. At Twenty, we had a <Toggle />
component that was visually working but couldn’t be used without a mouse. I suggested a change, based on the code of the <Switch />
component of Chakra UI.
Basically, the component creates an invisible <input>
element and wraps it inside a <label>
. The <label>
container also contains the required elements to give a switch look. If you provide an id
to the input, you can have other <label>
elements targetting the input!
html
<!-- ✅ HTML has no limits --><label for="toggle-notifications">Notifications</label><label><input id="toggle-notifications" type="checkbox" class="sr-only" /><span><!-- Visual elements to give a *switch* look --></span></label>
Warning
Be careful! <label>
elements only accept phrasing content. It means that elements like <div>
or <p>
– which are not phrasing content – are prohibited! We can use <input>
and <span>
as both are phrasing content.
It’s also important to mention that <label>
elements can’t be nested. That’s why they are sibblings in my previous example.
This trick inspired me to write this article in honor of the <label>
element. However, I must be honest and confess that Chakra UI no longer uses this trick in the recent v3 release. Instead, the text label is put inside the most direct input’s label. It makes it possible to rely on a single <label>
element.
html
<!-- ✅ Make the HTML smaller --><label><input type="checkbox" class="sr-only" /><span><!-- Visual elements to give a *switch* look --></span><span>Notifications</span></label>
I didn’t find differences between these two implementations with VoiceOver. I think we can keep our version at Twenty and not worry about switching to the new Chakra implementation!
Semantic HTML is fantastic; don’t be afraid to use it!
If you want to learn more about the <label>
element, go check the documentation on mdn. You can also find interesting information on the HTML specification of the element.