QR Code
A scannable code for a link, guest Wi-Fi, a contact card, an event, an email, a phone number or text.
≈ 1,597 tokens per use (tool 1,223 · guidance 348 · typical payload 26)How token costs are estimated
Example: QR Code
Make a QR code for our menu
Tasca Velha menu
LinkWhen to use
- The user wants something to scan with another device or to print: a menu or sign-up link, guest Wi-Fi, a contact card for a badge, an event to add to calendars.
- The host's payment tool returned a payment request link and the user wants it as a code.
When not to use
- The user only needs the link on the device they're using. Send it as a link.
- The agent doesn't have the link, password, number or payment link from the user or a tool result. Ask; never invent them.
- The content is long (more than a few hundred characters). Share a file or a link instead.
- The content is a secret beyond guest Wi-Fi: account passwords, API keys, recovery codes.
How the agent should use it
- Pick the most specific type:
wifi,contact,event,emailorphonerather thantext, so the phone's camera offers the right action (join, save contact, add to calendar). - Pass links, Wi-Fi credentials, phone numbers and email addresses exactly as given. They are data-bound: they come from the user or a tool result, never from model free text. A payment request is only ever a
urlthe host's payment tool returned; the agent never builds or guesses one. - Give event date-times with their UTC offset (
2026-11-05T18:00:00+01:00); use a plain date for an all-day event. - Leave
errorCorrectionatM. UseQorHfor printed codes that may get scuffed,Lwhen the content is long. - Add a caption only when the title doesn't already say what scanning does, in a few words, e.g. "Scan to join". It's one quiet line under the code.
What gets encoded
The component builds the standard payload for each type, with the escaping each format needs:
type | Encodes | Example |
|---|---|---|
url | The link as given (http and https only) | https://menu.tascavelha.example |
text | The text, up to 900 characters | |
wifi | WIFI: string; \ ; , : " escaped | WIFI:T:WPA;S:Acme Guest;P:…;; |
contact | vCard 3.0, CRLF line endings | BEGIN:VCARD … END:VCARD |
event | iCalendar VEVENT, times in UTC | DTSTART:20261105T170000Z |
email | mailto: with an encoded subject and body | mailto:hello@acme.example?subject=… |
phone | tel: with digits and a leading + | tel:+15550100 |
Text is encoded as UTF-8 in byte mode, so accents, scripts and emoji scan correctly. The helpers live in qr-code-utils.ts (buildPayload, wifiPayload, vCardPayload, veventPayload, encodeQr, matrixToPath, …), pure functions you can reuse on the server.
Scanning and accessibility
- Always black on white, in dark mode too: scanners need the contrast. The quiet zone (4 modules) is part of the white tile.
- The code is one crisp SVG path, with
role="img"and a label such as "QR code for Wi-Fi network Acme Guest". - A readable summary sits under the code for people who can't scan: the link with an Open button, the Wi-Fi network and security with the password masked (a Show toggle and a Copy button), the contact's details, the event's local date and time. The content type (Link, Wi-Fi, …) is read to screen readers only.
- Nothing is encoded while the tool call streams, so the code never changes under a scanner. A skeleton holds its place.
- If the content doesn't fit at the chosen level, the component says so instead of drawing a code: "Too much data for a QR code at level H; shorten the text or use level L."
Actions
The footer has a closed set of actions: Download PNG (1024 px, quiet zone included, saved as qr-{title}.png), SVG (a vector download, named "Download SVG" for screen readers), and Copy (the encoded text, or the link for url). Each reports to onResult for analytics; nothing locks.
Installation
npx shadcn@latest add https://your-site.com/r/qr-code.json
Usage
The footer actions don't change what the agent does, so the tool can return immediately:
import { tool } from "ai"
import { qrCodeTool } from "@/components/genui/qr-code.schema"
const tools = {
[qrCodeTool.name]: tool({
description: qrCodeTool.description,
inputSchema: qrCodeTool.inputSchema,
execute: async () => ({ shown: true }),
}),
}Render it through the Renderer under show_qr_code, or directly. onResult reports which action the user took:
<QrCode
{...part.input}
status={part.state === "input-streaming" ? "streaming" : "ready"}
onResult={(r) => track("qr_code_action", r)} // { action: "download-png" | "download-svg" | "copy" }
/>The QR code never locks, so there's no finished state to restore from history.
With LangGraph
Push it from a graph node with ui.push({ name: "show_qr_code", props }, { message }), and render the thread's UI messages with the Renderer.
Schema
| Prop | Type | Notes |
|---|---|---|
title | string? | Defaults to the network, name, event or site |
content | QrContent | One of the types below |
errorCorrection | "L" | "M" | "Q" | "H" | Default M |
caption | string? | One line under the code |
size | "sm" | "md" | "lg" | 160, 220 or 280 px. Default md |
content.type | Fields |
|---|---|
url | url: http(s) only. Data-bound |
text | text: up to 900 characters |
wifi | ssid, password?, security (WPA, WEP, nopass), hidden?. Password required unless nopass. Data-bound |
contact | name, phone?, email?, org?, title?, url?. Data-bound |
event | title, start, end?, location?. ISO 8601 with offset, or plain dates for all-day (end inclusive) |
email | to, subject?, body?. Data-bound |
phone | number. Data-bound |
Host props: status (streaming or ready), onResult and className.