Skip to content

Components / Data

QR Code

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.

Live demo

Point a phone's camera at it: it opens this page.

Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.

Measured

JavaScript of its own

0 B

CSS 4.5 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.

Browser support

Baseline · widely available

every browser.

Install

One command adds the integration, the base tokens and every component. Then import what you use.

npx astro add moonarc
pnpm astro add moonarc
bunx astro add moonarc
src/pages/index.astro
---
import QrCode from '@moonarc/core/QrCode';
---
<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" />
Copy it into your project instead (shadcn registry)

Owns the file, no dependency. The registry item also installs QR encoder and the base tokens.

terminal
npx shadcn@latest add https://moonarc.dev/r/qr-code.json

The CLI needs a components.json and the @/* alias, which Setup has. The file lands in src/components/moonarc/.

Source

The whole component. Self-contained styles in a cascade layer so your classes always win. If you paste it, also copy qr.ts.

QrCode.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>

Props

PropTypeDefaultDescription
valuestringnoneWhat 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.
sizestring'8rem'The code's width, any CSS length; it stays square.
colorstring'#0c0c0d'The modules' colour.
groundstring'#fff'The ground under the modules, the quiet zone included. Keep it lighter than the modules.
labelstringnoneWhat a screen reader hears; unset, "QR code to" and the value.

Reduced motion

Nothing moves.

With ClientRouter

Static SVG; nothing to rebind.

Why it is built this way

Replaces: qrcode (npm) in a client bundle · QR code image services · react-qr-code. See the migration table.

A browser window around a screenshot or any content: the three lights, an address bar with a lock, optional tabs with one active.

every browseralways

A generic phone frame around a screenshot or any content, with three cut-outs (island, notch, punch), a home bar, and no logo, buttons or brand.

every browseralways

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown