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-react
$pnpm add @kroszborg/rune-vue
$pnpm add @kroszborg/rune-wc
$pnpm add @kroszborg/rune # core / SSR string API

React:

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.

PackageWhat it is
@kroszborg/runeCore: encoder, renderer, exports, data builders, presets. Zero runtime deps.
@kroszborg/rune-reactReact <QRCode> (web). Also the base for React Native.
@kroszborg/rune-vueVue 3 <QRCode>.
@kroszborg/rune-wcVanilla renderRune() + a <rune-qr> Web Component.
@kroszborg/rune-decodeDecoder: read a QR back from a matrix or image.
@kroszborg/rune-cliThe rune CLI: generate or decode from the terminal.

RuneOptions

The single options object accepted by every renderer and adapter.

PropTypeDefaultDescription
valuestring-Data to encode (required).
sizenumber256Output width/height in px.
marginnumber4Quiet zone in modules.
dots{ style, color, gradient }-Data-module style and fill.
cornersCornerOptions-Finder ring + core styles and fills.
backgroundstring | BackgroundOptions'#ffffff'Color, gradient, or image. 'transparent' allowed.
logoLogoOptions-Center logo, image or bare plate; picks the lowest safe ECL.
frameFrameOptions-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.
presetPresetName-Named base style; explicit options win.
ariaLabelstringsee noteAccessible label. Defaults to 'QR code: {value}' for short http(s) URLs and plain 'QR code' otherwise, so WiFi passwords are never read aloud.
titlestring-<title> element (tooltip) inside the SVG.
idPrefixstringhashNamespace 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.

square
dot
rounded
extra-rounded
classy
classy-rounded
leaf
diamond
star
<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)

square
rounded
extra-rounded
circle
leaf

Core (corners.dot.style)

square
rounded
dot
FieldType
corners.square.style'square' | 'rounded' | 'extra-rounded' | 'circle' | 'leaf'
corners.square.colorstring
corners.square.gradientGradient
corners.dot.style'square' | 'rounded' | 'dot'
corners.dot.color / gradientstring | Gradient
corners.alignment.style'square' | 'rounded' | 'circle' | 'inherit'
corners.alignment.color / gradientstring | 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.

solid
linear
radial
Gradient fieldTypeNotes
type'linear' | 'radial'
rotationnumberDegrees, 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).

FieldTypeDescription
colorstringSolid fill, or 'transparent'.
gradientGradientGradient fill behind the modules.
imagestringImage URL rendered behind the modules.
background="transparent"
// or
background={{ gradient: { type: 'radial', stops: [...] } }}

Frame & CTA

An outer frame with an optional call-to-action label.

SCAN ME
FieldTypeDescription
style'none' | 'square' | 'rounded'Border shape. 'none' draws text only.
textstringCTA label, e.g. "SCAN ME".
position'bottom' | 'top'Where the text band sits.
color / textColorstringFrame and text colors.
fontstringCSS 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.

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

minimal
dots
fluid
mint
midnight
sunset
<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.

FunctionReturnsWhere
toSVGString(o)stringanywhere (sync)
renderToParts(o)SvgPartsanywhere (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
FunctionInputReturns
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
FlagDescription
-o, --outOutput file (.svg .png .jpeg .webp .pdf). Stdout SVG if omitted.
-s, --size / -m, --marginSize in px / quiet-zone modules.
--eclL | M | Q | H (any case, or low/medium/quartile/high).
--dots / --dot-colorDot style (all nine) / color.
--gradient "a,b,r"Linear gradient from,to,rotation.
--square / --core / --alignmentFinder ring / finder core / alignment-pattern style.
--bg / --presetBackground color, preset.
--frame / --frame-style / --frame-colorCTA text, border style (none | square | rounded), color.
--logo / --logo-size / --logo-shape / --no-clampLogo path, URL or data URI (local files are inlined), size, plate shape.
--scale / --qualityRaster pixel density (PDF default 4) and JPEG/WebP quality.
--eci / --qr-version / --mask / --title / --aria-labelEncoding 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