A browser window around a screenshot or any content: the three lights, an address bar with a lock, optional tabs with one active.
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.
Adjust · props are custom properties, set inline, so you edit the running instance
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
every browser.
Install
One command adds the integration, the base tokens and every component. Then import what you use.
npx astro add moonarcpnpm astro add moonarcbunx astro add moonarc---
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.
npx shadcn@latest add https://moonarc.dev/r/qr-code.jsonThe 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 — 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
| 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. |
Reduced motion
Nothing moves.
With ClientRouter
Static SVG; nothing to rebind.
Why it is built this way
- 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. See the migration table.
Related
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.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown