# QR Code

A crisp SVG QR code in dots, rounded or square modules. Renders on the server, and can draw itself in.

Docs: https://corsairui.vercel.app/docs/qr-code

## Install

```bash
pnpm dlx shadcn@latest add @corsair-ui/qr-code
```

Also adds: `utils`, `use-in-view`, `use-entrance`.
npm: `clsx`, `qrcode`, `tailwind-merge`.

## API

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` |  | The text or URL to encode. |
| `size` | `number` | `200` | Width and height in px. |
| `variant` | `"dots" \| "rounded" \| "squares"` | `"dots"` | Module style; corners stay solid. |
| `errorCorrection` | `"L" \| "M" \| "Q" \| "H"` | `"M"` | Damage it survives, 7% to 30%. |
| `margin` | `number` | `2` | Quiet zone, in modules. |
| `color / background` | `string` | `"#0b0b0c" / "#ffffff"` | Keep it dark on light. |
| `reveal / revealDuration` | `"load" \| "in-view" / number` | `— / 800` | Draw the code in with a circle that grows from the centre. The server HTML is the full code. |

## Styling

Restyle it in the project that installs it: `className` on each part, the theme variables, and these attributes with arbitrary variants (`[&_[data-slot=…]]:…`, `data-[state=…]:…`).

data-slot: `qr-code`, `qr-code-reveal`, `qr-code-reveal-shape`

State attributes:

- `data-state` (computed)

## Accessibility

- It is role="img", named after what it encodes; pass aria-label to describe it better.
- Put the link or text next to it too: not everyone can scan.

## Examples

On the docs page: Module styles, Colours and a wider quiet zone, Drawing itself in. The usage examples in the source comments below are the reference.

## Source

What `shadcn add` writes into the project:

### qr-code.tsx

```tsx
import { create } from "qrcode";
import type { ComponentProps, ReactNode } from "react";

// Encoding runs where the component renders; on the server that means no
// encoder in the browser bundle.

import { cn } from "@/registry/default/lib/utils";
import { QRCodeReveal } from "@/registry/default/ui/qr-code-reveal";

/** The three 7×7 corner squares scanners lock on to. */
function inFinder(row: number, column: number, size: number) {
  return (
    (row < 7 && column < 7) || (row < 7 && column >= size - 7) || (row >= size - 7 && column < 7)
  );
}

/** A rounded rectangle as path data, so a ring can be cut out with even-odd filling. */
function roundedRect(x: number, y: number, size: number, radius: number) {
  const r = Math.min(radius, size / 2);
  return `M${x + r} ${y}h${size - 2 * r}a${r} ${r} 0 0 1 ${r} ${r}v${size - 2 * r}a${r} ${r} 0 0 1 -${r} ${r}h-${size - 2 * r}a${r} ${r} 0 0 1 -${r} -${r}v-${size - 2 * r}a${r} ${r} 0 0 1 ${r} -${r}z`;
}

interface QRCodeProps extends Omit<ComponentProps<"svg">, "children"> {
  /** The text or URL to encode. */
  value: string;
  /** Width and height in px; the SVG scales cleanly beyond it. */
  size?: number;
  /** How the modules are drawn. Every style keeps the corner squares solid, so it scans. */
  variant?: "dots" | "rounded" | "squares";
  /**
   * How much damage the code survives: L 7%, M 15%, Q 25%, H 30%. Higher
   * levels make denser codes; use Q or H when something covers the middle.
   */
  errorCorrection?: "L" | "M" | "Q" | "H";
  /** Quiet zone around the code, in modules. Scanners want at least 2; the standard asks for 4. */
  margin?: number;
  /** Colour of the modules. Keep it dark on a light background: many scanners fail on inverted codes. */
  color?: string;
  /** Colour behind the modules. */
  background?: string;
  /**
   * Opt-in: the modules draw themselves from the centre outwards, on first
   * paint ("load") or when the code scrolls into view ("in-view"). The
   * background never animates, and with `prefers-reduced-motion` the code
   * simply shows. Leave it unset for a plain server-only image.
   */
  reveal?: "load" | "in-view";
  /** How long the reveal takes, in ms. */
  revealDuration?: number;
}

/**
 * A QR code drawn as one crisp SVG, from the `qrcode` encoder. It has no
 * state and no effects, so it renders on the server with no JavaScript in
 * the page. It is an image named after what it encodes; pass `aria-label`
 * to describe it better ("QR code to download the app").
 *
 * @example
 * <QRCode value="https://github.com/KevinGirelli/corsair-ui" size={180} />
 *
 * @example
 * <QRCode value="https://github.com/KevinGirelli/corsair-ui" reveal="in-view" />
 */
function QRCode({
  value,
  size = 200,
  variant = "dots",
  errorCorrection = "M",
  margin = 2,
  color = "#0b0b0c",
  background = "#ffffff",
  reveal,
  revealDuration = 800,
  className,
  "aria-label": label,
  ...props
}: QRCodeProps) {
  let modules: ReturnType<typeof create>["modules"];
  try {
    modules = create(value, { errorCorrectionLevel: errorCorrection }).modules;
  } catch {
    // Too much data for a QR code at this level.
    return null;
  }

  const count = modules.size;
  const quiet = Math.max(0, margin);
  const total = count + quiet * 2;
  const shapes: ReactNode[] = [];

  for (let row = 0; row < count; row++) {
    for (let column = 0; column < count; column++) {
      if (!modules.get(row, column) || inFinder(row, column, count)) continue;
      const x = column + quiet;
      const y = row + quiet;
      shapes.push(
        variant === "dots" ? (
          <circle key={`${row}-${column}`} cx={x + 0.5} cy={y + 0.5} r={0.4} />
        ) : (
          <rect
            key={`${row}-${column}`}
            x={x + (variant === "rounded" ? 0.05 : 0)}
            y={y + (variant === "rounded" ? 0.05 : 0)}
            width={variant === "rounded" ? 0.9 : 1}
            height={variant === "rounded" ? 0.9 : 1}
            rx={variant === "rounded" ? 0.3 : 0}
          />
        )
      );
    }
  }

  const corners = [
    [quiet, quiet],
    [quiet + count - 7, quiet],
    [quiet, quiet + count - 7],
  ] as const;
  const square = variant === "squares";
  const modulesGroup = (
    <g fill={color}>
      {corners.map(([x, y]) => (
        <g key={`${x}-${y}`}>
          {/* The ring: an outer square with the inner one cut out. */}
          <path
            fillRule="evenodd"
            d={`${roundedRect(x, y, 7, square ? 0 : 2.2)}${roundedRect(x + 1, y + 1, 5, square ? 0 : 1.4)}`}
          />
          <path d={roundedRect(x + 2, y + 2, 3, square ? 0 : 1)} />
        </g>
      ))}
      {shapes}
    </g>
  );

  return (
    <svg
      role="img"
      aria-label={label ?? `QR code: ${value}`}
      data-slot="qr-code"
      width={size}
      height={size}
      viewBox={`0 0 ${total} ${total}`}
      shapeRendering={square ? "crispEdges" : undefined}
      className={cn("block h-auto max-w-full", className)}
      {...props}
    >
      <rect width={total} height={total} rx={square ? 0 : Math.min(quiet, 2)} fill={background} />
      {reveal ? (
        <QRCodeReveal total={total} trigger={reveal} duration={revealDuration}>
          {modulesGroup}
        </QRCodeReveal>
      ) : (
        modulesGroup
      )}
    </svg>
  );
}

export { QRCode, type QRCodeProps };
```

### qr-code-reveal.tsx

```tsx
"use client";

import { useId, type ReactNode } from "react";

import { useEntrance, type EntranceTrigger } from "@/registry/default/hooks/use-entrance";
import { cn } from "@/registry/default/lib/utils";

interface QRCodeRevealProps {
  /** The dark modules to reveal. The background stays outside, so the quiet zone always shows. */
  children: ReactNode;
  /** Width and height of the code in viewBox units, quiet zone included. */
  total: number;
  /** "load" plays on first paint with CSS alone; "in-view" when the code scrolls into view. */
  trigger: EntranceTrigger;
  /** How long the reveal takes, in ms. */
  duration: number;
}

/**
 * The client half of `QRCode reveal`: masks the modules with a circle that
 * grows from the centre, so the code seems to draw itself. At rest the circle
 * covers every corner, so server HTML, reduced motion and the end of the
 * animation all show the complete, scannable code. `QRCode` renders it for
 * you when `reveal` is set.
 *
 * @example
 * <QRCodeReveal total={29} trigger="in-view" duration={800}>
 *   <g fill="currentColor">…</g>
 * </QRCodeReveal>
 */
function QRCodeReveal({ children, total, trigger, duration }: QRCodeRevealProps) {
  const [observe, phase] = useEntrance<SVGGElement>({ trigger, once: true });
  // useId can contain characters url(#…) does not accept.
  const maskId = `qr-code-reveal-${useId().replace(/[^\w-]/g, "")}`;
  const centre = total / 2;

  return (
    <g ref={observe} data-slot="qr-code-reveal" data-state={phase}>
      <defs>
        <mask id={maskId} maskUnits="userSpaceOnUse" x={0} y={0} width={total} height={total}>
          {/* Half the diagonal: the full circle reaches every corner. */}
          <circle
            data-slot="qr-code-reveal-shape"
            cx={centre}
            cy={centre}
            r={centre * Math.SQRT2}
            fill="white"
            className={cn(
              "origin-center [transform-box:fill-box]",
              phase !== "static" && "motion-safe:animate-qr-code-reveal"
            )}
            style={
              phase === "static"
                ? undefined
                : {
                    animationDuration: `${duration}ms`,
                    animationPlayState: phase === "armed" ? "paused" : undefined,
                  }
            }
          />
        </mask>
      </defs>
      <g mask={`url(#${maskId})`}>{children}</g>
    </g>
  );
}

export { QRCodeReveal, type QRCodeRevealProps };
```
