QRCode

A QR code drawn on canvas or as SVG, with an icon in the middle, an error correction level and an expired state that offers a refresh.

Accessibility

A QR code is a picture of a URL that only a camera can read, so most of this tab is about readers who do not have one.

What it renders

A <div> holding a <canvas> or an <svg>, depending on type. Both have role="img" and no name, so a screen reader announces nothing useful.
  • There is no text inside. Nothing about the destination can be read except by a camera.
  • Name it with aria-label. ARIA attributes and role go to the canvas or svg, and every other prop stays on the wrapper.
  • title is not a name. It stays on the wrapper as a browser tooltip, which only a mouse can show.
  • Name it by what it does: “QR code to open your boarding pass”, not “QR code”. Never put the encoded URL in the label, since a URL read aloud is not an instruction.

The same destination without a camera

No prop can do this for you. A QR code does not work for a blind reader, for someone without a working camera, for someone reading on the device that shows it, or for someone who cannot hold a phone steady. Give the destination another route on the same screen.
  • Put the URL next to the code as a real link when the reader may be on this device.
  • Otherwise, show a short code they can type on the other device: six to eight characters, easy to tell apart, in text a screen reader can spell out.
  • Both routes must lead to the same place and expire together.
  • Say which is which: “Scan this, or go to example.com/pair and enter 4F2K9Q”.

Expired and loading are drawn, not announced

Every status except active covers the code with an overlay: text in a plain div, or a spinner. None of it is a live region, so a reader who cannot see it is never told the code expired.
  • Announce the change yourself with a polite live region near the code, using the same sentence the overlay shows.
  • loading shows a spinner and no text. Say “generating a new code” somewhere a screen reader reaches.
  • expired shows the refresh button only when onRefresh is set. Without it, nobody can get past the expired state.
  • Move focus to the refresh button when a code the reader was waiting on expires, or only sighted readers will find it.

Two different contrast checks

A camera reads the code and a person reads the overlay, so check each one separately.
  • The scanner needs dark on light. color defaults to the text color and bgColor to transparent, so in dark mode the code turns light on dark, which some scanners refuse. Set both colors when it matters.
  • The overlay needs 4.5:1. It is a nearly solid layer over the code, so check the expiry text and the refresh link against it, in both modes.
  • Tinting the code for your brand costs margin on both checks. Tint the frame around it instead.
  • bordered={false} also removes the padding, which is the only light space around the code. Keep it on a busy or dark surface.

Nothing here is focusable

The code takes no tab stop, which is right for a picture. The only focusable thing is the refresh button in the expired state: a real Button with the reload icon next to its text.
  • A custom statusRender replaces all three overlays, refresh button included. Rebuild it with a real button, since a <div onClick> cannot be reached by keyboard.
  • Keep the refresh text. An icon alone leaves the button unnamed, and locale gives you the text already translated.
  • Do not make the code itself clickable in place of a link. Put the link next to it.