QR code
A scannable code for a URL or secret, with a download action.
Share Where is my payment
Anyone with the link can call the agent from a browser.
Print it on supplier onboarding packets or remittance notices so suppliers can call from a phone camera.
import { Button } from "@oration/canon/components/button";import { CopyButton } from "@oration/canon/components/copy-button";import { QRCode } from "@oration/canon/components/qr-code";import { toast } from "@oration/canon/components/toast";import { ArrowUpRightIcon } from "lucide-react";import * as React from "react";export function Hero() { const url = "https://call.oration.app/cedarline/where-is-my-payment"; const headingId = React.useId(); return ( <section aria-labelledby={headingId} className="flex w-full max-w-md flex-col gap-4 rounded-xl bg-popover p-4 shadow-lg" > <div className="flex flex-col gap-1"> <h2 id={headingId} className="text-base leading-none font-medium text-foreground" > Share Where is my payment </h2> <p className="text-sm text-muted-foreground"> Anyone with the link can call the agent from a browser. </p> </div> <div className="flex items-center gap-2 rounded-[10px] bg-muted/70 py-1 pr-1 pl-3"> <span className="min-w-0 flex-1 truncate font-mono text-xs text-foreground"> {url} </span> <CopyButton value={url} label="Copy link" onCopied={() => toast.add({ title: "Link copied" })} /> </div> <div className="flex flex-col items-center gap-4 sm:flex-row sm:items-start"> <QRCode value={url} size={120} label="QR code for the Where is my payment agent" downloadName="where-is-my-payment-qr" /> <div className="flex min-w-0 flex-1 flex-col gap-2 text-13 text-muted-foreground"> <p> Print it on supplier onboarding packets or remittance notices so suppliers can call from a phone camera. </p> <Button type="button" variant="ghost" size="sm" className="-ml-2 w-fit" onClick={() => toast.add({ title: "Opening the call page", description: "call.oration.app opens in a new tab.", }) } > Open call page <ArrowUpRightIcon data-icon="inline-end" aria-hidden="true" /> </Button> </div> </div> </section> );}Usage#
QR code renders a scannable code for a URL, phone number or setup secret on a white tile, with an optional PNG download. Oration uses it to share an agent's call page and to test the WhatsApp channel from a phone. The modules stay black on white in both themes, because many scanners fail on inverted codes. What people forget is the name: the default accessible label is just QR code, so always say what it opens.
When to use
- To move a link from screen to phone: an agent's public call page, a WhatsApp chat, a supplier portal sign-in.
- For a code people will print, such as on supplier onboarding packets or remittance notices, with
downloadName. - To add an account to an authenticator app during two-step setup, beside the typed setup key.
- Beside the same link as text with a copy button, never instead of it.
When not to use
- As the only way to reach a link. Show the URL with a copy button too. Use Copy row
- For a decorative identity mark. Use Identicon
- For a code people read and type, such as a join code. Use One-time code
Black on white, always
The Machine Mono Rule
Anatomy#
- Tile. A white tile with 12px corners, 12px of padding and a 10% ink outline (white at 10% in dark), so it reads on any surface.
- Code. An SVG at
sizepixels withrole="img"and yourlabelas its name. - Download. Optional with
downloadName: an outlinesmbutton that saves a 1024px PNG.
Examples#
Sizes
size sets the code's width in pixels; the white tile adds 12px of padding around it. 96 to 140px suits dialogs and setup steps, 160px is the default.
import { QRCode } from "@oration/canon/components/qr-code";export function Sizes() { const sizes = [96, 120, 160]; return ( <> {sizes.map((size) => ( <figure key={size} className="flex flex-col items-center gap-2"> <QRCode value="https://wa.me/13125550100?text=Where%20is%20my%20payment" size={size} label={`QR code for Cedarline on WhatsApp, ${size}px`} /> <figcaption className="text-xs text-muted-foreground tabular-nums"> {size === 160 ? "160px, default" : `${size}px`} </figcaption> </figure> ))} </> );}With a download
downloadName adds an outline Download PNG button. The PNG is drawn separately at 1024px with a four-module quiet zone, so it prints cleanly.
Test it
Scan with your phone to open a chat with Cedarline on WhatsApp. The PNG is 1024px with the standard quiet zone, ready for print.
import { QRCode } from "@oration/canon/components/qr-code";export function WithDownload() { return ( <div className="flex flex-col items-center gap-3 sm:flex-row sm:items-start sm:gap-6"> <QRCode value="https://wa.me/13125550100?text=Where%20is%20my%20payment" size={140} label="QR code for Cedarline on WhatsApp" downloadName="cedarline-whatsapp" /> <div className="flex max-w-xs flex-col gap-1"> <p className="text-sm font-medium text-foreground">Test it</p> <p className="text-13 text-muted-foreground"> Scan with your phone to open a chat with Cedarline on WhatsApp. The PNG is 1024px with the standard quiet zone, ready for print. </p> </div> </div> );}Error correction
level trades density for resilience. Higher levels survive smudges and folds on printed notices but pack more modules into the same size.
import { QRCode } from "@oration/canon/components/qr-code";export function ErrorCorrection() { const levels = [ { level: "L" as const, note: "Recovers about 7%" }, { level: "M" as const, note: "About 15%, the default" }, { level: "H" as const, note: "About 30%, for print" }, ]; return ( <> {levels.map((entry) => ( <figure key={entry.level} className="flex flex-col items-center gap-2" > <QRCode value="https://call.oration.app/cedarline/where-is-my-payment" size={112} level={entry.level} label={`QR code for the Where is my payment agent, level ${entry.level}`} /> <figcaption className="flex flex-col items-center text-xs"> <span className="font-medium text-foreground"> Level {entry.level} </span> <span className="text-muted-foreground"> {entry.note} </span> </figcaption> </figure> ))} </> );}Authenticator setup
An otpauth:// URI for two-step sign-in, with the same secret as a mono setup key people can copy and type instead.
Scan with your authenticator app
Or enter this key by hand.
import { CopyButton } from "@oration/canon/components/copy-button";import { QRCode } from "@oration/canon/components/qr-code";import { toast } from "@oration/canon/components/toast";export function Authenticator() { const secret = "JBSW Y3DP EHPK 3PXP"; const uri = "otpauth://totp/Oration:maya.okafor@cedarline.io?secret=JBSWY3DPEHPK3PXP&issuer=Oration"; return ( <div className="flex w-full max-w-md flex-col gap-4 sm:flex-row sm:items-start"> <QRCode value={uri} size={128} label="QR code to add Oration to your authenticator app" /> <div className="flex min-w-0 flex-1 flex-col gap-3"> <div className="flex flex-col gap-1"> <p className="text-sm font-medium text-foreground"> Scan with your authenticator app </p> <p className="text-13 text-muted-foreground"> Or enter this key by hand. </p> </div> <div className="flex items-center gap-2 rounded-[10px] bg-muted/70 py-1 pr-1 pl-3"> <span className="min-w-0 flex-1 font-mono text-xs tracking-wide text-foreground"> {secret} </span> <CopyButton value={secret.replaceAll(" ", "")} label="Copy setup key" onCopied={() => toast.add({ title: "Setup key copied" }) } /> </div> </div> </div> );}States#
| State | Treatment |
|---|---|
| Default | The tile and code, at 160px unless size says otherwise. |
| With download | The tile, then a 12px gap and the Download PNG button, centered under it. |
| Dark theme | Unchanged: a white tile with black modules. Only the tile's outline turns to white at 10%. |
Behavior#
- The code updates whenever
valuechanges; there's no loading state. - The visible code is an SVG, so it stays sharp at any size and zoom.
- With
downloadName, a hidden 1024px canvas renders the same value with a four-module quiet zone. The button saves it as{downloadName}.png, adding the extension if you left it off. levelsets error correction: L recovers about 7% of a damaged code, M about 15% (the default), Q about 25% and H about 30%. Higher levels mean more, smaller modules at the same size.- The on-screen tile's 12px padding is the quiet zone. Keep other content from crowding the tile's edge.
Do and don't#
Content#
- Put one sentence beside the code that says what scanning does: Scan with your phone to open a chat with Cedarline on WhatsApp.
- Show the same link or key as text, in Geist Mono, with a copy button.
- Name downloads for what they are:
cedarline-whatsapp,where-is-my-payment-qr. - Keep the default button label, Download PNG, unless the format changes.
Accessibility#
- The SVG is
role="img"witharia-labelfromlabel. Name the destination: QR code for Cedarline on WhatsApp. - A code is unusable without a camera. Always offer the same link or secret as selectable text beside it.
- The hidden download canvas is
aria-hiddenanddisplay: none, so it's never announced. - The download button's icon is decorative; the label names the action.
| Keys | Action |
|---|---|
| Tab | Moves to the download button, if there is one. |
| Enter | Downloads the PNG. |
Design tokens#
| Token | Used for |
|---|---|
bg-white | The tile, fixed in both themes |
outline-black/10 | The tile edge in light |
outline-white/10 | The tile edge in dark |
--radius-xl | 12px tile corners |
API reference#
QRCode
A QR code tile with an optional download. Takes only the props below.
| Prop | Type | Default | Description |
|---|---|---|---|
valueRequired | string | No default | What the code encodes: a URL, tel: number or otpauth:// URI. |
size | number | 160 | Width and height of the code in pixels, before the tile's padding. |
label | string | "QR code" | Accessible name of the code image. |
downloadName | string | No default | File name for the PNG. Setting it shows the download button. |
downloadLabel | string | "Download PNG" | The download button's label. |
level | "L" | "M" | "Q" | "H" | "M" | Error correction level. |
className | string | No default | Merged onto the outer column. |
Known gaps#
Where the implementation and the system disagree today. Follow the system, not the gap.
The tile draws its edge with a CSS outline at 10% ink rather than the hairline lift. That's deliberate so the tile stays white in dark, but it's a different edge from every other raised surface.
The on-screen code has no margin of its own; its quiet zone is the tile's 12px padding, two to three modules for a typical link at the default size, where the QR spec asks for four. The downloaded PNG has the full four.
The default label is QR code. It would be safer as a required prop.
There's no feedback after a download; the browser's own download UI is the only confirmation.