If you want to create a button from scratch, you must first create the universe

🔥 Discover this insightful post from Hacker News 📖

📂 **Category**:

📌 **What You’ll Learn**:

The title is a tongue-in-cheek parody of a quote from Carl Sagan(External link).
And it ties in very well with the thought exercise we are doing today.

When we start learning about web accessibility the first thing we hear one of two things:

The first rule of ARIA is to not use ARIA.

And:

If there is a native element, use that instead of recreating the element.

But people never explain why this advice is given, and why we should not recreate things from scratch.

I’m going to try to explain why recreating some components from scratch is usually a Sisyphean task(External link) with the example of (mostly) recreating a button from scratch.

Native elements have a set of expectations on their behaviour. They follow some laws of UX. (See what I did there? lol)

Users on your application expect to interact with the element in specific ways and expect that the element will behave in specific ways.

For references, we will use the following links as a base of what a button is, how it behave, and how users expect to interact with it.

Those references give us the following list or requirements.
Along with the items are the WCAG Success Criteria (SC) that represent that requirement.

  1. Have a role of button. (SC 4.1.2(External link))
  2. Have an accessible label. (SC 4.1.2(External link), and SC 1.3.1(External link))
  3. Be focusable. (SC 2.4.3(External link), and SC 2.4.7(External link))
  4. Activate with a mouse click.
  5. Activate by touch.
  6. Activate by stylus and other pointing devices.
  7. Activate on Space and Enter keys when focused. (SC 2.1.1(External link))
  8. Have a type and: submit a form; reset a form; or not do anything with the related form.
  9. A name and value, and form attributes when participating in forms. (SC 4.1.2(External link))
  10. Participate in form validation. (SC 3.3.1(External link))
  11. Support states, like disabled. (SC 4.1.2(External link))
  12. Support newer APIs like Popover API(External link), Invoker Commands API(External link), or Interest Invokers API(External link).

If that didn’t scare you already of trying to re-implement a button from scratch, buckle up, because we will work through that list and I hope that will discourage you from recreating elements.

Our custom element nucleus will be a new tag. We do so because custom tags(External link) behave just like a by default. That gives us an “empty state” to start working on.

We then add a role of button to it. Now it will be exposed to the accessibility tree as a button.


NOTE: styling the element will be left as an exercise to the reader for two reasons:

  1. It will make things unnecessarily longer for this blog post.
  2. Custom elements can be styled however you like, with no general restrictions.



<sagan-button role="button">A button from scratchsagan-button>

So far so good…

Our button should also have an accessible label, that can be either provided by the developer or as a last case fallback derived from the element’s content.

The problem here is specifically icon buttons. If we update our markup to something like this:

<sagan-button role="button">💩sagan-button>

It will be read by a screen reader as something like1:

Button, pile of poo

There are a few issues here:

To remediate this we could add a label to the button, and mark the emoji as presentational and/or hide it, so it is not announced by screen readers.

The code will look as follows:

<sagan-button role="button" aria-label="Data Sharing Options">
	<span role="presentation" aria-hidden="true">💩span>
sagan-button>

The component now looks like a button, is announced like a button, but cannot be interacted with yet.

The first part is to make the element keyboard focusable, to do so, we need to add the tabindex attribute.
This will make the element become part of the tab order of the page(External link), meaning when a user press Tab they will be able to navigate and reach the element.

<sagan-button
	role="button"
	aria-label="Data Sharing Options"
	tabindex="0"
>
	<span role="presentation" aria-hidden="true">💩span>
sagan-button>

The value for the attribute here is 0 so the element will follow the existing order of the page, any positive value would make it jump in front of other elements and the focus of the page move all over, so it is not recommended(External link).

We then need to add some way to activate the component. Click, touch, and pointer interactions are ways to interact with the element using those modalities.

They cover, click for mouse buttons, touch for touch screens, and pointer for everything else like stylus.


NOTE: For maintaining compatibility, browsers do map both touch and pointer events (to some extent) to click events but here we are implementing all of them independently for the sake of argument.

Some keyboard events may also be mapped by default, but it gets more complicated than that…


We then add the events. Starting with the mouseup for handling mouse events:

<sagan-button
	role="button"
	aria-label="Data Sharing Options"
	tabindex="0"
	onomouseup="console.log('mouse')"
>
	<span role="presentation" aria-hidden="true">💩span>
sagan-button>

Let’s also add two more events:

  • ontouchend for handling when the user lifts their finger from the element.
  • onpointerup for handling other pointer devices, like styluses.

Now our component looks like this:

<sagan-button
	role="button"
	aria-label="Data Sharing Options"
	tabindex="0"
	onmouseup="console.log('mouse')"
	ontouchend="console.log('touch')"
	onpointerup="console.log('pointer')"
>
	<span role="presentation" aria-hidden="true">💩span>
sagan-button>

The code for the custom element is getting a little bit verbose on the HTML side of things.
We will give it a little bit more powers by using JavaScript, registering the component(External link), and attaching a Shadow DOM(External link).


NOTE: To make event handling easier, it is done using the handleEvent method of our component.

This article explains the event handling pattern in detail.


The code for the element looks like this:

class SaganButton extends HTMLElement  null

Okay, we are back to where we were before adding JS into the mix, but now with more complexity! 🎉

The button can be activated with clicks, touches, and pointers, but not using the keyboard. We now need to add a keyup and keydown event listener so the component can be interacted with using the keyboard.


NOTE: this is the bare minimum for a semi-working keyboard accessible button.

There are more nuance on the events and expectations. Adrian Roselli covers this topic in more details.



class SaganButton extends HTMLElement  null

Now we are getting into attribute territory, this means implementing behaviors to match the native

{💬|⚡|🔥} **What’s your take?**
Share your thoughts in the comments below!

#️⃣ **#create #button #scratch #create #universe**

🕒 **Posted on**: 1784192532

🌟 **Want more?** Click here for more info! 🌟

By

Leave a Reply

Your email address will not be published. Required fields are marked *