A good[ish] website
Web development blog, loads of UI and JavaScript topics
Juicy and versatile React component that handles icons and icon links, satisfying all the accessibility requirements on the way. 100 percent pleasure guaranteed, or get your money back.
Icons can be divided roughly into two buckets:
For example, the info icon in the below "info box" is purely decorative. The text text inside tells everything needed:
But the icons in this example table have a meaning, and there’s no text coupled up with them:
IE | Safari | Chrome | Firefox | Opera | |
---|---|---|---|---|---|
mp4 | Supported | Supported | Supported | Supported | Supported |
webm | Not supported | Not supported | Supported | Supported | Supported |
Now that we know what we need to do, let’s try to make it happen.
The example code expects the SVG icons to be a React component, like so:
const ArrowRight = props => (
<svg viewBox="0 0 16 16" {...props}>
<path d="M15.5 8L8 .5V5H0v6h8v4.5z" />
</svg>
)
Here’s a really basic example with the bare minimum amount of code (scroll down for a fully fledged example):
import { Wrap, VisuallyHidden } from './styles'
const Icon = ({ Icon, label }) => (
<Wrap>
<Icon aria-hidden focusable="false" /> {label && <VisuallyHidden>{label}</VisuallyHidden>} </Wrap>
)
Here I’ve listed the relevant bits happening in the above Icon
component:
When using the Icon component, add the label if the icon is not coupled with text:
import Icon from '../components/Icon'
import MyCompanyLogo from '../components/MyCompanyLogo'
const App = () => (
<Logo>
<Icon icon={MyCompanyLogo} label="Logo" />
<<Logo>
)
Or skip the icon label if you already have text in the element:
const App = () => (
<NoteBox>
<Icon icon={Info} />
Did you know, that SVG stands for Succulently Veracious Giraffe
</NoteBox>
)
There’s a "built-in" way of labelling elements: aria-label
. But keep in mind that aria-label
is less robust way of doing this than having the label inside the <VisuallyHidden>
element. Because aria-label
has some internationalization issues, and it’s not as widely supported (thanks Hugo for the valuable research).
Aria-label in use:
import { Wrap } from './styles'
const Icon = ({ Icon, label }) => (
<Icon aria-label={label} focusable="false" role="img" />
)
About that role
attribute:
If you are using embedded SVG images in your page, it is a good idea to set
role="img"
on the outer<svg>
element and give it a label. This will cause screen readers to just consider it as a single entity and describe it using the label, rather than trying to read out all the child nodes. MDN
Icon buttons or icon links are a common UI-pattern. In that case, the same rules apply, but the wrapping component will be button
or an a
.
Twitter is good example of a UI with a lot of buttons that are only icons:
Another classic example is the search button next to a search bar:
Below is some basic React code for an icon button component, not that much different from the earlier icon component, just handle clicks in the button, or pass in an href
attribute if it’s an anchor, or to
if it’s a router link:
import { Button, VisuallyHidden } from './styles'
const ComposeButton = ({ Icon, label }) => {
return (
<Button onClick={() => {/* Do stuff */})}>
<Icon aria-hidden focusable="false" />
{label && <VisuallyHidden>{label}</VisuallyHidden>}
</Button>
)
}
A cool UI pattern is to have the spinner loader animation appear in the button after it has been clicked. The spinner is just an icon that is spun with a keyframe animation (click it):
If a component will change, i.e.: introduces new elements or changes its value, then aria-live
attribute has to be used so that the screen reader will announce the changes (see more on that below):
const LoadingButton = () => {
const [isLoading, setIsLoading] = React.useState(false)
const handleClick = () => {
setIsLoading(true)
}
return (
<Button
aria-disabled={isLoading} isLoading={isLoading}
onClick={handleClick}
>
{isLoading && (
<SpinnerIcon
aria-live="assertive" icon="spinner"
isBlock={false}
/>
)}
Submit
</Button>
)
}
Here are the highlighted bits explained:
disabled
attribute should not be used, because that will hide the button from assistive technologies. It should rather be deactivated with CSS, that’s why the isLoading
props is passed into the styled-component.aria-live
makes the component to be a "live region", and tells the screen reader that the icon button has just changed. arial-live
can have three values: 1) off
the changes are not announced. 2) polite
the change is announced the next suitable moment. 3) assertive
the change is announced immediately, this should only be used for really important announcements, like errors, or in this case we’re telling the user that a form is been sent.I think there’s a bit of confusion about aria-role="alert"
used in conjunction with aria-live="assertive"
(or it’s just me), I’ve seen these two being used together, but as I was researching this post, the general consensus is that aria-live="assertive"
has an implied value of aria-role="alert"
, so it’s not needed to be defined.
Btw, this is a good video explaining it quite well (taken from cccaccessibility.org article called "Using Aria-live"):
Also an example in a w3.org article implies the similarity of these two attributes ...
.role=alert
which is equivalent to using aria-live=assertive
When isLoading
is true, the button should look disabled (in this case I just played with the opacity), and all the pointer-events should be turned off. You could set the cursor to wait
or not-allowed
, but since we’re setting pointer-events
to none
, that won’t have any effect.
const Button = styled.button(props => ({
marginBottom: '16px',
opacity: props.isLoading ? 0.5 : undefined, padding: '5px 10px',
pointerEvents: props.isLoading ? 'none' : undefined}))
The above code works but is super basic and doesn’t reflect the real-world usage.
Here’s few things you could consider adding to make it more usable:
className
so it can be styled with styled-component or Fela etc.currentcolor
, so it will inherit the color from its wrapping element.block
or inline-block
.import React from 'react'
import PropTypes from 'prop-types'
import ArrowRight from '../icons/ArrowRight'
import Book from '../icons/Book'
import CheckMark from '../icons/CheckMark'
import Cross from '../icons/Cross'
import Info from '../icons/Info'
import { Wrap, Label } from './styles'
export const icons = {
arrowRight: ArrowRight,
book: Book,
checkMark: CheckMark,
cross: Cross,
info: Info
}
const sizes = {
small: '16px',
medium: '24px',
large: '32px',
extraLarge: '40px'
}
const colors = {
red: '--red',
green: '--green',
gray: '--gray-dark',
black: '--black'
}
const Icon = props => {
const { className, icon, isBlock, label, role, size } = props
const IconComponent = icons[icon]
if (!IconComponent) return null
return (
<Wrap
className={className}
color={colors[color]}
isBlock={isBlock}
size={sizes[size]}
>
<IconComponent
aria-hidden
fill="currentcolor"
focusable="false"
role={role}
title={label}
/>
{label && <Label>{label}</Label>}
</Wrap>
)
}
Icon.defaultProps = {
color: '--black',
isBlock: true,
size: 'small'
}
Icon.propTypes = {
className: PropTypes.string,
color: PropTypes.oneOf(Object.keys(colors)),
icon: PropTypes.oneOf(Object.keys(icons)).isRequired,
isBlock: PropTypes.bool,
label: PropTypes.string,
role: PropTypes.string,
size: PropTypes.oneOf(Object.keys(sizes))
}
export default Icon
I mentioned in my other icon post earlier, that icon fonts are fidgety to size correctly, SVG icons aren’t nearly as bad, but one annoying thing I often have to deal with is alignment; sloppyly exported icons align in a different way. Because the icon is bigger, or it has a height and the other one doesn’t, or the viewbox
is off or missing etc. Then you have to tweak styles per icon, terrible.
To get rid of this, make an alignment acid test for the icons by placing some text next to the icon in your design system. Don’t force the alignment with flex-box or transforms etc. This way, at a glance you can see if some of the icons aren’t aligning properly, and you can go into the SVG and fix it. That you never have to worry about misaligned icons again.
In my example I’ve defined the icon size in pixels, because I just like the exactness of that. But another way to handle that is to use 1em
and the height and width of the icon, this way the icon will inherit the text size from its parent component. For example, an icon inside h2 title would be proportional to the text size.
const ArrowRight = props => (
<svg viewBox="0 0 16 16" heigh="1em" width="1em" {...props}>
<path d="M15.5 8L8 .5V5H0v6h8v4.5z" />
</svg>
)
Here’s a demo of the above mentioned icon component.
In the end, the rules related to icons are pretty simple: most of the time they’re hidden from screen readers, and if label is needed, put it inside a visually hidden element.
The better icons are prepared, the easier they’re to use, because you can trust them to be the same size and align the same way. Put in that effort in the beginning and you’ll thank yourself later.
Also, check my post about the technological singularity of icons in web.
Comments would go here, but the commenting system isn’t ready yet, sorry.