Dots

QR Code

A scannable code with an optional center mark, an expired state and a copy-link fallback.

Payment linkhttps://example.com/pay/alex.morgan
"use client"

import {

The QrCode component shows a scannable code on a light tile, with an optional center mark, a caller-controlled expired state and a copy-link fallback for people who can't scan. Use it for a pay-me link, a payment request for a set amount or a code that verifies a statement. When the other person can't scan, such as on the same phone, the copy link is the way in, so keep it next to the code.

Installation

pnpm dlx shadcn@latest add @dots/qr-code

Usage

import {
  QrCode,
  QrCodeCopyLink,
  QrCodeImage,
} from "@/components/ui/qr-code"
<QrCode value="https://example.com/pay/alex.morgan">
  <QrCodeImage label="QR code to pay Alex Morgan" />
  <QrCodeCopyLink label="Payment link" />
</QrCode>

Composition

Use the following composition to build a QR code:

QrCode
├── QrCodeImage
│   └── QrCodeOverlay
└── QrCodeCopyLink

Features

  • The tile stays light with dark modules in both themes, and its padding is the quiet zone
  • An optional center mark, such as an avatar, that switches error correction to H so the code still reads
  • Error correction steps down on its own when the value is too long for the level, so long values never break the render
  • An expired state that fades the code, hides it from assistive tech and announces the change once
  • A copy-link fallback that copies the same value and goes inert while the code is expired
  • Three sizes for a document, a screen and a share sheet

Mark

Pass mark to QrCodeImage to put a small decorative mark over the center, such as the person's avatar. The mark is hidden from assistive tech, so the label still says what scanning does.

Scan to pay Sam Rivera
"use client"

import { EntityAvatar } from "@/components/ui/entity-avatar"

Expired

Set expired on QrCode when the code stops working, such as a payment request past its time. Put a QrCodeOverlay inside QrCodeImage to show what happened. The component runs no timers: you decide when a code expires and when to make a new one.

Code expired
Payment linkhttps://example.com/pay/r/8F3K2Q?amount=25.00&v=1
"use client"

import * as React from "react"

Keep the control that makes a new code outside the overlay, so focus stays on it when the overlay goes away.

Sizes

Use size on QrCodeImage to switch between sm for a printed document, default and lg for a share sheet.

"use client"

import { QrCode, QrCodeImage } from "@/components/ui/qr-code"

Accessibility

QrCodeImage renders the code as an SVG with role="img". Everything else is plain markup and the CopyField button.

Label the image

label is required. Describe what scanning does, not what the image looks like.

<QrCodeImage label="QR code to pay Alex Morgan" />

Announce expiry

When expired turns on, a polite live region reads expiredLabel once, and the code is hidden from assistive tech. The overlay text can repeat the same words with aria-hidden="true" so they aren't read twice.

<QrCode value={value} expired expiredLabel="Code expired">
  <QrCodeImage label="QR code to pay $25.00">
    <QrCodeOverlay>
      <span aria-hidden="true">Code expired</span>
    </QrCodeOverlay>
  </QrCodeImage>
</QrCode>

Keep a way in

People who can't scan, such as screen reader users on the same phone, need the value itself. Keep a QrCodeCopyLink next to the code. It names its copy button from label, such as "Copy payment link".

Contrast

The tile stays light in dark mode on purpose: many scanners can't read inverted codes.

API Reference

QrCode

The root. It shares the value and the expired state with its parts. Accepts all div props.

PropTypeDefaultDescription
valuestring-What the code encodes, usually a link. The copy link copies the same string.
expiredbooleanfalseThe code no longer works. Fades the code and hides it from assistive tech.
expiredLabelstring"Code expired"Announced once, politely, when the code expires.
classNamestring-Additional classes to apply to the root element.

QrCodeImage

The code on a light tile. Accepts all div props except aria-label.

PropTypeDefaultDescription
labelstring-What scanning does, such as "QR code to pay Alex Morgan". Required.
markReactNode-A small decorative mark over the center. Switches error correction to "H".
level"L" | "M" | "Q" | "H""H" with a mark, else "M"Error correction. Steps down when the value is too long for the level.
size"sm" | "default" | "lg""default"The tile size.
classNamestring-Additional classes to apply to the tile.

QrCodeOverlay

Content over the code while it is expired. Accepts all div props.

PropTypeDefaultDescription
classNamestring-Additional classes to apply to the overlay.

Copies the encoded value. Renders a CopyField and accepts its props except value, which comes from QrCode; label is optional here. Dimmed and inert while the code is expired. See Copy Field for the rest of the props.

PropTypeDefaultDescription
labelstring"Link"What the value is. Also names the copy button.
classNamestring-Additional classes to apply to the copy field.