Glossary
-
accessibility
/ ækˌsɛs əˈbɪl ə ti / - noun: a physical, mental, cognitive, or other condition that interferes with, or limits a person's ability to engage in certain tasks or participate in typical daily activities and interactions.
-
disability
/ ˌdɪs əˈbɪl ɪ ti / - noun: the design of products, devices, services, or environments so as to be usable by people with disabilities.
-
Inclusive Design
/ ɪnˈklu sɪv / / dɪˈzaɪn / - compound noun: a design process in which a mainstream product, service or environment is designed to be usable by as many people as reasonably possible, without the need for specialized adaptions. Design for one, extend to many.
-
Universal Design
/ ˌyu nəˈvɜr səl / / dɪˈzaɪn / - compound noun: Universal Design is the design and composition of an environment so that it can be accessed, understood and used to the greatest extent possible by all people regardless of their age, size, ability or disability
-
Disability Inclusion
/ ˌdɪs əˈbɪl ɪ ti / / ɪnˈklu ʒən / - compound noun: creating equitable experiences for people with disabilities
Behind the Scenes of your new and inclusive five-star rating component
Web Developers alike have created many different five–star rating components. Yours truly is absolutely guilty as charged, these patterns are unfortunately often woven in an inaccessible way. One of my favorite patterns from the HTTP/1 days when sprites were common to avoid network requests involved a very clever CSS pattern with psuedo elements that drew the stars and appropriately filled them in. But these patterns often weren’t accessible and as clever and worthy of celebration they may be from a CSS–capabilities perspective, we need to ensure that critical features of our experience meet accessibility compliance.
So how do we create this familiar component in an accessible way? As always, we want to follow progressive enhancement guidelines and start with and fully leverage HTML and web standards. So what do we have at our disposal? Let’s open a web browser and navigate to any PDP page. Search for a shoe and navigate to a Product Display Page. Note on the right hand side the grid of size options. We call this component “Airplane Seating” and it is a very clever pattern worthy of repeatedly reminding ourselves of. Open up the Developer Tools and inspect one of the size options. Note that while the design is custom, these aren’t just div elements being used under the hood. The pattern leverage the CSS 3 Attribute Selector and a very clever hack of accessibly hiding the input[type=radio] elements by making them visually hidden. Note that the label elements for each corresponding input[type=radio] element are where our custom designs are applied. And get this, when a user “clicks” one of those labels the browser is clever enough to act as though the hidden input itself where interacted with. It just works, even without JavaScript. Brilliant.
So input[type=radio] + label.my-custom-styles is the pattern. Remember our label element is block level and can more or less contain whatever we want. A span, and/or an svg graphic. Let’s take a look at the RatingStars.tsx source file from @nike/web-a11y-react-aria together. Note that RatingStars merely composes RadioGroup (which composes InputGroup). Let’s examine the Radio component, which is exported with an alias of RatingStar, more closely:
import React from "react";
import { useRadio } from "@react-aria/radio";
import { useId } from "@react-aria/utils";
import { VisuallyHidden, useVisuallyHidden } from "@react-aria/visually-hidden";
import { useComponentSheets } from "@nike/web-a11y-kit";
import { Star as StarDefault } from "@nike/svg-icons";
import { RadioGroupContext as RadioContext } from "./../InputGroup";
import { RadioGroup } from "./../RadioGroup";
import { LIB_VERSION } from "./../../constants";
import { getSheetURL } from "./../../utils";
const RATING_STARS = "RatingStars";
export const Radio = ({
showTitle = true,
Star = StarDefault,
hideStarGraphic = true,
...props
}) => {
let { children } = props;
let id = useId(props.id);
let state = React.useContext(RadioContext);
let ref = React.useRef(null);
let { inputProps } = useRadio(props as any, state as any, ref) as any;
const { visuallyHiddenProps } = useVisuallyHidden();
const title = showTitle
? typeof children === "string"
? children
: props["aria-label"]
: undefined;
return (
<React.Fragment>
<input id={id} {...inputProps} ref={ref} {...visuallyHiddenProps} />
<label htmlFor={id} title={title}>
<Star aria-hidden={hideStarGraphic} />
<VisuallyHidden>{children}</VisuallyHidden>
</label>
</React.Fragment>
);
};
So there’s a fair amount of stuff going on with Hooks. Don’t worry about that right now, just trust it gets us the right props to spread on our input and label elements. What’s happening is we have a visually hidden input, followed by a label, which contains a SVG graphic. Also note that we are removing the SVG graphic from the accessibility tree by default via a hideStarGraphic prop. This is safe to do because the visually hidden text will convey the star rating to the accessibility tree.
In “user–land” folks leverage Radio, aka RatingStar, like so:
export const HTMLSignature = () => (
<RatingStars name="rate-stuff" id="rate-stars" label="Rate stuff" onSelect={(value, state) => {
console.log(value, state);
}}>
<RatingStar value="1" id="r_1">1. Unsatisfactory.</RatingStar>
<RatingStar value="2">2. Poor.</RatingStar>
<RatingStar value="3">3. Fair.</RatingStar>
<RatingStar value="4">4. Good.</RatingStar>
<RatingStar value="5">5. Excellent.</RatingStar>
</RatingStars>
);
or if the HTML–signature isn’t your thing, like this:
export const PropDrivenSignature = () => (
<RatingStars name="rate-stuff" id="rate-stars" label={<VisuallyHidden>Rate Stuff!</VisuallyHidden>} data={[{
value: '1',
id: 'r_1',
children: '1. Unsatisfactory',
}, {
value: '2',
id: 'r_2',
children: '2. Poor',
}, {
value: '3',
id: 'r_3',
children: '3. Fair',
}, {
value: '4',
id: 'r_4',
children: '4. Good',
}, {
value: '5',
id: 'r_5',
children: <span className="foo">5. Excellent</span>,
}]} />
);
The group label, such as “Choose a rating” can also be set in a variety of ways.
Pretty clean right? One more thing to go over, the styles. Now, @nike/web-a11y does not use runtime CSS–in–JS solutions such as Emotion, but it does have a mechanism for safely styling components and scoping them as well. If you checkout the repository and run yarn build you’ll see minified and hashed component–level stylesheets generated in packages/web-a11y-react-aria/dist.
Let’s have a glance at the source code for our rating-star.css code. It’s ok if this isn’t fully digestable as Sass may be a less familiar syntax.
@use 'addons' as nr;
@mixin star($fill: none) {
fill: $fill;
}
@mixin filled-star() {
@include star(currentColor);
}
%filled-star-legacy { // used to pack selectors
@supports not selector(div:has(input[type="radio"]:checked)) {
@include filled-star;
}
}
@include nr.component("RatingStars") {
input[type="radio"]:focus-visible, input[type="radio"].focus-visible {
+ label {
outline: 2px solid var(--focus-ring-color, var(--webkit-focus-ring-color, blue));
}
}
svg path {
stroke: currentColor;
stroke-width: 2px;
}
@supports not selector(div:has(input[type="radio"]:checked)) {
svg path {
@include star;
}
input[type="radio"]:checked + label {
svg path {
@include filled-star;
}
}
@for $i from 2 through 5 {
&[data-nr-web-a11y-selected-value="#{$i}"] {
@for $j from 2 through $i {
input[type="radio"]:nth-of-type(#{$j - 1}) + label {
svg path {
@extend %filled-star-legacy;
}
}
}
}
}
} // end @supports
@supports selector(div:has(input[type="radio"]:checked)) {
svg path {
@include filled-star();
}
&:has(input[type="radio"]:checked) {
input[type="radio"] + label {
svg path {
@include filled-star(); // fill all stars
}
}
// uncheck subsequent stars
input[type="radio"]:checked + label ~ label {
svg path {
@include star; // unfill subsequent stars
}
}
}
} // end @supports
} // end RatingStars scope
Uh. Ya. There’s a lot going on there. We won’t unpack it all. The TL;DR is:
- an
@include nr.component("RatingStars")mixin is used to scope our styles - we use Sass'
@forloop to keep our code DRY - a
data-nr-web-a11y-selected-valueis set (by React) so we can key style changes off it (without CSS–in–JS)
Note the @supports block. That’ll only happen for modern browsers that support a, as of the time of this writing, bleeding edge CSS 4 :has selector. This allows us to say “apply these styles only if and when the div container has a checked star within it”. In UX terms this may also be referred to as when the component has become “dirty” meaning been interacted with. Note that fore this futuristic implementation, we actually fill all the stars, and then un–fill them as needed because it made more sense to uncheck subsequent stars. Our modern pattern does not need the funky data-nr-web-a11y-selected-value attribute meaning one day, when our supported browsers all support CSS 4 more fully, we’ll be able to delete the legacy CSS and the JavaScript code that manages that attribute.
Let’s examine the rating-stars.css file that is ultimately delivered:
@supports not selector(div:has(input[type="radio"]:checked)) {
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="5"] input[type=radio]:nth-of-type(4) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="5"] input[type=radio]:nth-of-type(3) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="5"] input[type=radio]:nth-of-type(2) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="5"] input[type=radio]:nth-of-type(1) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="4"] input[type=radio]:nth-of-type(3) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="4"] input[type=radio]:nth-of-type(2) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="4"] input[type=radio]:nth-of-type(1) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="3"] input[type=radio]:nth-of-type(2) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="3"] input[type=radio]:nth-of-type(1) + label svg path, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style][data-nr-web-a11y-selected-value="2"] input[type=radio]:nth-of-type(1) + label svg path {
fill: currentColor;
}
}
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style] input[type=radio]:focus-visible + label, [data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style] input[type=radio].focus-visible + label {
outline: 2px solid var(--focus-ring-color, var(--webkit-focus-ring-color, blue));
}
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style] svg path {
stroke: currentColor;
stroke-width: 2px;
}
@supports not selector(div:has(input[type="radio"]:checked)) {
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style] svg path {
fill: none;
}
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style] input[type=radio]:checked + label svg path {
fill: currentColor;
}
}
@supports selector(div:has(input[type="radio"]:checked)) {
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style] svg path {
fill: currentColor;
}
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style]:has(input[type=radio]:checked) input[type=radio] + label svg path {
fill: currentColor;
}
[data-nr-web-a11y-component=RatingStars][data-nr-web-a11y-style]:has(input[type=radio]:checked) input[type=radio]:checked + label ~ label svg path {
fill: none;
}
}
We were able to leverage the Sass preprocessor to keep our styles (at least fairly) legible and author a complex, powerful, and safely–scoped stylesheet. In userland, our consumers have flexibility for how the author RatingStars instances. Consumers can use various signatures, set simply string labels, or complex JSX elements such as span for A/B testing, provide their own custom Star component, and theme styles thanks to our dynamic references to color. Notice we never hard code a hex color.
Conclusion
We’ve followed progressive enhancement principles and leveraged the implicit accessibility of HTML form elements along with some clever CSS patterns to allow our rating star form to fully leverage the creative freedom that is so needed by our designers and A/B testers. Very little JavaScript is needed, and we’ve gotten completely out of the business of setting aria- attributes by leveraging React Hooks provided by react-aria. With visible focus styles, keyboard functionality, and being semantically related to the accessibility tree for assistive technology such as screen readers, our rating component truly supports everyone. And that’s exactly how it should be. If everyone’s voice isn’t being heard, how valuable are the ratings we capture to begin with? Remember that ‘critical accessibility’ isn’t just about getting people through checkout. Anyone should be able to browse with equity, and contact support, or leave feedback, with equity and inclusion. As we iterate towards an evermore inclusive .com experience let’s welcome openly the new perspectives we provide access to and thus access ourselves. This pattern is just one example of a trivial HTML component. Perhaps more importantly, let’s impress upon ourselves the steps we took to arrive at such a flexible yet accessible and performant implementation:
- fully leverage HTML that is then enhanced with clever CSS patterns for implicit accessibility before adding business logic in the JavaScript layer
- try to keep things flexible in userland
- avoid runtime performance costs like DOM hits
By following these steps you should be able to inclusively implement just about any component our designers image. Whether you run into difficulty along the way, or simply would like to bask in success, we’d love to hear about your experience and journey with accessibility over at our #accessibility-support Slack Channel. Accessibility can be fun, or extremely frustrating. It can be challenging or sometimes incredibly straightforward. Whatever you are dealing with, remind yourself, remind yours truly if needed, that is worthy of celebration because even if we’re taking steps backwards at times, we’re working towards an evermore inclusive enterprise. And that’s really something.