Skip to content

QR Code

Size, quiet zone, error correction level — and always printing the destination as text beside it.

Also called 2D Barcode, Scan Code — in this system all of them are QR Code.

Live preview

Everything below is the real component. Change the controls, tab through it, and turn on Inspector Mode to read any value off the screen.

Playground
Open on your phonehttps://acme.dev/d/9fJk2Lm

The quiet zone is not padding

Four modules of white on every side is what lets a scanner locate the symbol. Cropping it is the most common cause of a code that will not read — and it still looks correct.

4 modulesScans
2 modulesMarginal
NoneFrequently fails

Never invert for dark mode

Most scanners assume dark modules on light. A code is a machine target that happens to be on your page — it keeps its white field in every theme.

White field
InvertedFails on many scanners

Always beside the destination

The code, the URL as selectable text, and a copy control. Anyone who cannot scan still has a way through.

Scan to continue on your phonehttps://acme.dev/d/9fJk2LmExpires in 10 minutes

Interaction states

Every state a user can put this component into, rendered side by side. If a state is missing here, it is missing in production too.

Default
Large
With logo
No quiet zone
Inverted
Loading
Expired
Expired
On a card

Anatomy

Every part, every measurement, and the reason it is that number.

Open on your phonehttps://acme.dev/d/9fJk2Lm

Three finder patterns, a field of modules, four modules of quiet zone, and the destination printed as text beside it.

  1. Module≥ 4px on screen

    The smallest square. Below about four device pixels a phone camera cannot resolve them reliably, which is what sets the minimum overall size.

  2. Quiet zone4 modules, every side

    Part of the specification, not padding. It is what lets a scanner find the symbol’s edges, and cropping it is invisible in review.

  3. Finder patterns7 × 7, three corners

    How the scanner establishes orientation. They must never be covered, restyled or rounded away.

  4. FieldWhite, in every theme

    The code keeps its own surface. It is a machine target on your page, not a surface that should follow the theme.

  5. Error correctionM by default, H with a logo

    Level M tolerates about 15% damage; H tolerates 30% and makes the symbol denser. Only raise it when something covers the centre.

  6. Logo area≤ 20% of the centre

    With a white gap around it. Past 20% even level H stops recovering reliably.

  7. The URLSelectable text beside it

    The only route for anyone who cannot scan. Without it the code is a dead end for a screen-reader user or a laptop with no camera.

Design tokens used

Values are read live from the running stylesheet, so this table can never drift from the code. Click any value to copy it.

Color

TokenValueUsed for
#0a0b0e—Modules — near-black rather than pure black, for print
#ffffff—The field and quiet zone, in every theme
--ds-border-subtle—An optional card edge, outside the quiet zone
--ds-fg-secondary—The destination URL
--ds-fg-muted—Expiry and helper text

Spacing

TokenValueUsed for
quiet zoneMargin on every side — part of the code

Radius

TokenValueUsed for
--radius-mdCorners of the white field, never of the modules

Typography

TokenValueUsed for
font-mono—The URL, so it can be transcribed if it must be

Recommended sizes

Pick a size from this table. Do not invent a new one — a fourth height is how a design system starts dying.

SizeHeightLabel gapMin widthMax widthWhen to use
Minimum96px———About 4px per module for a 21-module symbol. Below this, scanning becomes unreliable.
Default128px———Comfortable on a laptop screen at a normal viewing distance.
Feature192px+———A dedicated pairing or hand-off screen where the code is the subject.
Print——2cm—The physical minimum for a phone camera at arm’s length. Larger for anything scanned from further away.
Quiet zone—4 modules——Non-negotiable. It scales with the module size, not with the pixel size.
Logo———20% of the symbolWith a white gap around it, and error correction raised to H.

Do

Keep the full quiet zoneFour modules on every side is what lets a scanner find the symbol. Cropping it is the most common cause of a code that will not read, and the code still looks fine.
acme.dev/d/9fJk2Lm
Print the destination as textA code is unreadable to a human. The URL beside it is the only route for anyone without a camera, and it costs one line.
Keep the white field in dark modeThe code is a machine target, not a surface. Most scanners assume dark on light, and inverting fails on a meaningful share of devices.
M
H + logo
Raise correction only when you cover the centreLevel H tolerates 30% damage and makes the symbol denser. It is the price of a logo, not a default to reach for.

Don't

Do not crop the quiet zoneIt is part of the specification. Removing it to make the code sit tighter in a layout is the most common way a working code stops working.
Do not invert for dark modeMost scanners expect dark modules on a light field. An inverted code fails silently on a large fraction of phones, and the user blames their camera.
Do not tint the modulesThe contrast between module and field is what makes it readable. A brand-coloured code trades a real function for a decorative one.
Do not show one to a phone userNobody scans a code with the device displaying it. On a small viewport the same value should be a link or a copy button.

Accessibility

Not a checklist to run at the end. These are the requirements the component was built from.

1.1.1Non-text ContentA1.4.11Non-text ContrastAA1.4.5Images of TextAA2.5.8Target Size (Minimum)AA

Contrast

  • Module to field contrast must be as close to maximum as possible. This is a machine-vision requirement, and it is stricter than any WCAG ratio.
  • The code keeps a white field in dark mode. Contrast against the page is the container’s problem, not the code’s.
  • The URL beside it is content and owes 4.5:1.
  • In forced-colors mode the code must be exempt — a QR rendered in system colours is not scannable.

Keyboard

TabReaches the copy and download controls beside the code. The code itself is not focusable.
EnterCopies the destination, which is what a keyboard user actually needs from this component.

Screen readers

  • Announce the destination, not the format: "QR code linking to acme.dev/d/9fJk2Lm".
  • The readable URL is the real accessibility feature. Everything else is a convenience for people who can point a camera at a screen.
  • If the code expires, say so in text as well as visually — a code that stopped working with no explanation is unexplainable.

Focus & touch

  • The code is not interactive and never focusable. The copy and download controls beside it are, and they are what make the component usable without a camera.
  • On a phone, do not show the code at all — nobody scans a screen with the device rendering it. Detect the viewport and offer a link or a copy button instead. If the code must appear for a hand-off to a second device, keep it at least 128px so the other camera can resolve the modules.
AttributeApplied toNotes
role="img"The SVGWith aria-label naming the destination: "QR code linking to acme.dev/d/9fJk2Lm". "QR code" alone is useless.
Visible textBeside the codeThe most important accessibility feature here. A code with no readable destination is a dead end.
aria-describedbyThe codePointing at expiry or instructions, so the constraints are announced with it.
aria-live="polite"A refreshing codeAnnounce regeneration: "Code refreshed". A silently changing code is disorienting.

Code

Example usage

tsx
1import { QrCode } from '@/ui/Display'23<Row>4  <QrCode value={pairingUrl} size={128} />5  <Stack>6    <span>Open on your phone</span>7    {/* The only route for anyone who cannot scan. Never omit it. */}8    <code>{pairingUrl}</code>9    <CopyButton value={pairingUrl} label="Copy link" />10  </Stack>11</Row>1213// Nobody scans a code with the device displaying it.14const isPhone = useMediaQuery('(max-width: 640px)')15{isPhone ? <Link href={pairingUrl}>Continue</Link> : <QrCode value={pairingUrl} />}1617// Error correction is a trade, not a default. M is right until something18// covers the centre; H makes the symbol denser.19<QrCode20  value={url}21  errorCorrection={logo ? 'H' : 'M'}22  logo={logo}                     // ≤ 20% of the symbol, with a white gap23/>2425// The code keeps its own white field in every theme — it is a machine target26// on your page, not a surface that follows the theme.27<div className="rounded-lg bg-white p-3">28  <QrCode value={url} />29</div>3031// A refreshing code must announce itself, or it changes silently mid-scan.32<p role="status" aria-live="polite" className="sr-only">Code refreshed</p>

Framework-free HTML

html
<figure class="ds-qr">
  <!-- Name the DESTINATION. "QR code" on its own is useless. -->
  <svg
    role="img"
    aria-label="QR code linking to acme.dev/d/9fJk2Lm"
    aria-describedby="qr-expiry"
    viewBox="0 0 29 29"
    shape-rendering="crispEdges"
  >
    <!-- The quiet zone is part of the code: 4 modules on every side. -->
    <rect width="29" height="29" fill="#ffffff" />
    <g transform="translate(4 4)">…</g>
  </svg>

  <figcaption>
    <p>Open on your phone</p>
    <!-- The real accessibility feature. -->
    <code>https://acme.dev/d/9fJk2Lm</code>
    <p id="qr-expiry">Expires in 10 minutes</p>
  </figcaption>
</figure>

CSS

css
.ds-qr svg {
  /* The code keeps its own field in every theme: it is a machine target
     that happens to be on your page. */
  background: #ffffff;
  border-radius: var(--radius-md);
  /* Modules are squares. Anti-aliasing softens their edges and costs
     scan reliability at small sizes. */
  shape-rendering: crispEdges;
  /* About 4px per module for a 21-module symbol. */
  min-inline-size: 96px;
}

/* On the container, never inside the quiet zone. */
.ds-qr { padding: 8px; background: #fff; border-radius: var(--radius-lg); }

.ds-qr code {
  font-family: var(--font-mono);
  font-size: 12px;
  color: var(--ds-fg-secondary);
  word-break: break-all;             /* a long URL must stay fully visible */
}

/* A QR rendered in system colours is not scannable. */
@media (forced-colors: active) {
  .ds-qr svg { forced-color-adjust: none; }
}

/* Nobody scans a screen with the device rendering it. */
@media (max-width: 640px) {
  .ds-qr__code { display: none; }
  .ds-qr__link { display: block; }
}

/* Print is where codes are most useful, and printers drop backgrounds by
   default. */
@media print {
  .ds-qr svg { background: #fff !important; print-color-adjust: exact; }
}

Component API

QrCode

PropTypeDefaultDescription
value*string—The encoded value. Keep it short — a long URL means more modules and a denser, harder-to-scan symbol.
sizenumber128Pixels. 96 is the practical floor for reliable scanning on screen.
errorCorrection'L' | 'M' | 'Q' | 'H''M'H only when a logo covers the centre. Higher levels make the symbol denser.
quietZonenumber4Modules of margin. Reducing it is the most common cause of a code that will not read.
logoReactNode—Centred, at most 20% of the symbol, with a white gap. Requires level H.
labelstring—The accessible name. Defaults to "QR code linking to {value}".

Notes

Professional tips

  • Keep the encoded URL short. Every extra character adds modules, and a denser symbol is harder to scan at the same physical size — use a short link, not the canonical one.
  • Show an expiry when the code is a session or a pairing token, and refresh it before it lapses rather than after.
  • Offer a download for print, and export SVG rather than PNG so it stays sharp at any physical size.
  • Test on real hardware, at arm’s length, in poor light. A code that scans in a design review at 20cm may fail on a wall at two metres.
  • For Wi-Fi credentials, use the WIFI: URI scheme rather than a URL — the phone joins the network directly instead of opening a browser.

Performance

  • Generate the SVG on the client and cache it by value. Fetching a code from a rendering service is a network round trip for something computable locally.
  • Prefer SVG over canvas: it scales, it prints sharply, and it is smaller than a PNG at any useful size.
  • Only regenerate when the value changes. A code that re-renders on every parent update flickers at exactly the moment someone is aiming a camera at it.
  • Do not animate a QR code. Any motion during a scan is a failed scan.

Common mistakes

  • Cropping the quiet zone, which breaks scanning while looking correct.
  • Inverting for dark mode, which fails on a large share of scanners.
  • Tinting the modules, trading a real function for decoration.
  • A logo larger than 20% of the symbol, past what even level H can recover.
  • No readable URL, leaving anyone without a camera with a dead end.
  • Showing a code on a phone, where nobody can scan it.
  • aria-label of "QR code" with no destination.
  • Encoding a very long URL, producing a symbol too dense to scan at the size shown.

Real-world recommendations

  • Codes work best in genuine hand-off moments: desktop to phone, screen to scanner, print to camera. Outside those, a link is almost always better.
  • The most valuable part of the component is the readable URL beside it. It handles every case where scanning is impossible, and it costs one line.
  • Two-factor enrolment is the archetype: a long secret, a device that cannot type it, and a scanner already open. That is where the pattern earns its place.
  • Print is where quiet zones die. Layout tools crop them, and the failure only shows up after the run — check the artwork, not the screen.