Display

QR Code

A tokenized QR code renderer — encodes any string to SVG and inherits `currentColor`, so it auto-inverts in dark mode.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import QRCode from "qrcode"
import { cn } from "@/lib/utils"

export interface QrCodeProps extends React.HTMLAttributes<HTMLDivElement> {
  /** Content to encode — a URL, payment payload, or plain text. */
  value: string
  /** Rendered box size in px (square, safe-zone padding included). */
  size?: number
  /** Error-correction level: higher survives more damage/overlay at the cost of density. */
  level?: "L" | "M" | "Q" | "H"
}

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/qr-code.json

Prompt

The prompt behind this component — paste it into your AI assistant to recreate or adapt it.

Build a React + TypeScript + Tailwind "QrCode" component using the `qrcode`
npm package to generate SVG markup client-side.

Contract
- Export a forwardRef<HTMLDivElement, QrCodeProps> extending
  React.HTMLAttributes<HTMLDivElement>.
- Props: value: string (required — the text/URL to encode), size?: number
  (px, default 160, the full rendered box including safe-zone padding),
  level?: "L" | "M" | "Q" | "H" (error-correction level, default "M"),
  className.

Behavior
- Generate asynchronously with QRCode.toString(value, { type: "svg",
  errorCorrectionLevel: level, margin: 0 }) inside a useEffect keyed on
  [value, level]; resolve into state via .then, never setState synchronously
  in the effect body itself.
- Guard against stale writes: flip a local `cancelled` flag true in the
  effect's cleanup so a fast value/level change can't let an older pending
  promise overwrite a newer result.
- On generation failure (value too long for the encodable capacity —
  qrcode's promise rejects), catch it and render a small error placeholder
  instead of a blank box; never let the rejection throw past the component.
- Re-generate whenever value or level changes — there is no caching, each
  new value is a fresh async encode.

Rendering & styling
- Root: rounded-lg border bg-background p-3 text-foreground, fixed
  width/height = size (the padding is the QR safe-zone, not additive) —
  merge className via cn().
- Color tokenization (the differentiator vs a plain <img> QR): qrcode's SVG
  renderer always emits exactly two hex colors — #000000 stroke for the dark
  modules, #ffffff fill for the light background rect. String-replace
  #000000 → currentColor and #ffffff → transparent before injecting, so the
  mark inherits text-foreground (or whatever text-* an ancestor sets) and
  the background rect disappears in favor of the container's own bg — dark
  mode and custom accent colors work with zero extra color props.
- Inject the tokenized SVG through a single aria-hidden wrapper div with
  dangerouslySetInnerHTML, sized [&_svg]:size-full so it fills the padded box.
- Accessibility: role="img" and aria-label={`QR code for ${value}`} on the
  root container itself — never on the injected SVG, which stays aria-hidden.

Customization levers
- Fixed black/white for print or physical scanning: skip the tokenization
  step (keep the raw #000000/#ffffff) when the code will be printed or
  scanned against an arbitrary surface, where theme-matching contrast can't
  be guaranteed to stay scannable.
- Center logo overlay: absolutely position a small <img>/<div> at the
  container's center once level is "Q" or "H" — higher error correction
  tolerates the obscured center modules; keep the logo under ~20% of size
  or the symbol stops decoding.
- Download as PNG: swap toString(value, { type: "svg" }) for
  QRCode.toDataURL(value, { errorCorrectionLevel: level }) to get a raster
  PNG data URI, then wire it to an <a download href={dataUrl}> button — a
  separate code path from the tokenized SVG preview, since a rasterized PNG
  can't inherit currentColor.
- Density/robustness trade-off: level walks L → M → Q → H for more damage
  tolerance at the cost of a denser pattern — pair higher levels with a
  larger size so individual modules stay scannable.

Concepts

  • SVG currentColor tokenization — the generated markup only ever contains two hex colors; string-replacing them lets the mark inherit text-foreground and the container supply its own background, so dark mode and brand accents need zero extra props.
  • Async generate-to-state — encoding runs in QRCode.toString(...).then(...) inside an effect, never a synchronous setState in the effect body.
  • Stale-response guard — a cancelled flag set in the effect cleanup stops an in-flight promise from a previous value/level from overwriting a newer result.
  • Error-correction trade-offlevel (LH) trades pattern density for tolerance to damage or a center logo overlay; it is not a visual style knob.
  • Safe-zone via padding — the quiet zone a scanner needs around the mark comes from the container's own p-3, not from the encoder's margin option, so it composes with size predictably.

On This Page