Accessible icons
Most icons sit next to a label and should stay out of the way of assistive technology; a few carry meaning on their own and need a name. glyphfox gets the first case right by default and makes the second a single prop.
Decorative by default
Every glyphfox icon renders with aria-hidden="true" unless you give it a name. Screen readers skip it, which is what you want for an icon next to visible text: the text already says what the control does, and an extra "image" in the announcement is noise.
<!-- <DownloadIcon /> next to a label --> <button type="button"> <svg class="glyphfox glyphfox-download" aria-hidden="true" ...>...</svg> Download </button>
The same default applies everywhere: the React, Vue, Svelte and Angular components, the glyph-fox element, the plain HTML runtime and the strings from toSvg. You only change it when an icon is the only carrier of its meaning.
Icons that carry meaning
Pass title and the icon becomes an image with a name. The renderer adds three things: a <title> element as the first child, role="img" and aria-label with the same text, so the name is exposed consistently across browsers and screen readers.
import { CircleCheckIcon, TriangleAlertIcon } from 'glyphfox/react'; export function SyncStatus({ ok }) { return ok ? <CircleCheckIcon title="Synced" color="#15803d" /> : <TriangleAlertIcon title="Sync failed" color="#b91c1c" />; }
<svg class="glyphfox glyphfox-circle-check" role="img" aria-label="Synced" ...> <title>Synced</title> ... </svg>
In React, Vue and Svelte you can also pass aria-label or aria-labelledby straight to the component. They land on the svg, and the icon stops being decorative on its own, without the <title> element and its hover tooltip. decorative overrides the automatic choice in both directions: decorative hides an icon even with a title, decorative={false} exposes it.
Write the name for what the icon means in this place, not for what it depicts. A trash icon is "Delete draft", not "Trash can"; a circle check in a list of payments is "Paid".
Icon only buttons and links
When an icon is the only content of a button or a link, name the control, not the icon. The control is what receives focus and what a screen reader announces; the icon inside stays decorative.
import { XIcon, SettingsIcon, SearchIcon } from 'glyphfox/react'; export function Toolbar({ onClose }) { return ( <> <button type="button" aria-label="Close dialog" onClick={onClose}> <XIcon /> </button> <a href="/settings" aria-label="Settings"> <SettingsIcon /> </a> <button type="submit"> <SearchIcon /> <span className="visually-hidden">Search the docs</span> </button> </> ); }
Visually hidden text, the third pattern, is translated by browser translation tools, which often skip attributes; prefer it on multilingual sites. A typical visually-hidden class clips the element to one pixel without removing it from the accessibility tree.
.visually-hidden { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
Toggles need their state, not a new name. A menu button that morphs from menu to x keeps its label and sets aria-expanded; a play button that morphs to pause changes its label or uses aria-pressed. The morph is the visual half of that change; the animated icons guide shows both halves together.
Status, color and contrast
Color alone must not carry meaning (WCAG 1.4.1). Use different shapes for different states, such as circle-check, circle-alert and circle-x, and pair them with text where you can. Badges help too: file+check and file+alert differ in shape, not only in color.
Icons that people need to understand the page or to find a control must reach a contrast of 3 to 1 against their background (WCAG 1.4.11, non text contrast). Measure the stroke color, because that is what draws the shape; the translucent duotone layer is a decoration on top of it. Decorative icons next to text are not covered by the rule, but low contrast still makes them harder to scan.
Thin strokes look lighter than their color suggests, because antialiasing blends them with the background. glyphfox compensates below 24 pixels by making the stroke up to 30 percent heavier; at 14 or 16 pixels on a muted color, a weight of 2.25 or 2.5 keeps icons readable. absoluteWeight turns the compensation off, so use it with care at small sizes.
Motion
Every living state and every morph respects prefers-reduced-motion: reduce: presets do not play, CSS morphs switch without a transition and component morphs jump to their final shape. The end state stays visible, so no information is lost.
Content that moves on its own for more than five seconds needs a way to pause it (WCAG 2.2.2). Tie looping animations such as spin to the process they show and stop them when it ends, and avoid looping attention seekers such as a bell that shakes forever.
Right to left layouts
In Arabic, Hebrew or Persian interfaces, icons that point along the reading direction must flip: back and forward arrows, chevrons in breadcrumbs, reply and undo. mirrorRtl adds a class that mirrors the icon whenever it is inside dir="rtl", so the same markup works in both directions.
import { ChevronLeftIcon, ChevronRightIcon } from 'glyphfox/react'; export function Pager({ onPrev, onNext }) { return ( <nav aria-label="Pagination"> <button type="button" onClick={onPrev}><ChevronLeftIcon mirrorRtl /> Previous</button> <button type="button" onClick={onNext}>Next <ChevronRightIcon mirrorRtl /></button> </nav> ); }
Do not mirror icons whose direction does not follow the text: media controls such as play and fast-forward keep pointing right, a clock keeps turning clockwise, a check stays as it is, and rotate-cw still means clockwise.
Masks, sprites, the element and Angular
- Tailwind CSS classes render an empty element with a mask, which has no semantics at all. Add
aria-hidden="true"and give the control a visually hidden label. In Windows high contrast mode a mask icon can vanish; the Tailwind CSS guide has the fix. - SVG sprites leave the outer
svgto you: addaria-hidden="true"for a decorative icon, orrole="img"andaria-labelfor a meaningful one. The sprite guide has examples. - The
glyph-foxelement and the Angulargf-iconrender thesvginside the host element. Anaria-labelon the host makes the innersvgan image but does not name it, and a label on a generic element is not announced reliably. Use thetitleattribute or input, which names the innersvg.
<!-- names the inner svg: role img, aria-label and a title --> <glyph-fox name="circle-alert" title="Payment failed"></glyph-fox> <!-- Angular --> <gf-icon name="circle-alert" title="Payment failed"></gf-icon>
Test it
- Open the accessibility tree in your browser devtools and select the control: its name should be the label you wrote, and decorative icons should not appear at all.
- Navigate with a screen reader: VoiceOver on macOS and iOS, NVDA on Windows, TalkBack on Android. Listen for doubled names and for anything announced as just "image".
- Emulate
prefers-reduced-motionandforced-colorsin the rendering tools of Chromium devtools and check that icons stay visible and still. - Check contrast of meaningful icons with a color picker against the actual background, including hover and focus states.
Props that matter here
| Prop | Type | Default | Description |
|---|---|---|---|
color | string | currentColor | Main color. |
weight | number | 2 | Stroke from 1 to 3 on the 24 grid, optically compensated below 24 px. |
absoluteWeight | boolean | false | Keeps the stroke at weight pixels at any size. |
mirrorRtl | boolean | false | Mirror in right to left layouts. |
title | string | none | Accessible name: title, role img and aria-label. |
decorative | boolean | true without a title | Hides the icon from assistive technology. |
Pitfalls
- Naming the control and the icon
- A
titleon an icon inside a button that already has a label makes screen readers announce both, and the<title>also shows as a hover tooltip. Name one thing: the control. - An image without a name
decorative={false}with notitleand noaria-labelexposes an image with no name, which screen readers announce as "image" or by its file name. Give every exposed icon a name.- State that only the icon shows
- A morph from
copytocheckis invisible to a screen reader. Announce the result in text: change the button label, or put "Copied" in a live region withrole="status". - Tooltips from titles
- The browser tooltip from
<title>appears on mouse hover only, never on focus or touch. Do not put information there that is not available another way. - Labels on the host element
- On
glyph-foxandgf-icon, usetitlerather thanaria-label, as described above.
Questions
Should every icon have a title?
No. Most icons sit next to text or inside a labelled control, and a title there only adds noise. Give a title to icons that stand alone and carry information, such as a status icon in a table cell.
Is aria-hidden enough for decorative icons?
Yes. aria-hidden="true", which glyphfox adds by default, removes the icon from the accessibility tree. SVG icons are not focusable, so there is nothing else to hide.