QR code
A working QR encoder, in the browser, with no dependency — Galois field arithmetic, Reed–Solomon error correction and all. It renders as SVG, so it stays sharp at any size and prints at the printer's resolution rather than the screen's.
When to use it
To move something from a screen to a phone, or from paper to a phone: a ticket, a Wi-Fi network, a link to a page someone is standing in front of, a two-factor enrolment.
The test is whether the reader has a second device. A QR code on a phone screen, to be scanned by that same phone, is a puzzle rather than a shortcut — use a link.
<span class="qr" data-deck-qr="https://example.com/tickets/9fQ2xR"
role="img" aria-label="QR code for your ticket"></span>
<span class="qr" data-deck-qr="https://example.com/tickets/9fQ2xR"
role="img" aria-label="QR code for your ticket"></span>
An empty data-deck-qr falls back to the element's own text content, which
is occasionally handy — the value stays visible and selectable if the script never
runs.
Options
-
data-deck-qr— the value to encode. Anything: a URL, aWIFI:string, anotpauth://URI. -
data-ecl— error-correction level,L,M,QorH, defaulting toM. Higher levels survive more damage and hold less data. -
data-caption— renders a.qr-captionunderneath, capped to the code's own width and set tooverflow-wrap: anywhereso a long URL breaks rather than stretching the layout.
<div class="cluster" style="align-items:flex-start">
<span class="qr qr-sm" data-deck-qr="https://example.com/a"
role="img" aria-label="QR code, small"></span>
<span class="qr" data-deck-qr="https://example.com/b" data-ecl="Q"
data-caption="example.com/b"
role="img" aria-label="QR code for example.com/b"></span>
<span class="qr qr-lg" data-deck-qr="https://example.com/c" data-ecl="H"
role="img" aria-label="QR code, large"></span>
</div>
<div class="cluster" style="align-items:flex-start">
<span class="qr qr-sm" data-deck-qr="https://example.com/a"
role="img" aria-label="QR code, small"></span>
<span class="qr" data-deck-qr="https://example.com/b" data-ecl="Q"
data-caption="example.com/b"
role="img" aria-label="QR code for example.com/b"></span>
<span class="qr qr-lg" data-deck-qr="https://example.com/c" data-ecl="H"
role="img" aria-label="QR code, large"></span>
</div>
Versions 1 to 10 only. The encoder covers the smaller symbol versions, which is roughly 170 alphanumeric characters at level M and considerably fewer at H. Past that it throws, and the component catches it and renders the error message in place of the code — so a value that is too long shows you a sentence rather than failing silently. Shorten it, or link to it.
A logo in the middle
.qr-logo on the frame and .qr-logo-mark on a child punches a
mark out of the centre — a white square at 22% of the code's width, with your logo
inside it.
This works because QR codes carry error correction, and it works only within that
budget. The source is explicit about the limit: only safe at error correction Q
or H. At the default M you are spending redundancy you may need
for a crease in the paper or a reflection on a screen.
<span class="qr qr-lg qr-logo" data-deck-qr="https://example.com/deck" data-ecl="H"
role="img" aria-label="QR code for example.com/deck">
<span class="qr-logo-mark">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#deck-mark"></use></svg>
</span>
</span>
<span class="qr qr-lg qr-logo" data-deck-qr="https://example.com/deck" data-ecl="H"
role="img" aria-label="QR code for example.com/deck">
<span class="qr-logo-mark">
<svg class="icon" aria-hidden="true"><use href="/assets/deck/deck-icons.svg#deck-mark"></use></svg>
</span>
</span>
Test the result by scanning it, on a real phone, from the size it will actually be printed at. A code that scans on a 240px screen may not scan at 20mm on a receipt.
Why it stays white in dark mode
.qr sets background: var(--qr-bg, #fff) and fills its modules
with var(--qr-fg, #000). Both defaults are literal black and white rather
than Deck tokens, so the code does not follow
light-dark() and does not invert in dark mode. In a dark interface you get
a white card.
That is deliberate and it is the right call. A scanner is not reading your theme: it is looking for high contrast between light and dark modules in the orientation the spec describes. Inverted codes are read by some scanners and not others, and low-contrast codes by almost none. The white card is slightly ugly and always works.
The two custom properties are there if you have a real reason — a brand-coloured foreground on white can scan fine if the contrast is high enough. Change the foreground, keep the background light, and test it before shipping.
--qr-quiet is the padding, and it defaults to 12px. That white margin is
part of the specification, not decoration: scanners use it to find the symbol's edges.
Setting it to zero produces a code that looks right and frequently will not scan.
Classes
Generated from src/24-media.css by tools/docs/extract.mjs.
| Class | Layer | What it does | Source |
|---|---|---|---|
.qr |
deck.components |
deck.js renders the modules into this frame. Kept square, with a quiet zone, so it stays scannable at any size. | 24-media.css:344 |
.qr-caption |
deck.components |
Sets margin-block-start, font-size, color, text-align, and 2 more. | 24-media.css:375 |
.qr-lg |
deck.components |
Sets --qr-size. | 24-media.css:357 |
.qr-logo |
deck.components |
A logo punched out of the middle. Only safe at error correction Q or H. | 24-media.css:360 |
.qr-logo-mark |
deck.components |
No declarations. | 24-media.css:363 |
.qr-sm |
deck.components |
Sets --qr-size. | 24-media.css:358 |
Tokens
| Token | Live | Declared value |
|---|---|---|
--line |
light-dark(var(--ink-200), oklch(28% .016 var(--hue-neutral))) |
|
--r-sm |
7px |
|
--r-xs |
4px |
|
--text-xs |
.75rem |
|
--text-muted |
light-dark(var(--ink-600), var(--ink-400)) |
|
--space-2 |
.5rem |
--qr-size, --qr-quiet, --qr-bg and
--qr-fg are component-level custom properties with literal fallbacks, not
Deck tokens — which is the whole point of the section above.
Accessibility
-
A QR code is an image and announces as nothing. The rendered SVG is a
grid of unlabelled rectangles. Give the frame
role="img"and anaria-labelsaying what scanning it does — every example here does. -
Never make it the only route. This is the important one. A QR code is
unusable to a blind reader, to anyone without a second device, and to anyone reading a
printout at a desk. The same destination must be available as a link or as text you
can type —
data-captionexists partly for this. - Scanning needs steady hands and a working camera. Motor impairment, low vision and an old phone all make it harder. It is a convenience, and treating it as a primary interface excludes people quietly.
-
The caption should be readable, not just present.
.qr-captionis--text-xsand muted, which is Deck's smallest text. If it carries the fallback URL, consider making it larger — the whole point is that someone can read and type it. - Do not encode something the reader cannot verify. A code is opaque until it is scanned, so a reader has no way to know where it goes. Print the destination beside it, which is both an accessibility measure and a phishing one.
-
Size for the distance. A poster code scanned from two metres needs to
be much larger than
.qr-lg. The usual rule is roughly one tenth of the scanning distance.
Right to left
A QR code has a fixed orientation defined by the specification and does not mirror in
any direction — a flipped code does not scan. Nothing in the component is directional
and nothing in src/19-logical.css touches it, which is correct.
The caption is ordinary text and follows the document direction, so an Arabic caption reads right to left under a code that has not moved. A URL inside it stays left-to-right on its own, through the bidirectional algorithm rather than through any rule of Deck's.
<span dir="rtl" class="qr" data-deck-qr="https://example.com/tickets/9fQ2xR"
data-caption="امسح للحصول على تذكرتك"
role="img" aria-label="رمز الاستجابة السريعة للتذكرة"></span>
<span dir="rtl" class="qr" data-deck-qr="https://example.com/tickets/9fQ2xR"
data-caption="امسح للحصول على تذكرتك"
role="img" aria-label="رمز الاستجابة السريعة للتذكرة"></span>
Reduced motion
Nothing here animates — no transitions, no keyframes, no transforms. The code appears
when the script has finished encoding it, which is not motion. There is nothing for
prefers-reduced-motion to act on.
Printing
This is the one component in Deck that is designed for print, and it shows. The
print block at the end of src/24-media.css reads:
/* A QR code is often the whole point of printing the page */
.qr { border: 0; break-inside: avoid; }
The border goes because on paper the code needs no frame, and
break-inside: avoid stops it being split across two pages — a half-printed
QR code is not a degraded QR code, it is nothing at all.
The rest is handled on the component itself rather than in a print block:
.qr carries print-color-adjust: exact, which is what stops the
browser dropping its white background and black modules as "decorative" colour. Without
that line the code would print as an empty square on most default settings.
And because it is SVG, it prints at the printer's resolution. A raster QR code scaled up for print acquires soft edges, and soft edges are what a scanner fails on.
Overriding it
/* One code, no layer needed */
<span class="qr" style="--qr-size:180px" data-deck-qr="…"></span>
@layer app.components {
/* A brand-coloured foreground. Keep the background light and test it. */
.qr-brand { --qr-fg: oklch(30% .09 264); }
/* A larger quiet zone for a code that will be printed small */
.qr-print { --qr-quiet: 20px; }
/* A readable caption */
.qr-caption { font-size: var(--text-sm); color: var(--text); }
}
Deck.qr exposes svg(), build() and
encode() if you need a code somewhere the attribute cannot reach — a canvas,
a download, a value that only exists after a fetch.
When not to use it
- Not on a phone, for that phone. If the reader is already holding the destination, give them a link.
- Not as the only way to reach something. See Accessibility — this is the rule that matters most on this page.
- Not for more than about 170 characters. The encoder stops at version 10 and will tell you so. Encode a short link, not the payload.
-
Not with a logo at the default error-correction level.
data-ecl="Q"at minimum, and test it. - Not for a secret. A code on screen is readable by anyone in the room and by anyone in a photograph of the room. Enrolment codes should be short-lived.
-
Not without the quiet zone. Setting
--qr-quiet: 0to make it look tidier is the single most reliable way to produce a code that does not scan.