Styling Accessibility: A Web Components Approach

Styling Accessibility: A Web Components Approach

a11y and the new Web Standards

The new Web Standards are evolving fast and sometimes it’s hard to actually know the current state of a particular subject in a sea of subjects. I often realize that the vast majority of web projects start without having Accessibility (a11y) in mind and becomes daunting to go back and fix it.

Since, hopefully, most of the Web Components project are still to be born, I decided to gather around the 101’s about those particular subjects and maybe guide who’s sailing in these seas for the first time — which means that on this article, you will find:

  • The basics about Accessibility
  • The basics about Web Components
  • What’s new regarding CSS
  • How can you make your Web Components more accessible

Let’s sail.



Basics #1. What’s Web Accessibility?

“Accessibility is often viewed as making your site work on screen readers. In reality, web accessibility is a subset of User Experience (UX) focused on making your websites usable by the widest range of people possible, including those who have disabilities.”

The above quote (from Dave Ruppert on “Myth: Accessibility is ‘blind people’” for the a11y project) reflects the biggest challenge about Web Accessibility: knowing exactly what it is.

Web Accessibility is essentially a way of giving access to your product to all your potential users.

The 5 categories of accessibility to take into account are:

  • Visual (e.g. non-sighted, myopia, color blindness, etc.)
  • Auditory
  • Motor
  • Cognitive
  • Temporarily disabled users (e. g. one-handed phone users)

If we need to translate those into a product, it usually means being concerned about:

  • Semantics
  • Keyboard inputs
  • Text alternatives
  • Color Contrast

In order to take that into account while developing a product, you should:

  • Make sure you convey meaning through not only color but also form
  • Make sure your product is resizable
  • Make sure your content subjects are distinguishable
  • Make sure you follow the guidelines from the W3C in general

…and don’t forget about the Accessibility Tree, which is the “structure produced by platform Accessibility APIs running parallel to the DOM, which exposes accessibility information to assistive technologies such as screen readers” (source).



Basics #2: A Brief History of Web Components

Web Components, on it’s essence is actually “nothing”: Web Components are a set of new Web Standards that help us achieve a native way of making Components. In broad strokes, I would define Web Components as:

A native way to achieve a  small and re-usable set of logic, behaviors and interface elements, through a series of  browsers standards.

So, what are the building blocks of Web Components?

  • HTML Templates
  • Shadow DOM
  • Custom Elements
  • …and HTML Imports (-ish)

HTML Templates

HTML Templates are a form of re-using pieces of HTML without the original “template” being rendered on the page.

It works as the following:

<html><head><title>HTML Templates</title><style>/* for dramatic effect */
            body { background-color: black; }
            * { color: white; }
        </style></head><body><template id="template-example"><h2>Yes it is! :D</h2></template>

        <div id="dummy-template-container"><h1>This is a template example!</h1></div>

        <script>const templateExample = document.getElementById('template-example');
            document
                .getElementById('dummy-template-container')
                .appendChild(templateExample.content.cloneNode(true));
        </script></body>
</html>

Which will render something like this:

You can check how currently HTML Templates are supported by the browsers on this caniuse page.

Shadow DOM

Shadow DOM is a way to achieve CSS scoping, DOM encapsulation and composition, making it easier to build isolated components.

There’s two modes of achieving Shadow DOM: “closed” and “open”, the difference being, that when you instance element.shadowRoot, the “open” mode returns the HTML node and the “closed” mode returns null. Both modes return null when you try to query the DOM. Bare in mind that you have to set a mode to use Shadow DOM since there is no default value for it.

It works as the following:

<html><head><title>Shadow DOM</title><style>/* for dramatic effect */
            body { background-color: black; }
            * { color: white; }
            * { text-decoration: underline; }
            :host { all: initial; }
        </style></head><body><div id="dummy-container"><h1>This is a Shadow DOM example!</h1><div id="shadow-dom-closed-example"></div><div id="shadow-dom-open-example"></div></div>

        <script>const shadowDomClosedExample =
                document
                    .getElementById('shadow-dom-closed-example');
            
            const shadowDomOpenExample =
                document
                    .getElementById('shadow-dom-open-example');
            const shadowRootClosed =
                shadowDomClosedExample.attachShadow({
                    mode: 'closed'
                });
            const shadowRootOpen =
                shadowDomOpenExample.attachShadow({
                    mode: 'open'
                });
            shadowRootClosed.innerHTML = `
                <style>
                    h1, h2 { 
                        color: red;
                    }
                </style>
                <h2>Closed</h2>
            `;
            shadowRootOpen.innerHTML = `
                <style>
                    h1, h2 { 
                        color: yellow;
                    }
                </style>
                <h2>Open</h2>
            `;
            // These aren't the droids you're looking forconsole.log(document.querySelector('h2'));
            console.log(shadowDomOpenExample.shadowRoot);
            // returns the HTML nodeconsole.log(shadowDomClosedExample.shadowRoot);
            // returns null</script></body>
</html>

Which will render something like this:

And the DOM tree will look something like this:

You can check how currently Shadow DOM is supported by the browsers on this caniuse page.

Custom Elements

Custom Elements are the way to achieve the full re-usable encapsulated pieces of logic and have the best from Shadow DOM and HTML Templates, including slots.

All this can be achieved by the following:

class OurVeryOwnCustomElement extends HTMLElement {
    constructor() {
      super();

      const templateExample = document
        .getElementById('template-here').content;

      const shadowRoot = this.attachShadow({mode: 'open'})
        .appendChild(templateExample.cloneNode(true));
    }

    connectedCallback() {
        // Called every time the element is inserted // into the DOM
    }

    disconnectedCallback() {
        // Called every time the element is removed from // the DOM
    }
    
    attributeChangedCallback(attrName, oldVal, newVal) {
        // Called when an observed attribute has been added, // removed, updated or replaced.
    }

    adoptedCallback() {
        // The custom element has been moved into a new // document (calling document.adoptNode).
    }
}

customElements.define('custom-exemple', OurVeryOwnCustomElement);


<html>
    <head><title>Custom Elements</title><style>/* for dramatic effect */
            body { background-color: black; }
            * { color: white; }
        </style></head>
    <body>
        <div id="dummy-container"><h1>This is a Custom Elements example!</h1>

            <custom-exemple></custom-exemple>

            <custom-exemple><h4 slot="smiley">:(</h4><h5 slot="some-other-slot">Ta daaa</h5></custom-exemple>

            <template id="template-here"><h2>Also showing off the "Slots".</h2><h3>Basically, it's an all in.</h3><slot name="smiley">:D</slot><slot name="some-other-slot">...</slot></template></div>

        <script src="./custom-elements.js"></script></body>
</html>

Which will render something like this:

You can see the Template “Slots” being replaced accordingly.

You can check how currently Custom Elements are supported by the browsers on this caniuse page.

…and the -ish: HTML Imports vs. ES Modules

HTML Imports were a big part of the Web Components standards but has stopped being supported and no longer is listed at the Web Components page (being replaced by ES Modules). No more than a footnote at this point on the Web Components history. As Wilson Page from the Firefox OS team puts it:

“We’ve been working with Web Components in Firefox OS for over a year and have found using existing module syntax (AMD or Common JS) to resolve a dependency tree, registering elements, loaded using a normal <script> tag seems to be enough to get stuff done.”

If you want to know more about the state of the HTML Imports vs. ES Modules, you can check this page.

Web Components are way more than this, make sure to continue to search for more information about them, specially regarding custom eventsobservedAttributestesting and performance.



Bonus round: the new CSS “theories”

If you check back the code snippets on this article, you’ve already glimpsed at a few new offers from CSS:

  • Scoped CSS (through Shadow DOM) solves one of the biggest problems with CSS, the “over-ruling”
  • With :host we can select to style a shadow host
  • There is also :host() and :host-context() — the first one targeting the host that is passed inside the parenthesis (e.g. :host(.some-custom-element)) and the second one targeting the content of a shadow host (e.g. :host-context(h2) targets the h2’s inside a shadow host)

“theories” you shouldn’t use

Since Web Components are standards that are continuously evolving, there are a few things that came and passed (like the already mentioned HTML Imports). That’s also the case for CSS and is especially true for the Shadow Piercing Combinators which were forms of styling shadowed elements. If you come across these, please avoided them :) They are:

  • ::shadow
  • /deep/
  • >>>

But wait, CSS Variables!

…and yes. There are proper forms of styling shadowed elements: CSS Variables — you can re-use generic styling inside (and actually, outside) Web Components. Let’s check how:

<html><head><title>CSS Variables</title><style>/* for dramatic effect */
            body { background-color: black; }
            * { color: white; }
            :root {
                --main-text-color: yellow;
            }
        </style></head><body><div id="dummy-container"><h1>This is a CSS Variables example!</h1><div id="css-variables-example"></div></div>

        <script>
            const CSSVariablesExample =
                document
                    .getElementById('css-variables-example');
            
            const shadowRoot =
                CSSVariablesExample.attachShadow({
                    mode: 'open'
                });
            shadowRoot.innerHTML = `
                <style>
                    h2 { 
                        color: var(--main-text-color, blue);
                    }
                </style><h2>Yes, it is.</h2>
            `;
        </script></body>
</html>


The h2 inside the shadowRoot will render as the content of the — main-text-color if it exists, if it does not exist, it will be render as blue. The result is something like this:

In this case, the content of the — main-text-color variable is yellow.

You can check how currently CSS Variables are supported by the browsers on this caniuse page.

::part() and ::theme()

::part() and ::theme() are very recent proposals to CSS that came to the aid as alternatives to style shadowed elements. Instead of trying to explain them, I’m just going to redirect to this article by Monica Dinculescu which is excellent. This are very recent proposals to CSS so it’s quite possible that by the time you read this article they are still not supported by your browser.



So, how can we make our components accessible?

Firstly…

Basics, basics, #3. The basics about accessibility:

There are a few things that we can do to our product from the ground-up that will immensely help making it accessible to our users.

One thing to remember is the blueberry theory (an idea “stolen” again from a talk of Monica Dunculescu):

What makes a blueberry muffin is not adding blueberries to an existing muffin, is actually cooking a blueberry muffin from the beginning. Making a product accessible, is not adding a few roles and ARIA labels after it’s built, is actually having accessibility in mind from the very start.

So…

Role

Role is a way of telling a new element to behave as a different one. Quick example:

<html><head><title>Role model</title></head><body><button>Tell me the problem</button>

        <!-- Screen reader would read it as: "Tell me the problem, button" -->

        <div role="button">Tell me the problem</div>

        <!-- Screen reader would read it as: "Tell me the problem, button" --></body>
</html>

TabIndex

TabIndex is a way of making an element focusable (essential for a screen reader). If you set it as 0, it’s focusable on the right order, if it’s -1 it’s focusable out of the normal order (as in, you can trigger programmatically the focus of the element) and if you set any other positive number, you change the actual order of the focus (highly avoidable). Quick example:

<html><head><title>tabIndex</title></head><body><!-- <button>Tell me the problem</button> -->

        <div role="button"tabindex="0">
            Tell me the problem
        </div>

        <!-- 0, -1 or a positive number (avoidable...) --></body>
</html>

Focus indicator

The focus indicator is something (usually) native to the browser and serves as a visual aid to which element is focused at the current moment. If you ever thought that the design is not perfect, please don’t remove it (e.g. on Chrome you might see it as a orange or blue glow around an input, for example) with { outline: none; } on the CSS for example. It’s extremely useful for everyone who uses screen readers — if you want to redesign it, please make sure you follow the accessibility guidelines.

ARIA

Aria is a way to improve how you label your components. There’s tons of them, so I won’t bother you with the huge list :) — you can find them here — just a quick example:

<html><head><title>ARIA</title></head><body><label id="labeling-inputs">Amount of problem</label><inputtype="range"name="problem"value="5"min="0"max="10"aria-labelledby="labeling-inputs"aria-valuemin="0"aria-valuemax="10"
        />

        <!-- The screen-reader would read: "ARIA: amount of problem. 5, slider. min: 0, max: 10." --><!-- The normal read: "5, slider." --></body>
</html>

Without the ARIA labels, a screen reader would perceive the input as “5, slider” but with them, it would read it as “ARIA: amount of problem. 5, slider. min: 0, max: 10”.

Here’s an excellent (and quick) tutorial on how to label a custom element:

Keyboard input

Like it was mentioned at the beginning of the article, it’s quite important to bind the behavior to the keyboard. The native elements from HTML should have this covered but if you write a custom element, never forget that onkeydown, onkeypress and onkeyup events are your best friends.



So, what’s really new?

The short answer:

Extending HTML interfaces.

The long answer:

Extending HTML interfaces :)

Let me explain.

Although a native element should be fully accessible, it might not provide the exact functionality you need or look the way you want. We can for sure write something adapted to our actual needs but we would have to take care of all the accessibility needs since custom elements have no implicit semantics or keyboard support.… So, why can’t we extend the functionality of a native element? Now we can. Or “can”.

Element interfaces

Here is the list of the existing HTML interfaces. With them, you can extend the native behavior. We can revisit our example for Custom Elements and extend HTMLButtonElement to add our own behavior. Here’s how:

<html><head><title>Extend HTML Interfaces</title><style>/* for dramatic effect */
            body { background-color: black; }
            * { color: white; }
        </style></head><body><button>Tell me the problem</button><button is="accessible-button">Tell me the problem</button><acessible-button>This one does not work</acessible-button>
        
        <script>class OurAccessibleButton extends HTMLButtonElement {
                constructor() {
                    super();
                    
                    this.setAttribute('aria-label', 'Telling the problem');
                    this.style.color = 'black';
                }
            }
            
            customElements
                .define('accessible-button', OurAccessibleButton, { 
                    extends: 'button'
                });
        </script></body>
</html>

If you remember the previous examples, I added (for dramatic effect :)) a CSS rule that all text would be white. This against the native look for a button makes it unreadable. While extending the normal element with the text black, we made it a bit more accessible (plus, I added an extra label on it).

The differences here are:

  • extends HTMLButtonElement instead of HTMLElement;
  • When we define the customElement, we pass as a third parameter an object with which element is extended (in this case “button”)
  • And we use it by referencing a native button element with is="accessible-button".

This renders something like:

The first button is not extended so it still has the color as white; The second one is extended so it now has the color black (plus, an ARIA label) and the third one does not work… Why? Because extending a HTML interface needs be done through the is attribute and not be referenced through the normal custom element tag.

Beware: if you check the “Extending custom HTML elements” page by google, there is a very important note:

Only Chrome 67 supports customized built-in elements ( status) right now. Edge and Firefox will implement it, but Safari has chosen not to implement it. This is unfortunate for accessibility and progressive enhancement. If you think extending native HTML elements is useful, voice your thoughts on  509 and  662 on Github.

It’s not the safest feature to use just yet, so always check where your product should be used on before using any of the features referenced on this article, specially this one.



So if Web Components are not supported everywhere yet, what should I do?

There’s an in-between state :)

Polyfills

First and foremost, there are polyfills. Check them here.

Libraries that makes your life easier

Angular ElementsPolymer and Stencil are just a few examples of libraries that can help you to use Custom Elements supported on multiple browsers.

Tools

There’s a bunch of tools to help you make your components more accessible. A favorite of mine is the Accessibility Developer Tools, offered by Google Accessibility: it’s an excellent Chrome extension.

There’s also a lot of linters out there to keep an eye on you (e.g., the current project I’m working on is a React one, so I’m using the eslint- plugin-jsx-a11yto keep an eye on me).



Tl;dr

Essential for accessibility is:

  • Semantics
  • Keyboard inputs
  • Text alternatives
  • Color Contrast

The building blocks of Web Components are:

  • HTML Templates
  • Shadow DOM
  • Custom Elements
  • …and a bit more

The basics for our code to be accessible is:

  • Using the Role model
  • Making our elements focusable
  • Conveying a visual aid for the focused element
  • Using ARIA labels
  • Binding events to the keyboard

There’s also a way to extend the native behavior of an element through Extending HTML Interfaces.



Finally, a lot of people that know way more than me:

Regarding the accessibility of Web Components:

Regarding accessibility in general:

Regarding Web Components in general:


This article is based on a talk first lectured on a meet-up that happened on the December 5th, 2018. You can find that presentation here.

要查看或添加评论,请登录

Cristiano Correia的更多文章

社区洞察

其他会员也浏览了