Inclusive Components
Inclusive Components
Inclusive Components
1 Toggle Buttons 8
2 A To-do List 37
9 Notifications 233
12 Cards 301
4 Introduction
I
am not a computer scientist. I have no idea how to grow a
computer in a test tube, or how to convert the mysterious
breast-enlarging substance “silicone” into a semi-sentient
logic machine. Or whatever it is computer scientists do.
You can do all the code, but only if you don’t do it all well.
There’s just too much to learn to be an expert in everything.
So when we hire generalist coders, we create terrible products
and interfaces. The web isn’t inaccessible because web acces-
sibility is especially hard to learn or implement. It’s inaccessi-
ble because it’s about the code where humans and computers
meet, which is not a position most programmers care to be in,
or are taught how to deal with. But they’re the coders so it’s
their job, I guess.
6 Introduction
1 https://smashed.by/inclusivecomponents
A Personal Note 7
Thank you to all the people who have read and shared the
articles from the blog, and especially to those who have
helped fund its writing. Writing is my favorite thing, whether
it’s natural language or code. I’m just lucky that English is
my first language, because it takes me forever to learn the
syntax of anything. If you wish to translate the book, please
contact me using [email protected], find me on
Twitter as @heydonworks, or on Mastodon as
@[email protected].
Yours — Heydon
8 Chapter 1
Toggle Buttons
S
ome things are either on or off and, when those things
aren’t on (or off), they are invariably off (or on). The
concept is so rudimentary that I’ve only complicated it
by trying to explain it, yet on/off switches (or toggle buttons)
are not all alike. Although their purpose is simple, their appli-
cations and forms vary greatly.
Changing state
If a web application did not change according to the instruc-
tions of its user, the resulting experience would be altogether
unsatisfactory. Nevertheless, the luxury of being able to make
web documents augment themselves instantaneously, without
recourse to a page refresh, has not always been present.
<fieldset>
<legend>Notify by email</legend>
<input type="radio" id="notify-on" name="notify"
value="on" checked>
<label for="notify-on">on</label>
<input type="radio" id="notify-off" name="notify"
value="off">
<label for="notify-off">off</label>
</fieldset>
2 https://smashed.by/radiobuttons
3 http://wtfforms.com/
14 Chapter 1
<button type="submit">Send</button>
Toogle Buttons 15
But these are only one variety of button, covering one use
case. In truth, <button> elements can be used for all sorts
of things, and not just in association with forms. They’re just
buttons. We remind ourselves of this by giving them the type
value of <button>.
<button type="button">Send</button>
Switching the state from true (on) to false (off) can be done
via a simple click handler. Since we are using a <button>, this
event type can be triggered with a mouse click, a press of
either the Space or Enter keys, or by tapping the button
through a touchscreen. Being responsive to each of these
actions is something built into <button> elements
as standard.
Toogle Buttons 17
You can see the toggle button demo, using aria-pressed, over at
https://smashed.by/togglebuttonpressed
4 https://smashed.by/buttonelement
18 Chapter 1
<button :aria-pressed="this.pressed.toString()">Press
me</button>
A CLEARER STATE
An interesting thing happens when a button with the
aria-pressed state is encountered by some screen readers:
it is identified as a “toggle button” or, in some cases, “push
button”. The presence of the state attribute changes the
button’s identity.
STYLING
The HTML we construct is an important part of the design
work we do and the things we create for the web. I’m a strong
believer in doing HTML First Prototyping™, making sure
there’s a solid foundation for the styled and branded product.
• aria-pressed="true" → [aria-pressed="true"]
attribute selector
/* For example... */
button {
color: white;
background-color: #000;
border-radius: 0.25rem;
padding: 1em 1.5em;
}
[aria-pressed='true'] {
box-shadow: inset 0 0 0 0.15rem #000, inset 0.25em
0.25em 0 #fff;
}
[aria-pressed] {
position: relative;
top: -0.25rem;
left: -0.25rem;
box-shadow: 0.125em 0.125em 0 #fff, 0.25em 0.25em
#000;
}
This styling method is offered just as one idea. You may find
that something more explicit, like the use of “on”/“off” labels
in an example to follow, is better understood by more users.
These versions of the control would fail WCAG 2.0 1.4.1 Use Of Color
(Level A).5
Focus styles
5 https://smashed.by/contrastwithoutcolor
Toogle Buttons 23
6 https://smashed.by/highcontrastmode
24 Chapter 1
[aria-pressed]:focus {
outline: 2px solid transparent; /* for WHCM */
box-shadow: 0 0 0 0.25rem skyBlue;
outline: 2px solid transparent;
}
Changing labels
The previous toggle button design has a self-contained,
unique label and differentiates between its two states through
a change in attribution that elicits a style. What if we wanted
to create a button that changes its label from “on” to “off” or
“play” to “pause”?
The problem with this method is that the label change is not
announced as it happens. That is, when you click the play
button, feedback equivalent to “pressed” is absent. Instead,
you have to unfocus and refocus the button manually to hear
that it has changed. Not an issue for sighted users, but less
ergonomic for blind screen reader users.
This works pretty well, except for where voice recognition and
activation is concerned. In voice recognition software, you typ-
ically need to identify buttons by vocalizing their labels. And if
a user sees a play symbol, their inclination is to say “play”, not
“pause”. For this reason, switching the label rather than the
state is more robust here.
Toogle Buttons 27
Never change label and state at the same time. In this example, that
would result in a paused button in the pressed state. Since the video
or audio would be playing at this point, the paused state cannot be
considered pressed, or on.
7 https://smashed.by/ariaxenophobe
28 Chapter 1
.visually-hidden {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px) !important;
padding:0 !important;
border:0 !important;
height: 1px !important;
width: 1px !important;
overflow: hidden !important;
}
Toogle Buttons 29
AUXILIARY LABELING
In some circumstances, we may want to provide on/off
switches that actually read “on/off”. The trick with these is
making sure there is a clear association between each toggle
switch and a respective, auxiliary label.
<h2>Notifications</h2>
<ul>
<li>
Notify by email
<button>
<span>on</span>
<span>off</span>
</button>
</li>
<li>
Notify by SMS
<button>
<span>on</span>
<span>off</span>
</button>
</li>
<!-- others -->
</ul>
30 Chapter 1
8 https://smashed.by/togglebuttonsswitches
Toogle Buttons 31
<h2>Notifications</h2>
<ul>
<li>
<span id="notify-email">Notify by email</span>
<button aria-labelledby="notify-email">
<span>on</span>
<span>off</span>
</button>
</li>
<li>
<span id="notify-sms">Notify by SMS</span>
<button aria-labelledby="notify-sms">
<span>on</span>
<span>off</span>
</button>
</li>
<!-- others -->
</ul>
<h2>Notifications</h2>
<ul>
<li>
<span id="notify-email">Notify by email</span>
<button role="switch" aria-checked="true" aria-
labelledby="notify-email">
<span>on</span>
<span>off</span>
</button>
</li>
<li>
<span id="notify-sms">Notify by SMS</span>
<button role="switch" aria-checked="true" aria-
labelledby="notify-sms">
<span>on</span>
<span>off</span>
</button>
</li>
<!-- others -->
</ul>
9 https://smashed.by/switchrole
Toogle Buttons 33
How you would style the active state is quite up to you, but
I’d personally save on writing class attributes to the <span>s
with JavaScript. Instead, I’d write some CSS using pseudo-
classes to target the relevant span dependent on the state.
[role='switch'][aria-checked='true'] :first-child,
[role='switch'][aria-checked='false'] :last-child {
background: #000;
color: #fff;
}
Even when navigating by Tab key, it’s not only the identity
and state of the interactive elements you are focusing that
will be announced in screen readers. For example, when you
focus the first <button>, you’ll hear that it is a switch with the
label “Notify by email”, in its on state. “Switch” is the role and
aria-checked="true" is vocalized as “on” where this role
is present.
34 Chapter 1
Conclusion
How you design and implement your toggle buttons is quite
up to you, but I hope you’ll remember this chapter when it
comes to adding this particular component to your pattern
36 Chapter 1
You can take the basics explored here and add all sorts of
design nuances, including animation. It’s just important to lay
a solid foundation first.
CHECKLIST
A To-do List
A
ccording to tradition, each new JavaScript framework
is put through its paces in the implementation of a
simple to-do list app: an app for creating and delet-
ing to-do list entries. The first Angular example I ever read
was a to-do list. Adding and removing items from to-do lists
demonstrates the immediacy of the single-page application
view/model relationship.
10 https://smashed.by/todomvc
38 Chapter 2
The heading
A great deal of usability is about labels. The <label> element
provides labels to form fields, of course. But simple text nodes
provided to buttons and links are also labels: they tell you
what those elements do when you press them.
HEADING LEVEL
Determining the correct level for the heading is often con-
sidered a question of importance, but it’s actually a question
of belonging. If our to-do list is the sole content within the
<main> content of the page, it should be level 1, as in the
previous example. There’s nothing surrounding it, so it’s at the
highest level in terms of depth.
• Bars (<h3>)
• Clubs (<h3>)
Even if you feel that your packing to-do list is less important
than establishing which bars are good to visit, it’s still on the
same level in terms of belonging, so it must have the same
heading level.
11 https://smashed.by/headinglevels
THE <SECTION> ELEMENT
With all this talk of sections, surely we should be using
<section> elements, right? Maybe. Here are a couple of
things to consider:
<section aria-labelledby="todos-label">
<h1 id="todos-label">My To-do List</h1>
<!-- content -->
</section>
The list
I talk about the virtues of lists in Inclusive Design Patterns.12
Alongside headings, lists help to give pages structure. With-
out headings or lists, pages are featureless and monoto-
nous, making them very difficult to unpick, both visually
and non-visually.
A to-do list is, as the name suggests, a list. Since our particular
to-do list component makes no assertions about priority, an
unordered list is fine. Here’s the structure for a static version
of our to-do list (the adding, deleting, and checking function-
ality has not yet been added):
12 https://smashed.by/inclusivedesignpatterns
A To-do List 45
<section aria-labelledby="todos-label">
<h1 id="todos-label">My To-do List</h1>
<ul>
<li>
Pick up kids from school
</li>
<li>
Learn Haskell
</li>
<li>
Sleep
</li>
</ul>
</section>
EMPTY STATE
Empty states are an aspect of UI design which you neglect at
your peril.13 Inclusive design has to take user life cycles into
consideration, and some of your most vulnerable users are
new ones. To them your interface is unfamiliar and, without
carefully leading them by the hand, that unfamiliarity can
be off-putting.
13 https://smashed.by/emptystates
46 Chapter 2
complex than this simple to-do list, so let’s add an empty state
anyway — for practice.
Your own interface may be a little more complex than this to-do list, so
let’s add an empty state for practice.
.empty-state, ul:empty {
display: none;
}
ul:empty + .empty-state {
display: block;
}
FORM OR NO FORM?
It’s quite valid in HTML to provide an <input> control outside
of a<form> element. The <input> will not succeed in provid-
ing data to the server without the help of JavaScript, but that’s
not a problem in an application using XHR.
<form>
<input type="text" placeholder="E.g. Adopt an owl">
<button type="submit">Add</button>
</form>
LABELING
Can you spot the deliberate mistake in the above code
snippet? The answer is: I haven’t provided a label. Only a
placeholder is provided and placeholders are intended
for supplementary information only, such as the “adopt an
owl” suggestion.
14 https://twitter.com/LeonieWatson
50 Chapter 2
In addition, make sure forms with multiple fields have visible labels for
each field. Otherwise the user does not know which field is for what.
<form>
<label for="add-todo" class="visually-hidden">Add a
to-do item</label>
<input id="add-todo" type="text" placeholder="E.g.
Adopt an owl">
<button type="submit">Add</button>
</form>
::-webkit-input-placeholder {
color: #444;
font-style: italic;
}
::-moz-placeholder {
color: #444;
font-style: italic;
}
:-ms-input-placeholder {
color: #444;
font-style: italic;
}
:-moz-placeholder {
color: #444;
font-style: italic;
}
15 https://smashed.by/levelaaa
A To-do List 53
SUBMISSION BEHAVIOR
One of the advantages of using a <form> with a button of the
submit type is that the user can submit by pressing the
button directly, or by hitting Enter . Even users who do not
rely exclusively on the keyboard to operate the interface may
like to hit Enter because it’s quicker. What makes interaction
possible for some, makes it better for others. That’s inclusion.
<form>
<input type="text" aria-invalid="true"
aria-label="Write a new to-do item"
placeholder="E.g. Adopt an owl">
<button type="submit" disabled>Add</button>
</form>
FEEDBACK
The deal with human–computer interaction is that when one
party does something, the other party should respond. It’s
only polite. For most users, the response on the part of the
computer to adding an item is implicit: they simply see the
item being added to the page. If it’s possible to animate the
appearance of the new item, all the better: some movement
means its arrival is less likely to be missed.
For users who are not sighted or are not using the interface
visually, nothing would seem to happen. They remain focused
on the input, which offers nothing new to be announced in
screen reader software. Silence.
.visually-hidden {
position: absolute !important;
clip: rect(1px, 1px, 1px, 1px) !important;
padding:0 !important;
border:0 !important;
height: 1px !important;
width: 1px !important;
overflow: hidden !important;
}
58 Chapter 2
<ul>
<li v-for="(todo, index) in todos">
<input type="checkbox" :id="`todo-${index}`"
A To-do List 59
v-model="todo.done">
<label :for="`todo-${index}`">{{todo.name}}</
label>
</li>
</ul>
:checked + label {
text-decoration: line-through;
}
<button>
<svg focusable="false">
<use xlink:href="#bin-icon"></use>
</svg>
<span class="visually-hidden">delete {{todo.name}}</
span>
</button>
<body>
<svg style="display: none">
<symbol id="bin-icon" viewBox="0 0 20 20">
<path d="[path data here]">
</symbol>
</svg>
16 https://smashed.by/mythicons
Fortunately, the accidental deletion of a to-do item is not
really a critical mistake, so users can safely find out what the
icon means through trial and error. Where deletion is critical,
a confirmation dialog should be provided, acting as both an
explanation and a means to complete the action.
FOCUS MANAGEMENT
When a user clicks the delete button for a to-do item, the
to-do item — including the checkbox, the label, and the
delete button itself — will be removed from the DOM. This
raises an interesting problem: what happens to focus when
you delete the currently focused element?
The truth is, browsers don’t know where to place focus when it
has been destroyed in this way. Some maintain a sort of ghost
focus where the item used to exist, while others jump to focus
the next focusable element. Some flip out completely and
default to focusing the outer document — meaning keyboard
users have to crawl through the DOM back to where the
removed element was.
One option is to focus the first checkbox of the list. Not only
will this announce the checkbox’s label and state, but also
the total number of list items remaining: one fewer than a
moment ago. All useful context.
document.querySelector('ul input').focus();
But what if we just deleted the last to-do item in our list
and had returned to the empty state? There’s no checkbox
we can focus. Let’s try something else. Instead, I want to do
two things:
17 https://smashed.by/navmechanisms
66 Chapter 2
After the focused element (and with it its focus style) has been
removed, the heading is focused. A keyboard user can then
press Tab to find themselves on that first checkbox or — if
there are no items remaining — the text input at the foot of
the component.
The feedback
function deletedFeedback(todoName) {
liveRegion.textContent = `${todoName} deleted.`;
}
68 Chapter 2
Working demo
I’ve created a demo18 to demonstrate the techniques in this
post. It uses Vue.js, but could have been created with any
JavaScript framework. It’s offered for testing with different
screen reader and browser combinations.
18 https://smashed.by/todolistvuedemo
A To-do List 69
Conclusion
Counting semantic structure, labeling, iconography, focus
management and feedback, there’s quite a lot to consider
when creating an inclusive to-do list component. If that
makes inclusive design seem dauntingly complex, consider
the following:
CHECKLIST
C
lassification is hard. Take crabs, for example. Hermit
crabs, porcelain crabs, and horseshoe crabs are not —
taxonomically speaking — true crabs. But that doesn’t
stop us using the “crab” suffix. It gets more confusing when,
over time and thanks to a process called carcinisation, untrue
crabs evolve to resemble true crabs more closely. This is the
case with king crabs, which are believed to have been hermit
crabs in the past. Imagine the size of their shells!
Let’s start with a quiz. Is the box of links hanging down from
the navigation bar in the illustration a menu?
• Home
• About
• Shop
• Dog costumes
• Waffle irons
• Magical orbs
• Contact
By the same token, many but not all smaller devices are
touch devices. In inclusive design, you cannot afford to
make assumptions.
19 https://smashed.by/contentorg
76 Chapter 3
Tables of content
Tables of content are navigation for related pages or page
sections and should be semantically similar to main site
navigation regions, using a <nav> element, a list, and a group
labeling mechanism.
<nav aria-labelledby="sections-heading">
<h2 id="sections-heading">Products</h2>
<ul>
<li><a href="/products/dog-costumes">Dog costumes
</a></li>
<li><a href="/products/waffle-irons">Waffle irons
</a></li>
<li><a href="/products/magical-orbs">Magical orbs
</a></li>
</ul>
</nav>
<!-- each section, in order, here -->
Menus & Menu Buttons 77
NOTES
<nav aria-labelledby="sections-heading">
<h2 id="sections-heading">Products</h2>
<ul>
<li><a href="#dog-costumes">Dog costumes</a></li>
<li><a href="#waffle-irons">Waffle irons</a></li>
<li><a href="#magical-orbs">Magical orbs</a></li>
</ul>
</nav>
<!-- dog costumes section here -->
<section id="waffle-irons" tabindex="-1">
<h2>Waffle Irons</h2>
</section><!-- magical orbs section here -->
20 https://www.gov.uk/
21 https://smashed.by/hugotemplates
80 Chapter 3
PROGRESSIVE ENHANCEMENT
But let’s not get ahead of ourselves. We ought to be mindful of
progressive enhancement and consider how this would work
without JavaScript.
<a href="#navigation">navigation</a>
<!-- some content here perhaps -->
<nav id="navigation">
<ul>
Menus & Menu Buttons 81
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/content">Content</a></li>
</ul>
</nav>
<nav id="navigation">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/content">Content</a></li>
</ul>
</nav>
You enhance this by adding the button, in its initial state, and
hiding the navigation (using the hidden attribute):
<nav id="navigation">
<button aria-expanded="false">Menu</button>
<ul hidden>
<li><a href="/">Home</a></li>
82 Chapter 3
<li><a href="/about">About</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
[hidden] {
display: none;
}
PLACEMENT
Where a lot of people go wrong is by placing the button
outside the region. This would mean screen reader users who
move to the <nav> using a shortcut would find it to be empty,
which isn’t very helpful. With the list hidden from screen read-
ers, they’d just encounter this:
Menus & Menu Buttons 83
<nav id="navigation"></nav>
ARIA-CONTROLS
As I wrote in “Aria-controls Is Poop,”22 the aria-controls
attribute, intended to help screen reader users navigate from
a controlling element to a controlled element, is only sup-
ported in the JAWS screen reader. So you simply can’t rely
on it.
22 https://smashed.by/ariacontrols
84 Chapter 3
In this case, I would recommend (1). It’s a lot simpler since you
don’t have to worry about moving focus back to the button
and on which event(s) to do so. Also, there’s currently nothing
in place to warn users that their focus will be moved to some-
where different. In the true menus we’ll be discussing shortly,
this is the job of aria-haspopup="true".
<nav id="navigation">
<button aria-expanded="false" aria-controls="menu-
list">Menu</button>
<ul id="menu-list" hidden>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
<li><a href="/shop">Shop</a></li>
<li><a href="/contact">Contact</a></li>
</ul>
</nav>
Menus & Menu Buttons 85
<ul role="menu">
<li role="menuitem">Item 1</li>
<li role="menuitem">Item 2</li>
<li role="menuitem">Item 3</li>
</ul>
The short answer is: no. The long answer is: no, because
our list items contain links and menuitem elements are not
intended to have interactive descendants.24 That is, they are
the controls in a menu.
23 https://smashed.by/ariarolesmenu
24 https://smashed.by/ariamenuitem
25 https://smashed.by/ariarolespresentation
86 Chapter 3
We want the user to know that they are using a link and can
expect link behavior, so this is no good. Like I said, true menus
are for (JavaScript-driven) application behavior.
26 https://smashed.by/consistentbehaviour
88 Chapter 3
True menus
Now that we’ve had the discussion about false menus and
quasi-menus, the time has arrived to create a true menu,
as opened and closed by a true menu button. From here on
in I will refer to the button and menu together as simply a
“menu button.”
NOTES
• The aria-haspopup property simply indicates that the
button secretes a menu. It acts as warning that, when
pressed, the user will be moved to the “popup” menu
(we’ll cover focus behavior shortly). Its value does not
change — it remains as true at all times.
27 https://smashed.by/htmlbutton
28 https://smashed.by/menubutton
Menus & Menu Buttons 91
MenuButton.prototype.open = function() {
this.button.setAttribute('aria-expanded', true);
this.menu.hidden = false;
this.menu.querySelector(':not([disabled])').focus();
return this;
};
Menus & Menu Buttons 93
We can execute this method where the user presses the down
key on a focused menu button instance:
this.button.addEventListener(
'keydown',
function(e) {
if (e.keyCode === 40) {
this.open();
}
}.bind(this)
);
/* menu closed */
[type="checkbox"] + [role="menu"] {
display: none;
}
/* menu open */
[type="checkbox"]:checked + [role="menu"] {
display: block;
}
29 https://smashed.by/hidecontent
/* class to hide spans visually */
.visually-hidden {
position: absolute;
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(100%);
padding: 0;
border: 0;
height: 1px
width: 1px;
overflow: hidden;
}
30 https://smashed.by/hamburgerwithoutjs
98 Chapter 3
MenuButton.prototype.choose = function(choice) {
// Define the 'choose' event
var chooseEvent = new CustomEvent('choose', {
detail: {
choice: choice
}
});
// Dispatch the event
this.button.dispatchEvent(chooseEvent);
return this;
};
exampleMenuButton.addEventListener('choose', e => {
// Get the node’s text content (label)
let choiceLabel = e.details.choice.textContent;
When a user chooses an option, the menu closes and focus is returned
to the menu button. It’s important users are returned to the triggering
element after the menu is closed.
100 Chapter 3
Persisting choices
Not all menu items are for choosing persistent settings. Many
just act like standard buttons which make something in the
interface happen when pressed. However, in the case of our
difficulty menu button, we’d like to indicate which is the cur-
rent difficulty setting — the one chosen last.
<div role="menu">
<button role="menuitemradio" tabindex="-1">Easy</
button>
<button role="menuitemradio" aria-checked="true"
tabindex="-1">Medium</button>
<button role="menuitemradio" tabindex="-
1">Incredibly Hard</button>
</div>
[role='menuitem'][aria-checked='true']::before {
content: '\2713\0020';
}
31 https://smashed.by/menubuttonvoiceover
Menus & Menu Buttons 103
32 https://twitter.com/HugoGiraudel
33 https://smashed.by/reactmenubutton
104 Chapter 3
Checklist
• Don’t use ARIA menu semantics in navigation
menu systems.
T
ooltips — affectionately misnomered as “tootlips”
by my friend Steve34 — are a precariously longstand-
ing interface pattern. Literally “tips for tools”, they
are little bubbles of information that clarify the purpose of
otherwise ambiguous controls/tools. A common example is a
control that is only represented by a cryptic icon, the meaning
of which the user has yet to learn.
34 https://twitter.com/stevefaulkner
35 https://smashed.by/toggletip
106 Chapter 4
“If you want to hide content from mobile and tablet users
as well as assistive tech users and keyboard only users, use
the title attribute.” — The Paciello Group blog36
36 https://smashed.by/titleattribute
Tooltips & Toggletips 107
37 https://smashed.by/idp
108 Chapter 4
The usual excuse for not providing textual labels is, “there’s
no space.” And there likely isn’t, if you set out not to include
textual labels in the first place. If you treat them as important
from the beginning, you will find a way.
There’s always room for text if you make it, but some configurations
leave more space for text than others.
Inclusive tooltips
The first thing to get right is making the text in the tooltip
accessible to assistive technologies. There are a couple of
different methods for associating the tooltip to the focused
Tooltips & Toggletips 109
</button>
<div role="tooltip" id="notifications-
label">Notifications</div>
<button class="notifications">Notifications</button>
38 https://smashed.by/arialabelledby
Tooltips & Toggletips 111
REDUNDANT TOOLTIPS
All the time as an interface design consultant, I see people
providing title attributes to links with identical text nodes.
.visually-hidden {
clip-path: inset(100%);
clip: rect(1px, 1px, 1px, 1px);
height: 1px;
overflow: hidden;
position: absolute;
white-space: nowrap;
width: 1px;
}
INTERACTION
To improve upon the notoriously awful title attribute, our
custom tooltips should appear on focus as well as hover. By
supplying the tooltip in an element adjacent to the button, we
can do this with just CSS:
Tooltips & Toggletips 115
[role='tooltip'] {
display: none;
}
button:hover + [role='tooltip'],
button:focus + [role='tooltip'] {
display: block;
}
.button-and-tooltip {
position: relative;
}
[role='tooltip'] {
position: absolute;
Touch interaction
So far this simply doesn’t work so well for touchscreen users
because the focus and active states happen simultaneously.
In practice, this means you’ll see the tooltip, but only as the
button is being pressed.
116 Chapter 4
There are other things you could try, of course. One might
be to suppress the button’s action on the first press so it just
shows the tooltip that time around. You could take this “tuto-
rial mode” idea further still and show the tooltips as inline
text for newcomers, but streamline the interface to just show
icons for established users. By then, they should have learned
what the icons represent.
In either case, the landing screen for each of the options should have a
clear (<h1>) heading with the same wording as the labels. Then at least
the user knows where the icon took them on arrival.
Inclusive toggletips
Toggletips are like tooltips in the sense that they can provide
supplementary or clarifying information. However, they differ
by making the control itself supplementary: toggletips exist to
reveal information balloons, and serve no other purpose.
<span class="tooltip-container">
<button type="button" data-toggletip-content="This
clarifies whatever needs clarifying">
<span aria-hidden="true">i</span>
<span class="visually-hidden">More info</span>
</button>
<span role="status"></span>
</span>
39 https://smashed.by/liveregions
Tooltips & Toggletips 119
<span class="tooltip-container">
<button type="button" data-toggletip-content="This
clarifies whatever needs clarifying">
<span aria-hidden="true">i</span>
<span class="visually-hidden">More info</span>
</button>
<span role="status">
<span class="toggletip-bubble">This clarifies
whatever needs clarifying</span>
</span>
</span>
(function() {
// Get all the toggletip buttons
const toggletips = document.querySelectorAll('[data-
toggletip-content]');
// Close on blur
toggletip.addEventListener('blur', e => {
liveRegion.innerHTML = '';
});
Notes
40 https://smashed.by/basictoggletip
122 Chapter 4
41 https://smashed.by/progtoggletip
124 Chapter 4
[data-tooltip]:not(button) {
42 https://smashed.by/idp
Tooltips & Toggletips 125
The clear red outline shows there is an error present and guides the
developer’s DOM inspector cursor.
Conclusion
Most of the time, tooltips shouldn't be needed if you provide
clear textual labeling and familiar iconography. Most of the
time toggletips are a complex way of providing information
that could just be part of the document’s prose content.
Remember that inclusive design is about choosing what you
need to implement before how you need to implement it. It
may not be beneficial to use either tooltips or toggletips. But
since I see them being implemented all the time regardless, I
wanted to talk about how to at least do them justice.
126 Chapter 4
CHECKLIST
A Theme Switcher
M
y mantra for building web interfaces is, “If it can’t
be done efficiently, don’t do it at all.” In fact, I’ve
preached about writing less damned code43 around
the UK, Europe, and China. If a feature can only be achieved
by taking a significant performance hit, the net effect is nega-
tive and the feature should be abandoned. That’s how critical
performance is on the web.
43 https://smashed.by/lesscodevideo
44 https://smashed.by/offerchoice
128 Chapter 5
:root {
filter: invert(100%);
}
The only trouble is that filter can only invert stated colors.
Therefore, if the element has no background color, the text
will invert but the implicit (white) background will remain the
same. The result? Light text on a light background.
:root {
background-color: #fefefe;
filter: invert(100%);
}
But we may still run into problems with child elements that
also have no stated background color. This is where CSS’s
inherit keyword comes in handy.
:root {
background-color: #fefefe;
filter: invert(100%);
}
* {
background-color: inherit;
}
:root {
background-color: #fefefe;
filter: invert(100%);
}
* {
background-color: inherit;
}
img:not([src*='.png']),
video {
filter: invert(100%);
}
45 https://smashed.by/reactcomponent
132 Chapter 5
<div>
<button aria-pressed="false">
dark theme:
<span aria-hidden="true">off</span>
</button>
<style media="none">
html { filter: invert(100%); background: #fefefe }
* { background-color: inherit }
img:not([src*=".png"]), video { filter:
invert(100%) }
</style>
</div>
SWITCHING STATE
Our component will be stateful, allowing the user to toggle
the dark theme between inactive and active. Note that
localStorage will be used to persist the user’s preference.
toggle() {
this.setState(
{
active: !this.state.active
},
() => {
localStorage.setItem(this.props.storeKey, this.
state.active)
}
)
}
Of course, when the dark theme is on, the button itself is also inverted.
useEffect(() => {
if (preserveRasters) {
setCss(`${cssString} ${rasterCss}`);
}
return () => {
setCss(cssString);
};
}, [preserveRasters]);
136 Chapter 5
useEffect(() => {
localStorage.setItem(storeKey, active);
}, [active, storeKey]);
46 https://smashed.by/modernizr
138 Chapter 5
return (
supported.current && (
<Fragment>
<button aria-pressed={active} onClick={toggle}>
Inverted theme:{' '}
<span aria-hidden="true">{active ? 'On' :
'Off'}</span>
</button>
<style media={active ? 'screen' : 'none'}>
{active ? css.trim() : css}
</style>
</Fragment>
)
);
What if the user has already chosen dark mode at the operat-
ing system level? We should support their decision by default,
by honoring the prefers-color-scheme: dark @media
query. This is possible by amending the default value for
active in the useState definition:
WINDOWS HIGH CONTRAST MODE
Windows users are offered a number of high contrast themes
at the operating system level — some light-on-dark like our
inverted theme. In addition to supplying our theme switcher
feature, it’s important to make sure WHCM is supported as well
as possible. Here are some tips:
• For inline SVG icons, use the currentColor value for fill
and stroke. This way, the icon color will change along with
the surrounding text color when the high contrast theme
is activated.
DEMO
A working version of this code is available to download and
run from the React Theme Switch repository.47
47 https://smashed.by/reactthemeswitch
48 https://smashed.by/themeswitcher
A Theme Switcher 141
PLACEMENT
The only thing left to do is decide where you're going to put
the component in the document. As a rule of thumb, utilities
like theme options should be found in a landmark region —
just not the <main> region, because the screen reader user
expects this content to change between pages. The <header>
(role="banner") or <footer> (role="contentinfo") are
both acceptable.
49 https://smashed.by/beconsistent
142 Chapter 5
CHECKLIST
Tabbed Interfaces
W
hen you think about it, most of your basic inter-
actions are showing or hiding something some-
how. I’ve already covered popup menu buttons
and the simpler and less assuming tooltips and toggletips.
You can add simple disclosure widgets, compound “accordi-
ons”, and their sister component the tabbed interface to that
list. It’s also worth noting that routed single-page applica-
tions emulate the showing and hiding of entire web pages
using JavaScript.
Enhancement
What if I used some CSS to make just the chosen section from
my table of contents visible? This is certainly possible using
the :target pseudo-class.
Tabbed Interfaces 145
section:not(:target) {
display: none;
}
50 https://smashed.by/hashupdating
Tabbed Interfaces 147
<ul role="tablist">
<li role="presentation">
<a role="tab" href="#section1" id="tab1" aria-
selected="true">Section 1</a>
</li>
<li role="presentation">
<a role="tab" href="#section2" id="tab2">Section
2</a>
</li>
<li role="presentation">
<a role="tab" href="#section3" id="tab3">Section
3</a>
</li>
</ul>
<section role="tabpanel" id="section1" aria-
labelledby="tab1">
...
</section>
<section role="tabpanel" id="section2" aria-
labelledby="tab2" hidden>
...
</section>
<section role="tabpanel" id="section3" aria-
...
</section>
148 Chapter 6
Keyboard behavior
Unlike a same-page link, a tab does not move the user to the
associated section/panel of content. It just reveals the content
visually. This is advantageous to sighted users (including
sighted screen reader users) who wish to flit between different
Tabbed Interfaces 149
<ul role="tablist">
<li role="presentation">
<a role="tab" tabindex="-1"
href="#section1">Section 1</a>
</li>
<li role="presentation">
<a role="tab" href="#section2" aria-
selected="true">Section 2</a>
</li>
<li role="presentation">
<a role="tab" tabindex="-1"
Tabbed Interfaces 151
href="#section2">Section 3</a>
</li>
</ul>
tab.addEventListener('keydown', e => {
// Get the index of the current tab in the tabs node
list
let index = Array.prototype.indexOf.call(tabs,
e.currentTarget);
switchTab(e.currentTarget, tabs[newIndex]);
tabs[newIndex].focus();
}
});
51 https://smashed.by/keyboardinteraction
52 https://smashed.by/tabbedinterfacedemo
Tabbed Interfaces 153
tab.addEventListener(‘keydown’, e => {
// Get the index of the current tab in the tabs node
list
let index = Array.prototype.indexOf.call(tabs,
e.currentTarget);
53 https://smashed.by/nvda
154 Chapter 6
Since tab panels are labeled by their tabs, when the down
arrow is pressed and the relevant tab panel focused, a screen
reader will announce, “[tab label], tab panel”, thereby
assuring the user of their new location within the interface.
From there, they can continue to browse down through the
tab panel’s descendant elements or press Shift + Tab to
return to the tablist and the selected tab.
Note that this technique is not from the official W3C doc-
umentation on tabbed interfaces. It is a small, unintrusive
enhancement I created drawing on experience with testing.
While following conventions is important for establishing
familiar, easy-to-use interfaces, you still have to adapt and
improve where there’s room.
Tabbed Interfaces 155
54 https://smashed.by/focusorder
156 Chapter 6
RESPONSIVE DESIGN
Responsive design is inclusive design. Not only is a responsive
design compatible with a maximal number of devices, but it’s
also sensitive to a user’s zoom settings. Full-page zoom trig-
gers @media breakpoints just as narrowing the viewport does.
55 https://smashed.by/tabbedinterfacedemo
56 https://smashed.by/bbctabs
Tabbed Interfaces 157
Where there are very many tabs or the number of tabs are
an unknown quantity, an accordion at all screen widths is a
safe bet. Single-column layouts are responsive regardless of
content quantity.
USE LINKS!
Make sure the links that allow users to choose between views
are indeed links — whether or not those links return false
and use JavaScript to switch to the new view. Since these con-
trols will navigate the user (by changing their focus location;
see below) the link role is the most appropriate for the behav-
ior. Link elements do not need the link ARIA role attribute;
they are communicated as “link” by default.
57 https://smashed.by/xiaohome
Tabbed Interfaces 161
MANAGE FOCUS
Just replacing some content on the page does not automat-
ically move the user to that content or (in the case of blind
assistive technology users) alert them to its existence. As
covered under “The focus of non-interactive elements” on
page 155, you can focus the principle heading of the new
route view, or the outer view element. If you are focusing the
outer view element, it is recommended it is labeled either
directly using aria-label or by the principle heading using
aria-labelledby. The aria-labelledby method is pre-
ferred because it reduces redundancy — and the danger of
things going out of sync — while also ensuring the label, as a
text node, is translatable.
IN REACT
You can achieve the same ends in React on a per-component
basis, by tapping into each route’s component’s component-
DidMount() method. The best way is probably to create a ref
for the target element.
componentDidMount() {
// Change the <title>
document.title = ‘My App: Home’;
// Focus the view
this.focusTarget.focus();
}
Tabbed Interfaces 163
render() {
return (
<div aria-labelledby=”heading” role=”region”
tabindex=”-1” ref={this.focusTarget}>
<h1 id=”heading”>Home</h1>
// Content here
</div>
)
}
}
Conclusion
JavaScript can show and hide or create and destroy content
with ease, but these DOM events can have different purposes
and meanings depending on the context. In this chapter, we
facilitated the basic show/hide ability of JavaScript to create
two quite different interfaces: a tabbed interface and sin-
gle-page application navigation.
58 https://smashed.by/router
Tabbed Interfaces 165
CHECKLIST
Collapsible Sections
C
ollapsible sections are perhaps the most rudimentary
of interactive design patterns on the web. All they do
is let you toggle the visibility of content by clicking
that content’s label. Big whoop.
59 https://smashed.by/caniusedetails
Collapsible Sections 167
<h2>My section</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing
elit. Cras efficitur laoreet massa. Nam eu porta
dolor. Vestibulum pulvinar lorem et nisl tempor
lacinia.</p>
<p>Cras mi #nisl, semper ut gravida sed, vulputate vel
mauris. In dignissim aliquet fermentum. Donec arcu
nunc, tempor sed nunc id, dapibus ornare dolor.</p>
<h2><button>My section</button></h2>
<div>
<p>Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Cras efficitur laoreet massa. Nam eu
porta dolor. Vestibulum pulvinar lorem et nisl tempor
lacinia.</p>
<p>Cras mi nisl, semper ut gravida sed, vulputate
vel mauris. In dignissim aliquet fermentum. Donec arcu
nunc, tempor sed nunc id, dapibus ornare dolor.</p>
</div>
STATE
Our component can be in one of two mutually exclusive
states: collapsed or expanded. This state can be suggested
visually, but also needs to be communicated non-visually. We
can do this by applying aria-expanded to the button, initially
in the false (collapsed) state. Accordingly, we need to hide
the associated <div> — in this case, with hidden.
h2 button {
all: inherit;
}
60 https://smashed.by/ariacontrols
172 Chapter 7
The text label and/or icon for a button should always show what press-
ing that button will do, hence the minus sign in the expanded state
indicating that the button will take the section content away.
61 https://smashed.by/affordances
Collapsible Sections 173
<button aria-expanded="false">
My section
<svg viewBox="0 0 10 10" focusable="false">
<rect class="vert" height="8" width="2" y="1"
x="4"
/>
<rect height="2" width="8" y="4" x="1" />
</svg>
</button>
174 Chapter 7
Note the class of “vert” for the rectangle that represents the
vertical strut. We’re going to target this with CSS to show and
hide it depending on the state, transforming the icon between
a plus and minus shape.
[aria-expanded="true"] .vert {
display: none;
}
button.setAttribute('aria-expanded', !expanded);
// Not needed ↓
button.classList.toggle('expanded');
h2 button:focus svg {
outline: 2px solid;
}
[aria-expanded] rect {
fill: currentColor;
}
<button aria-expanded="false">
My section
<svg viewBox="0 0 10 10 aria-hidden="true"
focusable="false">
<use xlink:href="#plusminus" />
</svg>
</button>
62 https://smashed.by/uselement
Collapsible Sections 177
A SMALL SCRIPT
Given the simplicity of the interaction and all the elements
and semantics being in place, we need only write a very
terse script:
(function() {
const headings = document.querySelectorAll('h2');
Array.prototype.forEach.call(headings, h => {
let btn = h.querySelector('button');
btn.onclick = () => {
let expanded = btn.getAttribute('aria-expanded')
=== 'true';
btn.setAttribute('aria-expanded', !expanded);
target.hidden = expanded;
}
})
})()
63 https://smashed.by/collapsiblesectionsdemo
178 Chapter 7
PROGRESSIVE ENHANCEMENT
The trouble with the script above is that it requires the HTML
to be adapted manually for the collapsible sections to work.
Implemented by an engineer as a component via a template
or JSX, this is expected. However, for largely static sites like
blogs there are two avoidable issues:
64 https://smashed.by/progcollapsiblesections
Collapsible Sections 179
<toggle-section open="false">
<h2>My section</h2>
<p>Lorem ipsum dolor sit amet, consectetur
adipiscing elit. Cras efficitur laoreet massa. Nam eu
porta dolor. Vestibulum pulvinar lorem et nisl tempor
lacinia.</p>
<p>Cras mi nisl, semper ut gravida sed, vulputate
vel mauris. In dignissim aliquet fermentum. Donec arcu
nunc, tempor sed nunc id, dapibus ornare dolor.</p>
</toggle-section>
180 Chapter 7
if ('content' in document.createElement('template')) {
// Define the <template> for the web component
if (document.head.attachShadow) {
THE TEMPLATE
We could place a template element in the markup and refer-
ence it, or create one on the fly. I’m going to do the latter.
tmpl.innerHTML = `
<h2>
<button aria-expanded="false">
<svg aria-hidden="true" focusable="false"
viewBox="0 0 10 10">
<rect class="vert" height="8" width="2" y="1"
x="4"/>
<rect height="2" width="8" y="4" x="1"/>
</svg>
</button>
</h2>
<div class="content" hidden>
<slot></slot>
</div>
65 https://smashed.by/reactwebcomponents
182 Chapter 7
<style>
h2 {
margin: 0;
}
h2 button {
all: inherit;
box-sizing: border-box;
display: flex;
justify-content: space-between;
width: 100%;
padding: 0.5em 0;
}
button svg {
height: 1em;
margin-left: 0.5em;
}
[aria-expanded="true"] .vert {
display: none;
}
[aria-expanded] rect {
fill: currentColor;
}
</style>
66 https://smashed.by/progcollapsiblesections
184 Chapter 7
Now we just need to make sure the level for the Shadow DOM
heading is faithful to the Light DOM original. I can query the
tagName of the Light DOM heading and augment the Shadow
DOM level with aria-level accordingly.
if (!level) {
console.warn('The first element inside each <toggle-
section> should be a heading of an appropriate
level.');
}
<h2 aria-level="3">
<button aria-expanded="false">
<svg aria-hidden="true" focusable="false"
viewBox="0 0 10 10">
<rect class="vert" height="8" width="2" y="1"
x="4"/>
<rect height="2" width="8" y="4" x="1"/>
</svg>
</button>
</h2>
toggle-section [aria-level="2"] {
font-size: 2rem;
}
toggle-section [aria-level="3"] {
font-size: 1.5rem;
}
/* etc */
<toggle-section role="region">
...
</toggle-section>
67 https://smashed.by/screenreadersurvey
188 Chapter 7
get open() {
return this.hasAttribute('open');
}
set open(val) {
if (val) {
this.setAttribute('open', '');
} else {
this.removeAttribute('open');
}
Collapsible Sections 189
}
static get observedAttributes() {
return ['open']
}
attributeChangedCallback(name) {
if (name === 'open') {
this.switchState();
}
}
this.btn.onclick = () => {
this.toggleAttribute('open');
}
190 Chapter 7
this.switchState = () => {
let expanded = this.hasAttribute('open');
this.btn.setAttribute('aria-expanded', expanded);
this.shadowRoot.querySelector('.content').hidden =
!expanded;
}
EXPAND/COLLAPSE ALL
Since we toggle <toggle-section> elements via their open
attribute, it’s trivial to afford users an ‘expand/collapse all’
behavior. One advantage of such a provision is that users who
have opened multiple sections independently can reset to an
initial, compact state for a better overview of the content. By
the same token, users who find fiddling with interactive ele-
ments distracting or tiresome can revert to scrolling through
open sections.
68 https://smashed.by/expandcollapseall
Collapsible Sections 191
<ul class="controls">
<li><button id="expand">expand all</button></li>
<li><button id="collapse">collapse all</button></li>
</ul>
connectedCallback() {
if (window.location.hash.substr(1) === this.heading.
id) {
this.setAttribute('open', 'true');
this.btn.focus();
}
}
Collapsible Sections 193
this.btn.onclick = () => {
let open = this.getAttribute('open') === 'true';
this.setAttribute('open', open ? 'false' : 'true');
(Note that the presence of the open property will mean the
section is open, regardless of whether it matches the URL #)
69 https://smashed.by/withhistory
194 Chapter 7
Conclusion
Your role as an interface designer and developer (yes, you
can be both at the same time) is to serve the needs of the
people receiving your content and using your functionality.
These needs encompass both those of end users and fellow
contributors. The product should, of course, be accessible
and performant, but maintaining and expanding the product
should be possible without esoteric technical knowledge.
CHECKLIST
70 https://smashed.by/2ndrule
196 Chapter 8
A Content Slider
C
arousels (or content sliders) are like men. They are not
literally all bad — some are even helpful and consid-
erate. But I don’t trust anyone unwilling to acknowl-
edge a glaring pattern of awfulness. Also like men, I appre-
ciate that many of you would rather just avoid dealing with
carousels, but often don’t have the choice. Hence this chapter.
Control
In the broadest terms, any inclusive component should be:
• Performant
That last point is one I have been considering a lot lately, and
it’s why I added “Do not include third parties that compro-
mise user privacy” to the inclusive web design checklist.71 As
well as nefarious activities, users should also be protected
from unexpected or unsolicited ones. This is why WCAG pre-
scribes the 2.2.2 Pause, Stop, Hide72 criterion, mandating the
ability to cease unwanted animations. In carousel terms, we’re
talking about the ability to cease the automatic cycling of
content slides by providing a pause or stop button.
71 https://smashed.by/inclusivedesignchecklist
72 https://smashed.by/pausehide
198 Chapter 8
It’s something, but I don’t think it’s good enough. You’re not
truly giving control,73 you’re taking it away then handing it
back later. For people with vestibular disorders for whom
animations can cause nausea, by the time the pause button is
located, the damage will have been done.
Our slider will not slide except when slid. But how is
sliding instigated?
Multimodal interaction
Multimodal means “can be operated in different ways.”
Supporting different modes of operation may sound like a lot
of work, but browsers are multimodal by default. Unless you
screw up, all interactive content can be operated by mouse,
keyboard, and (where the device supports it) touch.
73 https://smashed.by/givecontrol
A Content Slider 199
HORIZONTAL SCROLLING
The simplest conceivable content slider is a region con-
taining unwrapped content laid out on a horizontal axis,
traversable by scrolling the region horizontally. The declara-
tion overflow-x: scroll does the heavy lifting.
.slider {
overflow-x: scroll;
.slider li {
display: inline-block;
white-space: nowrap;
}
KEYBOARD SUPPORT
For mouse users on most platforms, hovering their cursor over
the slider is enough to enable scrolling of the hovered ele-
ment. For touch users, simply swiping left and right does the
trick. This is the kind of effortless multimodality that makes
the web great.
For those using the keyboard, only when the slider is focused
can it be interacted with.
74 https://smashed.by/idaffordances
A Content Slider 201
75 https://smashed.by/focusorder
76 https://smashed.by/xenophobe
202 Chapter 8
[aria-label="gallery"]:focus {
outline: 4px solid skyBlue;
}
77 https://smashed.by/contentsliderdemo
A Content Slider 203
Affordance
There are already a couple of things that tell the user this is
a slidable region: the focus style; and the fact that the right-
most image is usually cut off, suggesting there is more to see.
[aria-label="gallery"]::-webkit-scrollbar {
height: 0.75rem;
}
[aria-label="gallery"]::-webkit-scrollbar-track {
background-color: #eee;
}
204 Chapter 8
[aria-label="gallery"]::-webkit-scrollbar-thumb {
background-color: #000;
}
css [aria-label="gallery"] {
/* Space separated: the thumb color followed by the
track color */
scrollbar-color: #000 #eee;
/* Keywords: none, thin, or auto */
scrollbar-width: thin; }
INSTRUCTIONS
We can take things one step further and literally spell out
how the gallery content slider can be used. Inclusive design
mantra: If in doubt, spell it out.
#hover {
display: none;
}
The path of a screen reader user in browse mode is much the same as
a keyboard user’s path given linked/interactive slides. In either case,
the browser/reader will slide the container to bring the focused items
into view.
[aria-label="gallery"]:hover:focus + .instructions
#hover-and-focus {
display: block;
}
208 Chapter 8
[aria-label="gallery"]:hover:focus + .instructions
#hover-and-focus ~ * {
display: none;
}
78 https://smashed.by/contentsliderinstructions
A Content Slider 209
window.addEventListener('touchstart', function
touched() {
document.body.classList.add('touch');
window.removeEventListener('touchstart', touched,
false);
}, false)
[aria-label="gallery"]:hover + #instructions,
[aria-label="gallery"]:focus + #instructions,
.touch #instructions {
display: block;
}
210 Chapter 8
Slides
Depending on your use case and content, you could just stop
and call the slider good here, satisfied that we have some-
thing interoperable and multimodal that only uses about 100
bytes of JavaScript. That’s the advantage of choosing to make
something simple, from scratch, rather than depending on a
one-size-fits-all library.
<li>
<figure>
<img src="[url]" alt="[description]">
<figcaption>[Title of artwork]</figcaption>
</figure>
</li>
[aria-label="gallery"] ul {
display: flex;
}
A Content Slider 211
[aria-label="gallery"] li {
list-style: none;
flex: 0 0 100%;
}
[aria-label="gallery"] figure {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 50vh;
}
[aria-label="gallery"] figcaption {
height: 2rem;
line-height: 2rem;
}
[aria-label="gallery"] img {
display: block;
margin: 2rem auto 0;
max-width: 100%;
79 https://smashed.by/contentslidercaptioned
80 https://smashed.by/shouldiuseacarousel
81 https://smashed.by/carouselstats
214 Chapter 8
const observerSettings = {
root: document.querySelector('[aria-
label="gallery"]')
}
if ('IntersectionObserver' in window) {
Array.prototype.forEach.call(slides, function
(slide) {
let img = slide.querySelector('figure > img');
});
• For each slide that intersects, we set its src from the
dummy data-src attribute in typical lazy-
loading fashion.
src='data:image/svg+xml;utf8,<svg xmlns="http://www.
w3.org/2000/svg" viewBox="0 0 6 2"
stroke="currentColor" stroke-dasharray="1,0.5"><path
d="M1,1 5,1" /></svg>'
(To see the effect, try throttling the network in Chrome’s devel-
oper tools by setting to Mid-tier or Low-end mobile).
82 https://smashed.by/contentsliderlazy
A Content Slider 217
NO JAVASCRIPT
Currently, users with no JavaScript running are bereft
of images because the data-src / src switching can-
not occur. The simplest solution seems to be to provide
<noscript> tags containing the images with their true src
values already in place.
<noscript>
<img src="[url]" alt="[description]">
</noscript>
.no-js .dots {
display: none;
}
83 https://smashed.by/mutationobserver
84 https://smashed.by/lazydemo
A Content Slider 219
slides.forEach(entry => {
entry.target.classList.remove('visible')
if (!entry.isIntersecting) {
return;
}
let img = entry.target.querySelector('img');
if (img.dataset.src) {
img.setAttribute('src', img.dataset.src);
img.removeAttribute('data-src');
}
entry.target.classList.add('visible');
})
To move the correct slide fully into view when the user
presses one of the buttons, we need to know just three things:
220 Chapter 8
If two slides are partially visible and the user presses next, we
identify the requested slide as the second of the .visible
node list. We then change the container’s scrollLeft value
based on the following formula:
Note the size of the previous and next buttons in the following
demo — optimized for easy touch interaction without hinder-
ing the desktop experience.
gallery.parentNode.insertBefore(controls, gallery);
222 Chapter 8
SCROLLING ENHANCEMENTS
A couple of final enhancements to improve the scrolling
experience: the first is to add scroll-behavior: smooth to
the scrollable element. Although not supported everywhere,
this is a highly efficient way to animate the button-
activated scrolling. Without it, artworks seem to just appear
and disappear, meaning users may not be aware there is a
linear continuum.
[aria-label="gallery"] {
-webkit-overflow-scrolling: touch;
-webkit-scroll-snap-type: mandatory;
-ms-scroll-snap-type: mandatory;
scroll-snap-type: mandatory;
-webkit-scroll-snap-points-x: repeat(100%);
-ms-scroll-snap-points-x: repeat(100%);
scroll-snap-points-x: repeat(100%);
}
A Content Slider 223
DISABLING BUTTONS
If the scroll position of the gallery element is right at the start
or all the way to the end, the previous or next button isn’t
going to do anything. You may want to consider disabling the
redundant button under these circumstances. But there are a
few things to consider:
85 https://smashed.by/contentsliderbuttons
224 Chapter 8
the button isn’t there anymore. Screen reader users can still
reach it by moving their virtual cursor to the element, but they
wouldn’t know to do this in the context, since Tab worked
perfectly well before.
var debounced;
gallery.addEventListener('scroll', function () {
window.clearTimeout(debounced);
debounced = setTimeout(disable, 200);
});
86 https://smashed.by/jsdebounce
87 https://smashed.by/lodash
A Content Slider 225
function disable() {
prev.disabled = gallery.scrollLeft < 1;
next.disabled = gallery.scrollLeft === list.
scrollWidth - list.offsetWidth;
}
prev.disabled = true;
88 https://smashed.by/buttonsdisabled
A Content Slider 227
</button>
<!-- disabled -->
<button
class="previous"
aria-label="previous artwork"
aria-disabled="true"
tabindex="-1">
</button>
But what if the content of each slide were linked? After you
focused the button controls, the first slide would take focus
no matter whether it is currently visible or not. That is, if
the user has scrolled the region to view the third item, they
would expect that item to be the one that receives focus next.
Instead, the first item takes focus and the slider is slung back
to the start, bringing that first item into view.
228 Chapter 8
The complete script for this content slider is less than 2KB
minified. The first result when searching for “carousel plugin”
using Google is 41.9KB minified and uses incorrect WAI-ARIA
attribution, in some cases hiding focusable content from
screen reader software using aria-hidden.
Conclusion
Inclusive design is not about giving everyone the same expe-
rience. It’s about giving as many people as possible a decent
experience. Our slider isn’t the fanciest implementation out
there, but that’s just as well: it’s the content that should be
wowing people, not the interface. I hope you enjoyed my gen-
erative artworks used in the demonstration. You can generate
your own at Mutable Gallery.91
89 https://smashed.by/4thrule
90 https://smashed.by/contentsliderlinked
91 https://smashed.by/mutablegallery
A Content Slider 231
92 https://smashed.by/lesscodevideo
232 Chapter 8
CHECKLIST
• If you are a man and got past the first paragraph without
being personally offended: Congratulations! You do not
see men and women as competing teams.
Notifications 233
Notifications
T
he key difference between a website and a web app
is… highly contested. On the whole, I’d say the more
links there are, the more site-like; and the more but-
tons, the more app-like. If it includes a page with a form, it’s
probably a kind of site. If it essentially is a form, you might call
it an app. In any case, your web product is really just inter-
active content, consumed and transmitted by an app we call
a browser.
One thing that certainly makes a web page feel more like a
desktop app is statefulness. Web pages that undergo changes
as you are operating them are something quite unlike web
pages that just load and unload as you click hyperlinks.
Drawing attention
One of the biggest challenges in creating usable interfaces is
knowing when to draw attention to something. Oversharing
may be considered a nuisance, but undersharing might make
the user feel they are missing critical information. This makes
some hesitant, even when there is really nothing they “need to
know” at the time.
93 https://smashed.by/comparableexperience
Notifications 237
94 https://smashed.by/liveregion
238 Chapter 9
Since the script creates hidden ARIA live regions and pop-
ulates them on the fly, it makes communicating to screen
readers procedurally trivial. However, in most cases — and
in the case of status messages especially — we want to be
communicating to users. Not users running screen readers
or users not running screen readers; just users. Live regions
make it easy to communicate through visual and aural
channels simultaneously.
A chat application
In a chat application (something like Slack, say, where most
everything happens in real time) there are a number of oppor-
tunities for status messages. For example:
95 https://smashed.by/visibilityapi
240 Chapter 9
document.addEventListener('visibilitychange', () => {
let setting = document.hidden ? ['none', 'off'] :
['status', 'polite'];
notification.setAttribute('role', setting[0]);
notification.setAttribute('aria-live', setting[1]);
});
However, you can’t rely on all your users having these setups
and — where they don’t — the experience is very off-putting.
Conversations
Even when inside the open tab for the chat application, you
won’t want to be inundated by a flurry of any and all noti-
fications. Visually, it could get irritating; aurally it almost
certainly will.
Notifications 241
On the other hand, if the user is focused on the text input for
a thread and a new message pops in, they’re probably going
to want to know about it. In this case, the message would
just appear if you’re a sighted user. For a blind screen reader
user, you make the new message its own notification with a
live region.
When the new mesage, in gray, appears, only its contents — and
not the contents of the other messages — are announced in
screen readers.
242 Chapter 9
if (message.includesMention()) {
message.alert = true;
}
FLASH MESSAGES
Flash messages — little colored strips of text that appear
above the action of the page — are often employed to keep
users abreast of changing state. A single ARIA live region will
suffice for these non-actionable notifications.
96 https://smashed.by/givecontrol
Headings inside legends
97 https://smashed.by/legendelement
246 Chapter 9
Should you wish to supplant the text with icons you’ll have to
be careful they are visually comprehensible, include alterna-
tive text for screen reader users, and are still visible where
Windows High Contrast Mode is running.
98 https://smashed.by/vishiddencontent
248 Chapter 9
DISMISSING NOTIFICATIONS
Working as a design consultant, I often see notification mes-
sages include little “x” buttons to dismiss them.
Notifier.prototype.notify = function(message) {
let note = document.createElement('p');
note.innerHTML = `
<svg viewBox="0 0 20 20" focusable="false">
<use xlink:href="#${this.type}"></use>
</svg>
<span class="visually-hidden">${this.type}:</span>
${message}
`;
this.regionEl.appendChild(note);
// Initialize
const infoNotifications = new Notifier(
document.getElementById('notifications'),
5000
);
(Note: The “type” string is used both for the inline SVG refer-
ence and as the alternative text for the icon.)
250 Chapter 9
But what if the user misses that notifications come and go?
Not a problem. Notifications should only refer to things that
are discoverable elsewhere in the updated interface.
Conclusion
Thanks to the marvelous “You add it, I say it” nature of ARIA
live regions, the technical implementation of inclusive notifi-
cation could hardly be simpler. That leaves you to perfect the
clarity of form and language.
Notifications 251
CHECKLIST
99 https://smashed.by/liveregionsupdated
252 Chapter 9
100 https://smashed.by/notification
Data Tables 253
Data Tables
T
he first thing I was told when I embarked on learning
web standards about twelve years ago was, “Don’t
use tables for layout.” This was sound advice in spirit,
but not very well qualified. As a result, there have been some
unfortunate interpretations. Using table markup inevitably
results in a visual layout, which has led some to abandon
HTML tables altogether. Tables: bad.
The lesson in “Don’t use tables for layout” is not to use HTML
elements in ways for which they were not intended. Twelve
years ago, the idea that I would be coding HTML wrong was
enough to put me off making such classic blunders. Vanity is
not a real reason, though.
<span>Press me</span>
Data Tables 255
Most of the time you’ll only want to add semantics where they
are useful, rather than choosing elements for their appear-
ance and removing the semantics where they aren’t needed.
But sometimes reverse engineering accessibility information
is the most efficient way to make good of a bad decision like a
layout table.
<table>
<tr>
<td><img src="some/image" alt=""></td>
<td>Lorem ipsum dolor sit amet.</td>
</tr>
<tr>
<td><img src="some/other/image" alt=""></td>
<td>Integer vitae blandit nisi.</td>
</tr>
</table>
The semantics issue to one side, these are all the elements
you really need to achieve a visual layout. You have your rows
and columns, like a grid.
<table>
<tr>
<td>Column header 1</td>
<td>Column header 2</td>
Data Tables 257
</tr>
<tr>
<td>Row one, first cell</td>
<td>Row one, second cell</td>
</tr>
</table>
<table>
<tr>
<th>Column header 1</th>
<th>Column header 2</th>
</tr>
<tr>
<td>Row one, first cell</td>
<td>Row one, second cell</td>
</tr>
</table>
ROW HEADERS
It’s possible to have both column and row headers in data
tables. I can’t think of any kind of data for which row headers
are strictly necessary for comprehension, but sometimes it
feels like the key value for a table row should be on the left,
and highlighted as such.
101 https://smashed.by/bulb
Data Tables 259
<table>
<tbody>
<tr>
<th scope="col">Region</th>
<th scope="col">Electricity</th>
<th scope="col">Gas</th>
</tr>
<tr>
<th scope="row">East England</th>
<td>10.40</td>
<td>2.31</td>
</tr>
<tr>
<th scope="row">East Midlands</th>
<td>2.77</td>
</tr>
<tr>
<th scope="row">London</th>
<td>10.10</td>
<td>2.48</td>
</tr>
</tbody>
</table>
Note that not setting row headers does not make a nonsense
of the data; it just adds extra clarity and context. For a table
that uses both column and row headers, some screen readers
will announce both the column and row labels for each of the
data cells.
Using tables with screen readers
102 https://smashed.by/datatablevoiceover
Data Tables 261
Captions
There used to be two ways to provide descriptive information
directly to tables: <caption> and <summary>. The <summary>
element was deprecated in HTML5, so should be avoided. The
<caption> element is superior regardless, because it provides
a visual and screen reader accessible label. The <summary>
element works more like an alt attribute and is not visible.
Since the table itself provides textual information, such a
summary should not be necessary.
103 https://smashed.by/captionheadings
262 Chapter 10
By using a heading inside the table <caption>, there are now three
ways to discover the table: by table shortcut, heading shortcut, or just
by browsing downwards.
const rows = [
['Napalm Death', 'Barney Greenway', '1981',
'Century Media'],
['Carcass', 'Jeff Walker', '1985', 'Earache'],
['Extreme Noise Terror', 'Dean Jones', '1985',
'Candlelight'],
['Discordance Axis', 'Jon Chang', '1992',
'Hydrahead']
];
Now the Table component just needs those consts passed in.
One of the best and worst things about HTML is that it’s
forgiving. You can write badly formed, inaccessible HTML
and the browser will still render it without error. This makes
the web platform inclusive of beginners, and those creating
rule-breaking experiments. But it doesn’t hold us to account
when we’re trying to create well-formed code that’s compati-
ble with all parsers, including assistive technologies.
Here’s how the basic component that handles this might look:
)}
</table>
);
}
}
If you don’t supply arrays for the headers and rows props
things are going to go spectacularly wrong, so if you dig “not a
function” errors, you’re in for a fun time.
Table.propTypes = {
headers: PropTypes.array.required,
rows: PropTypes.array.required
};
<tr key={i}>
{row.map((cell, i) =>
(this.props.rowHeaders && i < 1) ? (
<th scope="row" key={i}>{cell}</th>
) : (
<td key={i}>{cell}</td>
)
)}
</tr>
104 https://smashed.by/rowheaders
266 Chapter 10
Here’s the full script for the basic table component, coming in
at just 25 lines. Use it however you wish.
)}
</tr>
{this.props.rows.map((row, i) =>
<tr key={i}>
{row.map((cell, i) =>
(this.props.rowHeaders && i < 1) ? (
<th scope="row" key={i}>{cell}</th>
) : (
<td key={i}>{cell}</td>
)
)}
</tr>
)}
</table>
);
}
}
Data Tables 267
// Data
const rows = [
['Napalm Death', 'Barney Greenway', '1981',
'Century Media'],
['Carcass', 'Jeff Walker', '1985', 'Earache'],
// Initialization
<Table rows={rows} headers={headers} rowHeaders
caption="Grindcore bands" />
Going responsive
Responsive tables are one of those areas where the accessible
solution is more about what you don’t do than what you do. As
Adrian Roselli recently noted,105 using CSS display properties
to change table layout has a tendency to remove the underly-
ing table semantics. This probably shouldn’t happen, because
it messes with the separation of concerns principle.106 But it
happens anyway.
105 https://smashed.by/tabledisplay
106 https://smashed.by/soc
268 Chapter 10
This isn’t the only reason it’s a bad idea to change the way
tables are displayed. Visually speaking, it’s not really the same
table — or much of a table at all — if the columns and rows
collapse on top of one another. Instead, we want to provide
access to the same visual and semantic structure regardless of
the space available.
.table-container {
overflow-x: auto;
}
KEYBOARD SUPPORT
OK, it’s not quite that simple. As you may recall from chapter
8, “A Content Slider”, we need we need to make the scrollable
element focusable so it can be operated by keyboard. That’s
just a case of adding tabindex="0". But since screen reader
users will be able to focus it too, we need to provide some
context for them.
In this case, I’ll use the table’s <caption> to label the scroll-
able region using aria-labelledby.
Data Tables 269
107 https://smashed.by/rolesgroup
108 https://smashed.by/focusorder
270 Chapter 10
useEffect(() => {
const { scrollWidth, clientWidth } = container.
current;
let scrollable = scrollWidth > clientWidth;
setTabIndex(scrollable ? '0' : null);
}, []);
PERCEIVED AFFORDANCE
It’s not enough that
users can scroll the
table. They also need
to know they can
scroll the table. Fortu-
nately, given our table
cell border style, it
should be obvious
when the table is cut
off, indicating that
some content is out
of view.
Data Tables 271
We can do one better, just to be safe, and hook into the state
to display a message in the caption:
• <caption> → <h2>
• <td> → <dd>
<div className="lists-container">
<h2>{caption}</h2>
{sortedRows.map((row, i) => (
<div key={i}>
<h3>{row[0]}</h3>
<dl>
{headers.map(
(header, i) =>
i > 0 && (
<Fragment key={i}>
<dt>{header}</dt>
<dd>{row[i]}</dd>
</Fragment>
)
)}
</dl>
</div>
))}
</div>
109 https://smashed.by/htmlissues
110 https://smashed.by/gunnar
274 Chapter 10
.lists-container {
display: none;
}
}
Sortable tables
Let’s give users some control111 over how the content is sorted.
After all, we already have the data in a sortable format — a
two-dimensional array.
111 https://smashed.by/givecontrol
276 Chapter 10
ICONOGRAPHY
Visually, the sort order should be fairly clear by glancing down
the column in hand, but we can go one better by providing
icons that communicate one of three states:
Data Tables 277
You do not need to use the grid role to make most tables
accessible to screen readers. The grid-related behavior
should only be implemented where users not running screen
reader software need to easily access each cell to interact
with it. One example might be a date picker where each date
is clickable within a grid representation of a calendar month.
PERFORMANCE
The sorting function itself should look something like this,
and uses the sort method. Note that the Edge browser does
not support returning Booleans for sort methods, hence the
explicit 1, -1, or 0 return values.
Note the use of slice(0). If this were not present, the sort
method would augment the original data directly (which
280 Chapter 10
Conclusion
Yes, it’s still OK to use tables.114 Just don’t use them if you don’t
need them and, when you do need them, structure them in a
logical and expected way.
112 https://smashed.by/reacttabledemo
113 https://smashed.by/reacttablegit
114 https://smashed.by/oktables
Data Tables 281
CHECKLIST
• Don’t use tables just for layout or, to be more clear, don’t
use tables for anything but tabular data.
Modal Dialogs
O
ne component I get asked to write about a lot is
the modal dialog. I have mixed feelings about this,
because the proliferation of dialogs in web user
interface design is something of a scourge. In my talk “Writing
Less Damned Code”115 I joke that the fewer dialogs you use in
your project, the more there are available for Twitter’s web UI
— an interface almost entirely made of dialogs.
115 https://smashed.by/heydontalk
116 https://smashed.by/appsforall
Modal Dialogs 283
117 https://smashed.by/modaldialog
284 Chapter 11
118 https://make8bitart.com/
286 Chapter 11
remove(index, name) {
if (window.confirm(`Are you sure you want to delete
"${name}"?`)) {
this.todos.splice(index, 1);
this.feedback = `${name} deleted`;
document.getElementById('todos-label').focus();
}
}
119 https://smashed.by/todolistconfirm
288 Chapter 11
Modals or screens?
One of the nice things in the last confirm() example is how
I can write imperatively. By using just a simple if clause, I
invoke a complete — and quite accessible! — intermediary UI.
<aside aria-labelledby="your-progress">
<h2 id="your-progress" class="visually-hidden">Your
progress</h2>
<ol>
<li class="current">
<span>Quote</span>
<span class="visually-hidden">(you are here)</
span>
</li>
<li><span class="text">My Information</span></li>
<li><span class="text">Payment Details</span></li>
</ol>
</aside>
120 https://smashed.by/bulb
121 https://smashed.by/styledcomponents
Modal Dialogs 291
CUSTOM MODALS
Well, it’s happened: a requirement for the design system to
include a custom modal dialog has arrived (that was quick).
So let’s set about making one as efficiently and accessibly as
possible. The implementation to follow will closely resemble
the behavior (and brevity) of a confirm(), but using my own
HTML, CSS, and JavaScript.
292 Chapter 11
THE MARKUP
The markup for a straightforward dialog is quite simple.
The container needs role="dialog" and the buttons have
to be — you guessed it — <button> elements. The text
acts as a label for the container by connecting it up with
aria-labelledby. I’m creating the dialog on the fly with
JavaScript, because we don’t need it until we need it.
MODALITY
This next part can be tricky, depending on your approach.
We need to disable the rest of the page when the modal is
active. But that doesn’t just mean fading the page out (or
applying a similar effect). It needs to be unidentifiable to
assistive technologies, and none of the interactive contents
can be clickable.
This too is a bit of a cludge. You need to listen for Tab and
Shift + Tab key presses and programmatically reroute
focus between the buttons. It’s also a substandard approach,
because it makes reaching browser chrome like the address
bar impossible by Tab . This is not the case when using a
native confirm().
122 https://smashed.by/inertattribute
123 https://smashed.by/inertgit
296 Chapter 11
FOCUS
The demo implementation124 lets you provide a callback
function as the second argument. This function fires after the
close() function if OK has been pressed. As with confirm(),
Esc also closes the dialog.
okay.onclick = () => {
close();
func();
}
cancel.onclick = () => close();
dialog.addEventListener('keydown', e => {
if (e.keyCode == 27) {
e.preventDefault();
close();
}
});
remove(index, name) {
this.dialog(`Are you sure you want to delete
"${name}"?`,
() => {
this.todos.splice(index, 1);
document.getElementById('todos-label').focus();
this.feedback = `${name} deleted`;
}
);
}
124 https://smashed.by/todolistconfirmcustom
298 Chapter 11
THE CSS
While the confirm() emerges from the top of the viewport,
our custom dialog can appear wherever we like. Good ol’ CSS
Tricks125 offers a clever solution using transform to center
the dialog regardless of its size.
[role="dialog"] {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
[role="dialog"] .message {
max-width: 50ch;
max-height: 50vh;
overflow-y: auto;
}
125 https://smashed.by/stylingmodal
Modal Dialogs 299
CONCLUSION
One of the first painful lessons I learned from usability testing
was that giving people lots of freedom to explore and dis-
cover interfaces is not actually very popular. People don’t
want to be misled or forced to do things they don’t want to,
but they do like you (and your interface) to be clear and asser-
tive about what’s needed to get the task in hand done.
CHECKLIST
Cards
S
ome of the components I’ve explored here have spe-
cific standardized requirements in order to work as
expected. Tab interfaces, for example, have a pre-
scribed structure and a set of interaction behaviors as man-
dated by the WAI-ARIA specification.
In this case, let’s say the card is a teaser for a blog post. Note
the heading: like the list markup, headings provide navigation
cues to screen reader users. Each card has a heading of the
same level — <h2> in this case — because they belong to a
flat list hierarchy. Also note that the image is treated as deco-
Cards 303
<li>
<img src="/path/to/image.png" alt="">
<h2>Card design woes</h2>
<p>Ten common pitfalls to avoid when designing card
components.</p>
<small>By Heydon Pickering</small>
</li>
The question is: where do I place the link to that blog post?
Which part of the card is interactive? One reasonable answer
is “the whole card.” And by wrapping the card contents in an a
tag, it’s quite possible.
<li>
<a href="/card-design-woes">
<img src="/path/to/image.png" alt="">
<h2>Card design woes</h2>
<p>Ten common pitfalls to avoid when designing
card components.</p>
<small>By Heydon Pickering</small>
</a>
</li>
This is not without its problems. Now, all of the card contents
form the label of the link. So when a screen reader encounters
304 Chapter 12
<li>
<a href="/card-design-woes">
<img src="/path/to/image.png" alt="">
<h2>Card design woes</h2>
<p>Ten common pitfalls to avoid when designing
card components.</p>
<small>By <a href="/author/heydon">Heydon
Pickering</a></small>
</a>
</li>
126 https://smashed.by/blocklevel
Cards 305
<li>
<img src="/path/to/image.png" alt="">
<h2>
<a href="/card-design-woes">Card design woes</a>
</h2>
<p>Ten common pitfalls to avoid when designing card
components.</p>
<small>By Heydon Pickering</small>
</li>
This stretches the link’s layout over the whole card, making it
clickable like a button.
127 https://smashed.by/pseudocontent
308 Chapter 12
card.addEventListener('click', e => {
if (link !== e.target) {
link.click();
}
});
Now selecting the text is possible, but the click event is still
fired and the link followed. We need to detect how long the
user is taking between mousedown and mouseup and suppress
the event if it’s “likely-to-be-selecting-text”territory.
It’s not highly probable the user would choose to select text
from a card/teaser when they have access to the full content
to which the card/teaser is pointing. But it may be discon-
certing to them to find they cannot select the text. If it’s not
seemingly important, I recommend you use the pseudo-
content trick, because this approach means the link’s context
menu appears wherever the user right-clicks on the card:
a nice feature.
Affordance
If the whole card is interactive, the user should know about it.
We need to support perceived affordance.
card.style.cursor = 'pointer';
128 https://smashed.by/redundantclick
310 Chapter 12
.card:hover {
box-shadow: 0 0 0 0.25rem;
}
Where there are hover styles there should also be focus styles,
which presents us with an interesting problem. Using :focus,
we can only apply a style to the link itself. This isn’t a big
issue, but it would be nice if sighted keyboard users saw that
nice big, card-sized style that mouse users see. Fortunately,
this is possible using :focus-within:
Cards 311
.card a:focus {
text-decoration: underline;
}
.card:focus-within {
box-shadow: 0 0 0 0.25rem;
outline: 2px solid transparent;
}
.card:focus-within a:focus {
text-decoration: none;
}
Content tolerance
One unsung aspect of inclusive design is the art of making
interfaces tolerant of different levels of content. Wherever an
interface breaks when too much or too little content is pro-
vided, we are restricting what contributors can say.
<li class="card">
<div class="img">
<img src="/path/to/image.png" alt="">
</div>
<div class="text">
<h2>
<a href="/card-design-woes">Card design woes</a>
</h2>
<p>Ten common pitfalls to avoid when designing
card components.</p>
<small>By Heydon Pickering</small>
</div>
</li>
Cards 313
Then I make both the .card container and the .text wrapper
inside it Flexbox contexts, using flex-direction: column.
.card .text {
flex-grow: 1;
}
129 https://smashed.by/lobotomizedowls
Cards 315
Grid and Flexbox can both have this effect, but I prefer Grid’s
wrapping algorithm and grid-gap is the easiest way to dis-
tribute cards without having to use negative margin hacks.
.card .text {
max-width: 60ch;
}
.card + .card {
margin-top: 1.5rem;
}
@supports (display: grid) {
.cards > ul {
display: grid;
grid-template-columns: repeat(auto-fill,
minmax(15rem, 1fr));
316 Chapter 12
grid-gap: 1.5rem;
}
.card + .card {
margin-top: 0;
}
}
130 https://smashed.by/caniusecssgrid
Cards 317
.card .img {
height: 5rem;
}
.card .img img {
object-fit: cover;
height: 100%;
width: 100%;
}
The slight slant given to the image box is achieved using clip-path.
This is a progressive enhancement too. No content is obscured where
clip-path is not supported.
318 Chapter 12
For argument’s sake, let’s say there is a use case for linking
the author within the card. This is viable alongside both the
pseudo-content and JavaScript techniques described above. A
declaration of position:relative will raise the link above
the pseudo-content in the first example. Contrary to popu-
lar belief, just the positioning is needed, and no z-index,
because the author link is after the primary link in the source.
.card small a {
position: relative;
}
Cards 319
So, why don’t we increase the hit area of the author link to
mitigate this? We can use padding. The left padding remains
unaffected because this would push the link away from the
preceding text.
.card small a {
position: relative;
padding: 0.5rem 0.5rem 0.5rem 0;
}
131 https://smashed.by/cardsauthorlink
320 Chapter 12
Calls to action
As I said already, multiple “read more” links are useless when
taken out of context and aggregated into a glossary. Best
to avoid that. However, it may prove instructive to have an
explicit call to action. Without it, users may not be aware
cards are interactive. Being obvious is usually the best
approach in interface design.
<li>
<img src="/path/to/image.png" alt="">
<h2>
<a href="/card-design-woes">Card design woes</a>
</h2>
<p>Ten common pitfalls to avoid when designing card
components.</p>
<span class="cta" aria-hidden="true">read more </
span>
<small>By Heydon Pickering</small>
</li>
322 Chapter 12
I look for focus within the <h2>, and use the general sib-
ling combinator132 to delegate the focus style to the
call-to-action button.
.card h2 a:focus {
text-decoration: underline;
}
.card h2:focus-within ~ .cta {
box-shadow: 0 0 0 0.125rem;
outline: 2px solid transparent;
}
.card:focus-within h2 a:focus {
text-decoration: none;
}
132 https://smashed.by/siblingselectors
Cards 323
<li>
<img src="/path/to/image.png" alt="">
<h2>
<a href="/card-design-woes" aria-describedby="desc-
card-design-woes">Card design woes</a>
</h2>
<p>Ten common pitfalls to avoid when designing card
components.</p>
<span class="cta" aria-hidden="true" id="desc-card-
design-woes">read more</span>
<small>By Heydon Pickering</small>
</li>
324 Chapter 12
Note that, since the call to action says “read more” in each
case, only one of the call-to-action elements needs to be
referenced by each of the cards’ links. Within a templating
loop, this is likely to be hard to implement, though.
UNIQUE STRINGS
When creating dynamic content by iterating over data, there
are certain things we can’t do. One of these is to manually
create id values.
133 https://smashed.by/clickreadmore
Cards 325
134 https://smashed.by/idjs
326 Chapter 12
Alternative text
So far, I’ve been working with the assumption that the card’s
image is decorative and doesn’t need alternative text, hence
alt="". If this empty alt was omitted, screen readers would
identify the image and read (part of) the src attribute as a
fallback, which is not what anyone wants here.
135 https://smashed.by/andykirk
Cards 327
<li class="card">
<div class="text">
<h2>
<a href="/card-design-woes">A great product</a>
</h2>
<p>Description of the great product</p>
<small>By Great Products(TM)</small>
</div>
<div class="img">
<img src="/path/to/image.png" alt="Description of
the great product’s appearance">
</div>
</li>
.card .text {
order: 1;
}
136 https://smashed.by/focusorder
Cards 329
Conclusion
Some of the ideas and techniques explored here may not be
applicable to your particular card designs; others will. I’m not
here to tell you exactly how to design a card because I don’t
know your requirements. But I hope I’ve given you some ideas
about how to solve problems you might encounter, and how
to enhance the interface in ways that are sensitive to a broad
range of users.
137 https://smashed.by/cardsalttext
330 Chapter 12
Checklist
• Design Systems
by Alla Kholmatova
• Digital Adaptation
by Paul Boag