# QR Code (Moonarc)

A QR code for a URL or a short text, encoded at build and drawn as one SVG path inside the quiet zone a reader needs. Byte mode at error correction level M (15 % of the code can be lost and it still reads), versions 1 to 10, a value of up to 213 ASCII characters; the mask is picked by the four penalty rules of the specification, and a longer text stops the build naming its length. Dark modules on a light ground by default, on a dark page too, since not every camera reads a code drawn light on dark. A screen reader hears where the code leads. Zero JavaScript; nothing of the encoder reaches the browser.

- Import: `import QrCode from '@moonarc/core/QrCode'`
- Install: `npx astro add moonarc` · copy-paste: `npx shadcn@latest add https://moonarc.dev/r/qr-code.json`
- Tier A · category data · trigger always
- Readout: `<QrCode value="https://…" />`
- Browser support: widely (every browser)
- Measured cost: 0 B JS (CSS 4.5 kB raw)
- Page: https://moonarc.dev/components/qr-code/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` | `string` | none | What the code opens: a URL, usually. Up to 213 ASCII characters (a letter outside ASCII takes two to four of them); a longer value stops the build. |
| `size` | `string` | `'8rem'` | The code's width, any CSS length; it stays square. |
| `color` | `string` | `'#0c0c0d'` | The modules' colour. |
| `ground` | `string` | `'#fff'` | The ground under the modules, the quiet zone included. Keep it lighter than the modules. |
| `label` | `string` | none | What a screen reader hears; unset, "QR code to" and the value. |

## Usage

```astro
<QrCode value="https://apps.example.com/app/sitename" size="9rem" />
<QrCode value="https://example.com/download?ref=poster" label="Scan to download the app" ground="#fff" color="#1c1917" />
```

## Reduced motion

Nothing moves.

## With ClientRouter

Static SVG; nothing to rebind.

## Craft

- Encoded at build: the browser gets one path of rectangles, one per run of dark modules in a row, so a version 4 code is a few hundred bytes of markup and no script.
- Level M is the middle of the four error levels: enough to survive a scuffed print or a logo laid over a corner, without the larger code level H needs.
- The quiet zone is four modules, as the specification asks: a code cropped tighter is one many readers skip.
- The mask is chosen by the specification's penalty rules (runs, blocks, finder-like patterns, the balance of dark and light), so the code avoids patterns a reader confuses with its finders.
- Checked by decoding: every version from 1 to 10 was drawn and read back by a browser's barcode reader before it shipped.

## Replaces

- qrcode (npm) in a client bundle
- QR code image services
- react-qr-code

## Source

```astro
---
/**
 * QrCode — a QR code for a URL or a short text, encoded at build and drawn as one SVG path: the dark modules as runs
 * in each row, inside the four-module quiet zone a reader needs. Byte mode at error correction level M, versions 1 to
 * 10 (up to 213 ASCII characters); a longer text stops the build. Dark modules on a light ground by default, on a dark page too:
 * not every phone's camera reads a code drawn the other way round. It names what it opens, so a screen reader hears
 * where the code leads. Zero JavaScript, and nothing of the encoder reaches the browser.
 */
import type { HTMLAttributes } from 'astro/types';
import { qr, qrPath } from '../lib/qr';

interface Props extends HTMLAttributes<'div'> {
  /** What the code opens: a URL, usually. */
  value: string;
  /** The code's width, any CSS length. */
  size?: string;
  /** The modules' colour; unset, near black. */
  color?: string;
  /** The ground, the quiet zone included; unset, white. Keep the modules darker than the ground: a code drawn light on
   *  dark is one some cameras do not read. */
  ground?: string;
  /** What a screen reader hears; unset, "QR code to" and the value. */
  label?: string;
}

const { value, size = '8rem', color, ground, label, class: className, style, ...rest } = Astro.props;
const code = qr(value);
const quiet = 4;
const n = code.size + quiet * 2;
const vars = [`--ma-qr-size:${size}`, color && `--ma-qr-color:${color}`, ground && `--ma-qr-ground:${ground}`, typeof style === 'string' ? style : ''].filter(Boolean).join(';');
---

<div class:list={['ma-qr', className]} data-version={code.version} style={vars} {...rest}>
  <svg class="ma-qr__svg" viewBox={`0 0 ${n} ${n}`} role="img" aria-label={label ?? `QR code to ${value}`} shape-rendering="crispEdges">
    <rect class="ma-qr__ground" width={n} height={n} />
    <path class="ma-qr__modules" d={qrPath(code, quiet)} />
  </svg>
</div>

<style is:global>
  @layer components {
    :where(.ma-qr) { display: inline-block; inline-size: var(--ma-qr-size, 8rem); max-inline-size: 100%; line-height: 0; }
    :where(.ma-qr__svg) { display: block; inline-size: 100%; block-size: auto; }
    :where(.ma-qr__ground) { fill: var(--ma-qr-ground, #fff); }
    :where(.ma-qr__modules) { fill: var(--ma-qr-color, #0c0c0d); }
    @media (forced-colors: active) {
      :where(.ma-qr__ground) { fill: Canvas; }
      :where(.ma-qr__modules) { fill: CanvasText; }
    }
    /* nothing of its own moves; a transition a page puts on the code is dropped with reduced motion */
    @media (prefers-reduced-motion: reduce) {
      :where(.ma-qr) { transition: none; }
    }
  }
</style>

```
