Reference
Rune documentation.
Getting started
Rune is a framework-agnostic QR code library. Install the adapter for your framework, or the core for a plain SVG string that works anywhere.
pnpm add @kroszborg/rune-reactpnpm add @kroszborg/rune-vuepnpm add @kroszborg/rune-wcpnpm add @kroszborg/rune # core / SSR string APIReact:
import { QRCode } from '@kroszborg/rune-react';
export default function App() {
return (
<QRCode
value="https://example.com"
dots={{ style: 'rounded' }}
corners={{ square: { style: 'extra-rounded' } }}
/>
);
}Anywhere (server, edge, Node, workers):
import { toSVGString } from '@kroszborg/rune';
const svg = toSVGString({ value: 'https://example.com', dots: { style: 'rounded' } });Every package (below) accepts the exact same RuneOptions object.
| Package | What it is |
|---|---|
@kroszborg/rune | Core: encoder, renderer, exports, data builders, presets. Zero runtime deps. |
@kroszborg/rune-react | React <QRCode> (web). Also the base for React Native. |
@kroszborg/rune-vue | Vue 3 <QRCode>. |
@kroszborg/rune-wc | Vanilla renderRune() + a <rune-qr> Web Component. |
@kroszborg/rune-decode | Decoder: read a QR back from a matrix or image. |
@kroszborg/rune-cli | The rune CLI: generate or decode from the terminal. |
RuneOptions
The single options object accepted by every renderer and adapter.
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | - | Data to encode (required). |
size | number | 256 | Output width/height in px. |
margin | number | 4 | Quiet zone in modules. |
dots | { style, color, gradient } | - | Data-module style and fill. |
corners | CornerOptions | - | Finder ring + core styles and fills. |
background | string | BackgroundOptions | '#ffffff' | Color, gradient, or image. 'transparent' allowed. |
logo | LogoOptions | - | Center logo, image or bare plate; picks the lowest safe ECL. |
frame | FrameOptions | - | Outer frame + CTA text. |
qr | { errorCorrectionLevel, version, mask, eci, boostEcl, kanji } | - | Encoding controls. ECL accepts 'H', 'h' or 'high'; eci adds a UTF-8 header; kanji (default on) packs Japanese text at 13 bits per character. |
preset | PresetName | - | Named base style; explicit options win. |
ariaLabel | string | see note | Accessible label. Defaults to 'QR code: {value}' for short http(s) URLs and plain 'QR code' otherwise, so WiFi passwords are never read aloud. |
title | string | - | <title> element (tooltip) inside the SVG. |
idPrefix | string | hash | Namespace for gradient ids when identical codes share a page. |
Every option is validated before anything is drawn: an unknown style or preset name, a non-finite size, a negative margin, an empty gradient or a bad ECL throws a RangeError that names the option, rather than producing a broken SVG. validateOptions() is exported for checking ahead of time. Keys whose value is undefined are ignored, so a framework component forwarding every prop never overrides a preset.
Dot styles
Set with dots.style. Nine styles; rounded and extra-rounded connect adjacent modules for a fluid look, and leaf gives each module a petal shape.
<QRCode value="..." dots={{ style: 'classy-rounded' }} />Finder styles
The three corner markers. corners.square styles the 7×7 ring; corners.dot styles the inner 3×3 block. Each takes its own color or gradient.
Ring (corners.square.style)
Core (corners.dot.style)
| Field | Type |
|---|---|
corners.square.style | 'square' | 'rounded' | 'extra-rounded' | 'circle' | 'leaf' |
corners.square.color | string |
corners.square.gradient | Gradient |
corners.dot.style | 'square' | 'rounded' | 'dot' |
corners.dot.color / gradient | string | Gradient |
corners.alignment.style | 'square' | 'rounded' | 'circle' | 'inherit' |
corners.alignment.color / gradient | string | Gradient |
corners.topLeft / topRight / bottomLeft | { square?, dot? } |
Each finder can be styled on its own: corners.topLeft, corners.topRight and corners.bottomLeft take the same square / dot shape and fill options and override the shared ones for that corner only.
<QRCode value="..." corners={{
square: { style: 'rounded' },
topLeft: { square: { style: 'circle', color: '#0b6b4f' }, dot: { style: 'dot' } },
bottomLeft: { dot: { color: '#7a1f3d' } },
}} />Alignment patterns (versions 2 and up) are drawn as solid shapes by default rather than in the dot style, because decoders locate them to correct perspective and a star-shaped alignment pattern is the first thing to fail on a print. Pass 'inherit' to style them like the data anyway.
Colors & gradients
Every element (dots, each finder part, background) takes a solid color or a gradient. A gradient is linear or radial with any number of stops.
| Gradient field | Type | Notes |
|---|---|---|
type | 'linear' | 'radial' | |
rotation | number | Degrees, linear only. |
stops | { offset, color }[] | offset is 0-1. |
dots={{ gradient: {
type: 'linear', rotation: 45,
stops: [{ offset: 0, color: '#0f7a5c' }, { offset: 1, color: '#0b6b4f' }],
} }}Background
Pass a color string, or a BackgroundOptions object for a gradient or image. 'transparent' is accepted (useful for overlaying).
| Field | Type | Description |
|---|---|---|
color | string | Solid fill, or 'transparent'. |
gradient | Gradient | Gradient fill behind the modules. |
image | string | Image URL rendered behind the modules. |
background="transparent"
// or
background={{ gradient: { type: 'radial', stops: [...] } }}Logo
A center logo. Rune automatically raises the error-correction level to H when a logo is present (unless you set one) and clamps the logo size to what stays scannable at that level, so you cannot accidentally produce an unreadable code.
| Field | Type | Default | Description |
|---|---|---|---|
src | string | - | https, blob:, data:image/…, or a path. |
size | number | 0.25 | Fraction of width, up to 0.3. Shrunk only if even ECL H cannot absorb it. |
margin | number | 1 | Clear space around the logo, in modules. |
hideDots | boolean | true | Clear the modules behind the logo. |
shape | 'square' | 'rounded' | 'circle' | 'square' | Backing plate shape. |
background | string | bg color | Backing plate color. |
clamp | boolean | true | Shrink the logo when it exceeds the safe damage budget. |
<QRCode value="..." logo={{ src: '/logo.png', size: 0.2, shape: 'circle' }} />Rune never forces ECL H for a logo, which would make the code denser and harder to scan. It counts exactly how many codewords the logo hides in each Reed-Solomon block and steps the level up M, Q, H only as far as that damage needs, keeping 30% of the correction capacity for real-world wear. If even H cannot absorb the logo (small symbols have little redundancy) the logo is shrunk to the largest safe size. Set qr.errorCorrectionLevel to pin a level instead.
Omit src to get a bare cleared plate, which is what the React logoElement prop and the Vue default slot use to overlay a live node (see Frameworks). renderToParts().logo reports the cleared box.
Frame & CTA
An outer frame with an optional call-to-action label.
| Field | Type | Description |
|---|---|---|
style | 'none' | 'square' | 'rounded' | Border shape. 'none' draws text only. |
text | string | CTA label, e.g. "SCAN ME". |
position | 'bottom' | 'top' | Where the text band sits. |
color / textColor | string | Frame and text colors. |
font | string | CSS font-family for the label. |
Error correction
Higher levels recover from more damage but hold less data. Set with qr.errorCorrectionLevel. You can also pin the version (1-40) and mask (0-7); both are chosen automatically by default. Mixed payloads are split into numeric, alphanumeric, byte and Kanji segments for the smallest symbol (Kanji mode uses the runtime's own Shift_JIS decoder, so no table ships in the bundle), and an empty value throws rather than rendering a blank code.
| Level | Recovery | Use when |
|---|---|---|
L | ~7% | Clean environments, minimal data. |
M | ~15% | General purpose (default). |
Q | ~25% | Print, harsher conditions. |
H | ~30% | Large logos, heavy wear. |
Presets
Named starting styles. Pass preset; any explicit option you also set wins over the preset.
<QRCode value="..." preset="mint" />Data builders
Turn structured input into correctly-escaped QR content. Import with import { data } from '@kroszborg/rune' and pass the result as value.
data.url('example.com')
data.wifi({ ssid: 'Home', password: 'hunter2', encryption: 'WPA' })
data.email({ to: 'a@b.com', subject: 'Hi', body: '...' })
data.sms({ to: '+15551234', body: 'hi' })
data.tel('+15551234')
data.geo({ lat: 26.9124, lng: 75.7873 })
data.vcard({ fullName: 'Ada Lovelace', email: 'ada@x.com', phone: '+1555' })
data.mecard({ fullName: 'Ada Lovelace', phone: '+1555' })
data.event({ title: 'Launch', start: '2026-07-14T09:00Z', end: '2026-07-14T10:00Z' })
data.crypto({ coin: 'bitcoin', address: '1abc', amount: 0.5 })Export
The core exports SVG anywhere; raster and PDF live in @kroszborg/rune/node so the browser bundle stays free of native binaries.
| Function | Returns | Where |
|---|---|---|
toSVGString(o) | string | anywhere (sync) |
renderToParts(o) | SvgParts | anywhere (adapters) |
toDataURL(o, r?) | Promise<string> | browser (Canvas) |
toBuffer(o, r?) | Promise<Uint8Array> | node (/node) |
toPDF(o, p?) | Promise<Uint8Array> | node (/node) |
RasterOptions: format ('png' | 'jpeg' | 'webp'), quality (0-1), scale (pixel density; defaults to devicePixelRatio in the browser and 1 in Node), background (JPEG only; PNG and WebP keep alpha) and inlineRemoteImages (default true: http(s) logo and background images are fetched and embedded, because neither Canvas nor resvg loads remote images). toPDF renders at 4x for print and takes page and printSize in points. Out-of-range values throw a RangeError.
import { toBuffer, toPDF } from '@kroszborg/rune/node';
const png = await toBuffer({ value: '...', size: 512 }, { format: 'png' });
const pdf = await toPDF({ value: '...', size: 512 });Decoding
@kroszborg/rune-decode reads a QR back into its data, with full Reed-Solomon error correction.
import { decode, decodeMatrix } from '@kroszborg/rune-decode';
// From raw RGBA pixels (e.g. a canvas ImageData)
const result = decode({ data, width, height });
if (result) console.log(result.text, result.version, result.ecl);
// From a boolean module matrix
const { text } = decodeMatrix(modules); // modules[y][x], true = dark| Function | Input | Returns |
|---|---|---|
decode(img) | { data, width, height } | MatrixDecodeResult | null |
decodeMatrix(m) | boolean[][] | MatrixDecodeResult |
MatrixDecodeResult = { text, version, ecl, mask }.
Supported: versions 1-40, every ECL and mask, numeric / alphanumeric / byte / Kanji modes, multi-segment payloads, ECI charsets, Structured Append and FNC1 headers (reported on the result), format-information BCH correction and Reed-Solomon correction up to half the block's ECC codewords. The image path binarizes with a local, shadow-tolerant threshold (falling back to a global one), locates the finders, refines the grid through the bottom-right alignment pattern into a perspective transform, samples each module by majority vote, and retries mirrored (the result carries mirrored: true). Rotated, tilted, unevenly lit, transparent and front-camera images all decode; very low resolution (under about 3 px per module) and heavy blur remain the domain of a camera-grade decoder.
CLI
@kroszborg/rune-cli generates and decodes from the terminal.
# generate
rune "https://example.com" -o qr.png --dots rounded --preset mint
rune "https://example.com" --gradient "#0f7a5c,#12946e,45" -o qr.pdf
rune "SCAN ME" --frame "SCAN ME" -o cta.svg # no -o prints SVG to stdout
# decode
rune decode qr.png| Flag | Description |
|---|---|
-o, --out | Output file (.svg .png .jpeg .webp .pdf). Stdout SVG if omitted. |
-s, --size / -m, --margin | Size in px / quiet-zone modules. |
--ecl | L | M | Q | H (any case, or low/medium/quartile/high). |
--dots / --dot-color | Dot style (all nine) / color. |
--gradient "a,b,r" | Linear gradient from,to,rotation. |
--square / --core / --alignment | Finder ring / finder core / alignment-pattern style. |
--bg / --preset | Background color, preset. |
--frame / --frame-style / --frame-color | CTA text, border style (none | square | rounded), color. |
--logo / --logo-size / --logo-shape / --no-clamp | Logo path, URL or data URI (local files are inlined), size, plate shape. |
--scale / --quality | Raster pixel density (PDF default 4) and JPEG/WebP quality. |
--eci / --qr-version / --mask / --title / --aria-label | Encoding and accessibility controls. |
- / --stdin / -- | Read the value from stdin; treat everything after -- as the value. |
Unknown flags and bad values exit with code 1 before anything is written, so a typo can never end up encoded in the QR. -o - streams the file to stdout.
Frameworks
React
Accepts every RuneOptions prop, plus style, className, and logoElement: a React node used as the logo. The core clears the modules behind it and picks the error-correction level exactly as for logo.src. Size, margin, shape and plate color come from logo. Re-renders are skipped while the options are structurally unchanged, so inline objects are fine. Any other prop (id, onClick, data-*, tabIndex) is forwarded to the <svg>; an empty value renders nothing, so controlled inputs can start blank.
import { QRCode } from '@kroszborg/rune-react';
<QRCode value="..." dots={{ style: 'rounded' }} logo={{ size: 0.2 }} logoElement={<MyLogo />} />Vue 3
The default slot is the logo, with the same plate and ECL handling as React.
<script setup>
import { QRCode } from '@kroszborg/rune-vue';
</script>
<template>
<QRCode value="..." :dots="{ style: 'rounded' }" />
<QRCode value="..." :logo="{ size: 0.2 }"><img src="/logo.svg" /></QRCode>
</template>Web Component (vanilla)
Attributes cover the common options: value, size, margin, preset, dot-style, dot-color, corner-style, corner-dot-style, corner-color, alignment-style, background, ecl, logo, logo-size, frame-style, frame-text, frame-color, aria-label, title. Gradients, background images and everything else go through the .options property. Renders are coalesced per microtask and skipped while disconnected; invalid input clears the element and dispatches a rune-error event instead of throwing.
import { register } from '@kroszborg/rune-wc';
register();
// <rune-qr value="..." dot-style="rounded" corner-style="extra-rounded" frame-text="SCAN ME"></rune-qr>
// el.options = { value: '...', dots: { gradient: {...} }, frame: { text: 'SCAN ME' } };React Native
import { SvgXml } from 'react-native-svg';
import { toSVGString } from '@kroszborg/rune';
const svg = toSVGString({ value: '...', dots: { style: 'rounded' } });
export default () => <SvgXml xml={svg} width={256} height={256} />;Svelte, Solid, Astro, Angular, Lit
No dedicated package needed - inject the string from toSVGString, or use the <rune-qr> Web Component.
const svg = toSVGString({ value: '...', dots: { style: 'rounded' } });
{@html svg} // Svelte
<div innerHTML={svg} /> // Solid
<Fragment set:html={svg} /> // Astro
<div [innerHTML]="svg"> // Angular (sanitizer-bypassed)
<rune-qr value="..."></rune-qr> // Lit / Alpine / plain HTML