npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

velvet-qr

v1.5.1

Published

The easiest, most customizable, developer-friendly QR code component for React. Gradients, dot & eye styles, logos, and PNG/SVG/JPEG export — built for React 18 & 19.

Readme

Velvet QR

Generate beautiful QR codes effortlessly.

Part of the Velvet ecosystem of React developer libraries — velvet-qr is the easiest, most customizable, developer-friendly QR code component for React. Gradients, dot & eye styles, logos, and PNG/SVG/JPEG export, all fully typed.

CI npm version npm downloads GitHub stars minzipped size license Sponsor

Live demo & docs →


Features

  • 🎨 Fully customizable — colors, gradients, dot styles, eye styles, rounded corners, transparent backgrounds
  • 🧵 Connected dot rendering — "rounded"/"dots"/"classy"/"classy-rounded"/"extra-rounded" styles merge adjacent modules into a smooth connected shape, not isolated blobs
  • 🖼️ Logo embedding — center a logo with configurable size, padding, and border radius, with automatic module excavation for scannability
  • 🧩 PresetsQR_PRESETS + QRPresetPicker for ready-made styles (gradient "Velvet" look, dark-mode "Midnight", and more)
  • 🗂️ Batch generationuseQRBatch / QRBatchGenerator for generating and bulk-downloading many QR codes at once
  • 🏷️ Frame / label templatesframe prop bakes a "SCAN ME"-style banner or bordered label directly into the exported SVG/PNG/JPEG, so it survives print/flat-image sharing
  • 📷 QR scanneruseQRScanner / QRScanner decode QR codes from a live camera or a static image, using the browser-native BarcodeDetector API where available and transparently falling back to the bundled jsQR decoder everywhere else — works in Chrome, Edge, Firefox, and Safari alike
  • 🕓 Local QR historyuseQRHistory / QRHistoryPanel remember recently generated configs in localStorage for quick re-use
  • 📊 Opt-in local analyticsuseQRAnalytics tracks generate/download/copy/in-session-scan events you observe in your own page — see the honesty note below, it cannot see real-world camera scans
  • 🔗 Dynamic QR helpersbuildDynamicQrValue / useDynamicQr help you build a QR that points at a stable URL under your own redirect endpoint, so the destination can change after printing (requires your own backend — see below)
  • 📦 Built-in content builders — URL, plain text, email, phone, SMS, WiFi, vCard, and Google Maps/geo location
  • ⬇️ One-line exports — download as PNG, SVG, or JPEG, or copy the image straight to the clipboard
  • 🪝 Hooks-first APIuseQRGenerator, useQRDownload, and useQRBatch for full control when you don't want the batteries-included components
  • 🔒 Strongly typed — complete TypeScript definitions, no any
  • Tiny & tree-shakeable — two runtime dependencies (qrcode for matrix encoding, jsqr for the scanner's cross-browser fallback decoding), ESM + CJS builds, "use client" banner for Next.js App Router
  • Accessible — proper role="img" / aria-label on generated SVGs, including an accessible placeholder if value is empty or generation fails

Installation

npm install velvet-qr
# or
yarn add velvet-qr
# or
pnpm add velvet-qr

Requires React 18 or 19.

Quick Start

import { VelvetQR } from "velvet-qr";

export default function Example() {
  return (
    <VelvetQR
      value="https://poojagohel.github.io/velvet-qr/"
      size={250}
      foregroundColor="#000"
      backgroundColor="#fff"
    />
  );
}

Downloading & copying

VelvetQR exposes an imperative handle via ref — no extra wiring required:

import { useRef } from "react";
import { VelvetQR, type VelvetQRHandle } from "velvet-qr";

function DownloadableQR() {
  const qrRef = useRef<VelvetQRHandle>(null);

  return (
    <>
      <VelvetQR ref={qrRef} value="https://poojagohel.github.io/velvet-qr/" size={250} />
      <button onClick={() => qrRef.current?.downloadPNG()}>Download PNG</button>
      <button onClick={() => qrRef.current?.downloadSVG()}>Download SVG</button>
      <button onClick={() => qrRef.current?.downloadJPEG()}>Download JPEG</button>
      <button onClick={() => qrRef.current?.copyToClipboard()}>Copy image</button>
    </>
  );
}

Or use the ready-made toolbar:

import { useRef } from "react";
import { VelvetQR, QRDownloader, type VelvetQRHandle } from "velvet-qr";

function Example() {
  const qrRef = useRef<VelvetQRHandle>(null);
  return (
    <>
      <VelvetQR ref={qrRef} value="https://poojagohel.github.io/velvet-qr/" />
      <QRDownloader qrRef={qrRef} />
    </>
  );
}

Gradients, dot styles, and a logo

<VelvetQR
  value="https://poojagohel.github.io/velvet-qr/"
  size={280}
  dotStyle="extra-rounded"
  eyeStyle="leaf"
  cornerRadius={24}
  gradient={{ type: "linear", colors: ["#7c3aed", "#ec4899"], rotation: 45 }}
  logoImage="/logo.png"
  logoSize={0.22}
  logoPadding={6}
  logoBorderRadius={12}
/>

Content builders

Every common QR use case has a small, pure helper that returns the correctly formatted string for value:

import {
  VelvetQR,
  buildWifiValue,
  buildVCardValue,
  buildEmailValue,
  buildSmsValue,
  buildPhoneValue,
  buildLocationValue,
  buildGoogleMapsValue,
} from "velvet-qr";

// WiFi
<VelvetQR value={buildWifiValue({ ssid: "Velvet Cafe", password: "s3cret!", encryption: "WPA" })} />

// vCard / business card
<VelvetQR
  value={buildVCardValue({
    firstName: "Pooja",
    lastName: "Gohel",
    org: "Velvet UI",
    title: "Founder",
    phone: "+1 555 0100",
    email: "[email protected]",
    website: "https://poojagohel.github.io/velvet-qr/",
  })}
/>

// Email, SMS, phone, location
<VelvetQR value={buildEmailValue({ to: "[email protected]", subject: "Hi!" })} />
<VelvetQR value={buildSmsValue({ phone: "+15550100", message: "Hello!" })} />
<VelvetQR value={buildPhoneValue("+15550100")} />
<VelvetQR value={buildLocationValue({ latitude: 37.7749, longitude: -122.4194 })} />
<VelvetQR value={buildGoogleMapsValue({ query: "Golden Gate Bridge" })} />

The customization panel

import { useState } from "react";
import { VelvetQR, QRCustomizer, type QRCustomizerConfig } from "velvet-qr";

function Playground() {
  const [config, setConfig] = useState<QRCustomizerConfig>({ value: "https://poojagohel.github.io/velvet-qr/" });
  return (
    <>
      <VelvetQR {...config} />
      <QRCustomizer value={config} onChange={setConfig} />
    </>
  );
}

Logo uploader

import { useState } from "react";
import { VelvetQR, QRLogoUploader } from "velvet-qr";

function WithLogo() {
  const [logo, setLogo] = useState<string | null>(null);
  return (
    <>
      <VelvetQR value="https://poojagohel.github.io/velvet-qr/" logoImage={logo ?? undefined} />
      <QRLogoUploader onLogoChange={setLogo} />
    </>
  );
}

Presets

Skip the styling decisions entirely with a curated set of ready-made looks:

import { useState } from "react";
import { VelvetQR, QRPresetPicker, type VelvetQRProps } from "velvet-qr";

function PresetDemo() {
  const [config, setConfig] = useState<Partial<VelvetQRProps>>({});
  return (
    <>
      <VelvetQR value="https://poojagohel.github.io/velvet-qr/" {...config} />
      <QRPresetPicker onSelect={setConfig} />
    </>
  );
}

Or apply one directly without the picker UI:

import { VelvetQR, QR_PRESETS } from "velvet-qr";

const velvet = QR_PRESETS.find((p) => p.id === "velvet")!;
<VelvetQR value="https://poojagohel.github.io/velvet-qr/" {...velvet.config} />;

Batch generation

Generate and bulk-download many QR codes at once:

import { QRBatchGenerator } from "velvet-qr";

function Batch() {
  return (
    <QRBatchGenerator
      items={[
        { id: "table-1", value: "https://poojagohel.github.io/velvet-qr/menu?table=1" },
        { id: "table-2", value: "https://poojagohel.github.io/velvet-qr/menu?table=2" },
        { id: "table-3", value: "https://poojagohel.github.io/velvet-qr/menu?table=3" },
      ]}
      sharedOptions={{ dotStyle: "rounded", eyeStyle: "circle" }}
    />
  );
}

Or drive it yourself with the underlying hook:

import { useQRBatch } from "velvet-qr";

function CustomBatch() {
  const { results, downloadAll } = useQRBatch(
    [{ id: "a", value: "https://a.example" }, { id: "b", value: "https://b.example" }],
  );
  // render results.map(r => r.svgMarkup) however you like
  // downloadAll('png' | 'svg' | 'jpeg') triggers one sequential download per item
}

Hooks — full control

import { useQRGenerator, useQRDownload } from "velvet-qr";

function Custom() {
  const { svgMarkup, getDataUrl, isGenerating, error } = useQRGenerator({
    value: "https://poojagohel.github.io/velvet-qr/",
    size: 240,
  });
  // render svgMarkup however you like, or call getDataUrl('png' | 'jpeg' | 'svg')
}

Frame / label templates

Bake a "SCAN ME" caption directly into the exported image — it's part of the generated SVG (not a separate on-screen wrapper), so it's included in PNG/SVG/JPEG downloads too:

<VelvetQR
  value="https://poojagohel.github.io/velvet-qr/"
  size={240}
  frame={{ style: "banner-bottom", text: "SCAN ME" }}
/>

frame.style is one of "none" (default), "banner-top", "banner-bottom", or "border-label". "banner-top"/"banner-bottom" grow the rendered SVG to size wide × size + size * 0.2 tall (the caption band is ~20% of size); "border-label" keeps the SVG at size × size and instead draws a border with the label in a small pill straddling its bottom edge. Because banner styles change the overall height, always read the actual viewBox/width/height off the generated markup (or the size VelvetQR's own downloadPNG/downloadJPEG/getDataUrl methods rasterize to) rather than assuming a square size x size canvas when doing layout. Keep frame.text to roughly 20 characters or fewer (14 or fewer for border-label) for reliably legible results — longer text is automatically shrunk and, if needed, truncated with an ellipsis.

<VelvetQR
  value="https://poojagohel.github.io/velvet-qr/"
  size={240}
  frame={{
    style: "border-label",
    text: "SCAN ME",
    backgroundColor: "#7c3aed",
    textColor: "#ffffff",
    cornerRadius: 16,
  }}
/>

Animation

Add a purely decorative, on-screen-only reveal or "alive" effect for a landing-page hero — it never affects scannability, and exports (PNG/SVG/JPEG downloads, getDataUrl, and batch downloadAll) are always the fully-settled, static QR regardless of animation:

<VelvetQR
  value="https://poojagohel.github.io/velvet-qr/"
  size={240}
  animation={{ type: "fade-in" }} // "fade-in" | "draw-in" | "pulse" | "gradient-shift"
/>
  • "fade-in" / "draw-in": a one-shot staggered reveal (data modules fade in, or fade+scale in) that plays once and settles at fully visible forever after.
  • "pulse": a continuous, gentle looping "breathing" effect for hero sections — never applied to a logoImage if one is set, so the logo stays static/legible.
  • "gradient-shift": continuously rotates a gradient (no-ops back to "none" if gradient isn't also set).

All animation types respect prefers-reduced-motion: reduce, falling back to the static, fully-settled render. Tune timing with duration (seconds) and, for the reveal types, staggerDelay (seconds of extra delay per module):

<VelvetQR value="https://poojagohel.github.io/velvet-qr/" animation={{ type: "draw-in", duration: 1.5, staggerDelay: 0.004 }} />

Scanner

Decode QR codes from a live camera or a static image. Uses the browser-native BarcodeDetector API where available (Chrome/Edge) and transparently falls back to the bundled jsQR decoder everywhere else (Firefox, Safari, and any other browser), so scanning works across all major browsers:

import { QRScanner } from "velvet-qr";

function Scan() {
  return (
    <QRScanner
      onDetected={(value) => console.log("Scanned:", value)}
      continuous={false}
    />
  );
}

Or drive it yourself with the underlying hook:

import { useRef } from "react";
import { useQRScanner } from "velvet-qr";

function CustomScanner() {
  const { isSupported, isScanning, result, error, startCamera, stopCamera, scanImage } = useQRScanner();
  const videoRef = useRef<HTMLVideoElement>(null);

  if (!isSupported) return <p>Camera scanning isn't supported in this browser.</p>;

  return (
    <>
      <video ref={videoRef} muted playsInline />
      <button onClick={() => videoRef.current && startCamera(videoRef.current)}>Start</button>
      <button onClick={stopCamera}>Stop</button>
      <input type="file" accept="image/*" onChange={(e) => e.target.files?.[0] && scanImage(e.target.files[0])} />
      {result && <p>Scanned: {result}</p>}
      {error && <p>Error: {error}</p>}
    </>
  );
}

isSupported is false (rather than throwing) only when neither BarcodeDetector nor the jsQR canvas fallback can run at all — in practice that's just non-browser/SSR environments. Every real browser reports true. Still always render a fallback (e.g. the QRScanner component's built-in file-input path) for the rare environments where it isn't.

History

useQRHistory keeps a small local record of recently generated/used QR configurations in localStorage (SSR-safe, capped at maxEntries), and QRHistoryPanel renders it as a row of live, clickable thumbnails:

import { useState } from "react";
import { VelvetQR, useQRHistory, QRHistoryPanel, type VelvetQRProps } from "velvet-qr";

function HistoryDemo() {
  const [config, setConfig] = useState<Partial<VelvetQRProps>>({});
  const [value, setValue] = useState("https://poojagohel.github.io/velvet-qr/");
  const history = useQRHistory({ maxEntries: 20 });

  return (
    <>
      <VelvetQR value={value} {...config} />
      <button onClick={() => history.add({ value, config })}>Save to history</button>
      <QRHistoryPanel
        history={history}
        onSelect={(entry) => {
          setValue(entry.value);
          setConfig(entry.config);
        }}
      />
    </>
  );
}

This is purely local bookkeeping of configs you explicitly add() — it has no relationship to whether the resulting QR code has ever been scanned by anyone.

Analytics — honestly scoped

This package is a client-side React component library with no backend and no hosting. It is architecturally impossible for it to detect when someone scans a printed or shared QR code with their own phone camera — that scan happens entirely outside any page this library's JS ever runs in. useQRAnalytics only tracks events observable within your own page session: generation, exports (PNG/SVG/JPEG), clipboard copies, and in-session decodes if you also use this package's own useQRScanner/QRScanner. Real "was my printed QR code scanned" tracking requires server-side infrastructure outside this package's scope — e.g. routing the QR's destination through your own backend redirect endpoint and logging hits there (see Dynamic QR below).

useQRAnalytics does not wrap or auto-instrument VelvetQR — you call track() explicitly, wherever you already have the relevant information:

import { useRef } from "react";
import { VelvetQR, useQRAnalytics, type VelvetQRHandle } from "velvet-qr";

function AnalyticsDemo() {
  const qrRef = useRef<VelvetQRHandle>(null);
  const { track, stats } = useQRAnalytics({
    persist: true, // aggregate local counts into localStorage
    onTrack: (event) => myAnalyticsBackend.send(event), // pipe to GA/PostHog/your own backend
  });

  return (
    <>
      <VelvetQR
        ref={qrRef}
        value="https://poojagohel.github.io/velvet-qr/"
        onGenerated={() => track({ type: "generate", value: "https://poojagohel.github.io/velvet-qr/" })}
      />
      <button
        onClick={async () => {
          await qrRef.current?.downloadPNG();
          track({ type: "download", value: "https://poojagohel.github.io/velvet-qr/", format: "png" });
        }}
      >
        Download PNG
      </button>
      <p>Generated locally {stats.byType.generate} times this session/device.</p>
    </>
  );
}

Dynamic QR — honestly scoped

A "dynamic QR" (one whose destination can change after printing) needs the printed code to always point at a stable URL your own backend controls, so the backend — not this package — decides where to redirect at scan time. buildDynamicQrValue builds that stable URL:

import { VelvetQR, buildDynamicQrValue } from "velvet-qr";

<VelvetQR value={buildDynamicQrValue({ baseUrl: "https://yourapp.com/r", id: "table-12" })} />;
// -> encodes "https://yourapp.com/r/table-12" — your server's /r/table-12 route decides
// where to redirect, and can change that destination at any time without reprinting the code.

useDynamicQr is a small convenience wrapper around that, with a localStorage-backed resolver meant only for local prototyping/demos of the "swap the destination" experience — it does not make the physical/printed code dynamic for anyone else scanning it with a real camera, since that requires a real server-side redirect endpoint:

import { VelvetQR, useDynamicQr } from "velvet-qr";

function DynamicQrDemo() {
  const { value, destination, isLoading, setDestination } = useDynamicQr({
    id: "table-12",
    baseUrl: "https://yourapp.com/r",
    // resolver: (id) => fetch(`/api/dynamic-links/${id}`).then((r) => r.json()).then((d) => d.destination),
  });

  return (
    <>
      <VelvetQR value={value} />
      <p>{isLoading ? "Loading…" : `Currently redirects to: ${destination ?? "(unset)"}`}</p>
      <button onClick={() => setDestination("https://example.com/new-menu")}>
        Update destination (localStorage demo only)
      </button>
    </>
  );
}

Pass your own resolver (backed by a real API) for production use — setDestination is a no-op (with a console.warn) when a custom resolver is supplied, since updating the real destination is your backend's job, not this hook's.

Props

VelvetQR

| Prop | Type | Default | Description | | --- | --- | --- | --- | | value | string | — | Required. The content to encode. Use the builder helpers for structured data (WiFi, vCard, etc). | | size | number | 240 | Rendered width/height in pixels. Scales down responsively (max-width: 100%). Clamped to > 0. | | margin | number | 4 | Quiet-zone size, in QR modules. Clamped to >= 0. | | backgroundColor | string | "#ffffff" | Background fill color. Ignored if transparentBackground is set. | | foregroundColor | string | "#000000" | Data module color. Ignored if gradient is set. | | errorCorrectionLevel | "L" \| "M" \| "Q" \| "H" | "M" | Higher levels tolerate more damage/logo overlap at the cost of density. Use "H" when embedding a logo. | | cornerRadius | number | 0 | Corner radius (px) of the outer background rect. | | dotStyle | "square" \| "rounded" \| "dots" \| "classy" \| "classy-rounded" \| "extra-rounded" | "square" | Shape of the data modules. | | eyeStyle | "square" \| "circle" \| "rounded" \| "leaf" | "square" | Shape of the three finder-pattern "eyes". | | eyeColor | string | foregroundColor | Override color for the eyes only. | | gradient | { type: "linear" \| "radial"; colors: [string, string]; rotation?: number } | — | Applies a gradient fill to data modules instead of foregroundColor. | | transparentBackground | boolean | false | Omit the background rect entirely (transparent PNG/SVG exports). | | logoImage | string | — | URL or data URL of a logo to embed at the center. | | logoSize | number | 0.2 | Logo size as a fraction (0–1) of size. Clamped to [0, 1]. | | logoPadding | number | 4 | Solid-color padding (px) around the logo. | | logoBorderRadius | number | 8 | Corner radius (px) of the logo's padded backdrop. | | logoExcavate | boolean | true | Skip rendering data modules directly under the logo so it stays legible. | | frame | { style: "none" \| "banner-top" \| "banner-bottom" \| "border-label"; text?: string; backgroundColor?: string; textColor?: string; cornerRadius?: number } | — ("none") | Bakes a caption banner/border-label into the exported SVG/PNG/JPEG. See "Frame / label templates" above for the exact dimension contract. | | animation | { type: "none" \| "fade-in" \| "draw-in" \| "pulse" \| "gradient-shift"; duration?: number; staggerDelay?: number } | — ("none") | Purely decorative, on-screen-only animation. See "Animation" above. Exports (downloadPNG/downloadSVG/downloadJPEG/getDataUrl, and useQRBatch's downloadAll) are always the fully-settled, static QR, regardless of this prop. | | className / style | — | — | Passed through to the root <svg>. | | ariaLabel | string | `QR code for ${value}` | Accessible label on the root <svg> (role="img"). | | onGenerated | (svgMarkup: string) => void | — | Called whenever the generated SVG markup changes. | | id | string | auto-generated | Root element id; also used internally for gradient/clip-path def scoping. |

VelvetQRHandle (ref)

| Method | Description | | --- | --- | | downloadPNG(filename?) | Rasterizes the current SVG and downloads it as a PNG. | | downloadSVG(filename?) | Downloads the raw SVG markup. | | downloadJPEG(filename?, quality?) | Rasterizes and downloads as JPEG (quality 0–1, default 0.92). | | copyToClipboard() | Copies the rendered PNG to the system clipboard via the Clipboard API. | | getDataUrl(format, quality?) | Returns a data URL ("png" \| "jpeg" \| "svg") without triggering a download. |

Components

| Component | Purpose | | --- | --- | | VelvetQR | The core QR renderer. | | QRPreview | Styled preview card wrapping VelvetQR. | | QRDownloader | Ready-made download/copy toolbar bound to a VelvetQR ref. | | QRCustomizer | Controlled customization panel (value + onChange) for building playgrounds/editors. | | QRLogoUploader | Drag-and-drop / file-picker input that resolves an image file to a data URL. | | QRPresetPicker | Row of live, clickable preset thumbnails calling onSelect(config). | | QRBatchGenerator | Grid of generated QR codes for a list of items, plus a "Download All" action. | | QRScanner | Camera + static-image QR scanner UI built on useQRScanner, calling onDetected(value). | | QRHistoryPanel | Row of live thumbnails for useQRHistory entries, calling onSelect(entry), with per-entry remove. |

Hooks

| Hook | Purpose | | --- | --- | | useQRGenerator(options) | Core generation hook: matrix + SVG markup + getDataUrl, without any component/DOM ref. | | useQRDownload(qrRef) | Wraps a VelvetQR ref with downloadPNG/downloadSVG/downloadJPEG/copyToClipboard. | | useQRBatch(items, sharedOptions?) | Generates { id, value, svgMarkup, error } for each item, plus downloadAll(format). | | useQRScanner() | Decodes QR codes from a camera stream (startCamera/stopCamera) or a static image (scanImage) via the native BarcodeDetector API. isSupported is false when unavailable. | | useQRHistory(options?) | Local, localStorage-backed history of QR configs: entries, add, remove, clear, restore. | | useQRAnalytics(options?) | Opt-in, explicitly-called event tracking (track) for generate/download/copy/scan-decode events observable in your own session — not real-world camera scans. See "Analytics — honestly scoped" above. | | useDynamicQr(options) | Builds a stable dynamic-QR value plus a destination/setDestination/refresh API; default resolver is localStorage-backed for prototyping only. See "Dynamic QR — honestly scoped" above. |

Presets

| Export | Purpose | | --- | --- | | QR_PRESETS | 8 curated { id, name, description, config } presets — spread .config onto <VelvetQR>. | | QrPreset | The preset type, for building your own preset list in the same shape. |

FAQ

SVG, PNG, or JPEG — which should I export? SVG for print/scalable use (logos, packaging); PNG when you need a transparent background or a raster file for the web; JPEG only if file size matters more than a transparent background (JPEG has no alpha channel).

Will a logo make my QR code unscannable? Keep logoSize under ~0.25 and use errorCorrectionLevel="H" (or at least "Q") when embedding a logo — the extra error-correction data is what keeps the code scannable despite the excavated center.

Does this work with Next.js / SSR? Yes — the package ships a "use client" banner, so import VelvetQR directly in a Client Component. See examples/nextjs-app-router in the repo for a full walkthrough of why your own page still needs its own "use client" directive too.

How big is the bundle? velvet-qr has two runtime dependencies: qrcode (used only to compute the encoding matrix — all rendering is a hand-written SVG engine) and jsqr (~15KB, used only as the scanner's cross-browser fallback decoder when the native BarcodeDetector API isn't available). The whole package tree-shakes per export.

Can velvet-qr track when my QR code gets scanned? No — not a real-world scan of a printed or shared code. velvet-qr is a client-side component library with no backend and no hosting, so it never runs any code on the device that scans your printed QR — there's nothing for it to observe. useQRAnalytics can track events that happen inside your own page, in your own visitor's session (QR generation, PNG/SVG/JPEG exports, clipboard copies, and in-session decodes if you also use useQRScanner), but that's a different thing from "someone scanned my flyer." Real scan tracking requires your own server: point the QR at a stable URL under your own redirect endpoint (buildDynamicQrValue/useDynamicQr) and log hits there.

For deeper coverage (batch generation, presets, logo scannability tuning, error-correction-level tradeoffs), see docs/FAQ.md in the repo root, alongside docs/INSTALLATION.md, docs/API-REFERENCE.md, and docs/MIGRATION.md.

License

MIT © Pooja Gohel