QR Code

Pixel-perfect QR codes for any text or URL, with optional brand-coloured corners.

zweiundeins <sb-qr-code> qr code share url svg Since 2026-09-22 MIT licence 4.4 kB1 Open in playground Edit on GitHub

Playground

Draws a QR code as a crisp SVG: every module is a pixel. It encodes with uqr (MIT), vendored unmodified from npm; vendor.json records the release it is verified against.

The code stays dark on white whatever the theme, since some scanners can't read inverted codes. With accent, the corner squares take your brand colour, darkened where it is too light to scan. Size it with CSS (inline-size), or set --sb-qr-color, --sb-qr-background and --sb-qr-accent.

Examples

Live from an input

<div data-signals="{_qr: 'https://starbase.example'}" style="display: grid; gap: 16px; justify-items: start">
  <sb-input label="Text or URL" data-bind:_qr__prop.value style="inline-size: 20rem"></sb-input>
  <sb-qr-code accent data-attr:value="$_qr" data-preserve-attr="value"></sb-qr-code>
</div>

Error correction

Higher levels survive more damage, like a logo over the middle, and need more modules.

<div style="display: flex; gap: 16px; flex-wrap: wrap">
  <sb-qr-code value="Ad astra" ecc="L" style="inline-size: 6rem"></sb-qr-code>
  <sb-qr-code value="Ad astra" ecc="H" style="inline-size: 6rem"></sb-qr-code>
</div>

Colours

<sb-qr-code value="https://github.com/zweiundeins/starbase" accent
  style="inline-size: 8rem; --sb-qr-color: #2E2780; --sb-qr-accent: #DC2626"></sb-qr-code>

Too much data

A code holds up to 2,953 bytes at ecc="L" (1,273 at H). A longer value shows "Too much data for a QR code" in its place; put your own text in the error slot, for example to translate it:

<sb-qr-code data-attr:value="$_note" data-preserve-attr="value">
  <span slot="error">Zu viele Daten für einen QR-Code</span>
</sb-qr-code>

From the server

Server-rendered values work as they are: <sb-qr-code value="https://example.com/invite/X7Q2">. When a stream re-renders the attribute, the code follows.

Styling

Style it from your page's CSS, without changing the component or importing anything into it. Custom properties, inherited properties and ::part() all reach into its shadow root.

  • Size: 10rem square by default; set inline-size on the element and it stays square. Longer values need more modules, and a module needs about 3 CSS pixels to scan reliably: 10rem fits about 150 characters at the default ecc. Give longer values more room (about 17rem for 500 characters) or a lower ecc.
  • Colours: --sb-qr-color for the modules (near-black), --sb-qr-background behind them (white) and, with accent, --sb-qr-accent for the three corner squares (unless set, your --sb-brand with its OKLCH lightness capped at 50%). Keep the contrast high: scanners find a code by those corners, so an accent you set needs about 4.5:1 against the background, like text.
  • Parts: svg, background, modules and corners, for anything the tokens don't cover (they are SVG shapes, so they take fill), and error, the box that replaces the code when the value doesn't fit (its text is the error slot). Your page's ::part() rules win over the component's own, without !important.
<style>
  .my-qr { inline-size: 7rem; --sb-qr-color: #1E3A8A; --sb-qr-accent: #DB2777; }
</style>
<sb-qr-code class="my-qr" accent value="https://data-star.dev"></sb-qr-code>

Accessibility

The SVG is an img named "QR code: " plus the value (or label). Put the link or text next to the code as well, for people who can't scan it.

Installation

Add Datastar with Rocket and the Starbase autoloader once per page, then use the tag: the autoloader imports each component the first time its tag appears, including tags a Datastar morph adds later. The Datastar here is v1.0.4 with Starbase's fixes until they are released; the components also run on the official release.

<script type="importmap">
  { "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" } }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/autoloader.js"></script>

<sb-qr-code value="https://data-star.dev"></sb-qr-code>

No flash of undefined elements: put class="sb-cloak" on <html> and add .sb-cloak :not(:defined) { visibility: hidden } to your CSS. The autoloader removes the class once the page's components are defined.

Load just this component, pinned to this version: all its modules in one minified file with its integrity hash, so the browser refuses it if a single byte changes.

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/qr-code@0d8b2096aa94/qr-code.bundle.min.js" integrity="sha384-TJLQpPPoXJggHGpW6tIoa6Thl0LGJfKjGrZ8IHTCpPlfTjPVIYs69G9/k2SKYNYx"></script>

<sb-qr-code value="https://data-star.dev"></sb-qr-code>

Or the minified module files (2 files, which the browser requests as it finds their imports), each pinned in the import map:

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
      "https://starbase.zweiundeins.gmbh/c/qr-code@0d8b2096aa94/qr-code.min.js": "sha384-lbjsew2dJTUODW2J/zRCPydPl1j6LcSeJ0H0fH4JlaxZr8Xmf/tIoHEtmuaXikbz",
      "https://starbase.zweiundeins.gmbh/c/qr-code@0d8b2096aa94/vendor/uqr.min.mjs": "sha384-lg8RyFvkbOp4oa6Ny6ZQLHos0CvIWFIdAQyc0NFGwKwdqeQIJJuF1Xh6tAlkACb3"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/qr-code@0d8b2096aa94/qr-code.min.js" integrity="sha384-lbjsew2dJTUODW2J/zRCPydPl1j6LcSeJ0H0fH4JlaxZr8Xmf/tIoHEtmuaXikbz"></script>

<sb-qr-code value="https://data-star.dev"></sb-qr-code>

In production, pin today's whole catalog: its autoloader and an import map with integrity hashes, so the browser refuses any file that changed. This map covers Datastar and this component; importmap.json has the hashes of every component, to merge into yours.

<script type="importmap">
  {
    "imports": { "datastar": "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js" },
    "integrity": {
      "https://starbase.zweiundeins.gmbh/c/datastar@f602fbe19d74/datastar-rocket.js": "sha384-mvTnynHaljKNMleD0bj6/rWROnluHlNmbc5rOQTd+fQH3YBDYhlvHdNquEX99by5",
      "https://starbase.zweiundeins.gmbh/c/qr-code@0d8b2096aa94/qr-code.bundle.min.js": "sha384-TJLQpPPoXJggHGpW6tIoa6Thl0LGJfKjGrZ8IHTCpPlfTjPVIYs69G9/k2SKYNYx"
    }
  }
</script>
<script type="module" src="https://starbase.zweiundeins.gmbh/c/@339249aca48f/autoloader.js" integrity="sha384-ZNpMayI1XShztIEX0iDHyCDDbhT8H+t+zEESUq4BCrN4b+7m931LQEl5x0vbY+Vo"></script>

<sb-qr-code value="https://data-star.dev"></sb-qr-code>

Components import only datastar and their own files, so copying is enough: save each component's one file (and a file it loads on first use, keeping its folder), and point the import map at your own copy of datastar-rocket.js (the /js/ paths stand for yours).

  • <sb-qr-code>
    • qr-code.bundle.min.js one file 4.4 kB brotli
<script type="importmap">
  { "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/qr-code/qr-code.bundle.min.js"></script>

<sb-qr-code value="https://data-star.dev"></sb-qr-code>

Or copy the module files, keeping each component's folder, and load each component's module:

<script type="importmap">
  { "imports": { "datastar": "/js/datastar-rocket.js" } }
</script>
<script type="module" src="/js/qr-code/qr-code.min.js"></script>

<sb-qr-code value="https://data-star.dev"></sb-qr-code>

Size

A page loads this component as one file, its bundle, compressed the way it is served (gzip -9, brotli -11): brotli is what it downloads. Datastar and Rocket are shared by every component and not counted.

FileOriginalgzipbrotli
qr-code.bundle.min.js 12.2 kB4.9 kB4.4 kB
As module files: 2 requests, 5.0 kB minified with brotli

For a page that loads the module files as they are, requesting each one as it finds its import, or bundles them itself. Each file compressed on its own; the minified one is what the page downloads, and the readable source is always there too.

FileOriginalgzipbrotliminified
qr-code.js 3.9 kB2.0 kB1.7 kB990 B
vendor/uqr.mjs 27.5 kB7.8 kB6.8 kB4.0 kB
Total31.4 kB9.8 kB8.5 kB5.0 kB

API reference

Props

AttributeTypeDefaultDescription
valuestring""Text or URL to encode.
ecc"L" | "M" | "Q" | "H""M"Error correction: L 7%, M 15%, Q 25%, H 30% of the code can be damaged or covered.
bordernumber2Quiet zone around the code, in modules (the standard asks for 4; 2 scans fine on a plain background).
accentbooleanfalseColour the three corner squares with --sb-qr-accent (default: the brand colour, darkened where it is too light to scan).
labelstring""Accessible name (default: "QR code: " and the value).

Slots

NameDescription
errorReplaces the "Too much data for a QR code" message, e.g. to translate it.