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:
10remsquare by default; setinline-sizeon the element and it stays square. Longer values need more modules, and a module needs about 3 CSS pixels to scan reliably:10remfits about 150 characters at the defaultecc. Give longer values more room (about17remfor 500 characters) or a lowerecc. - Colours:
--sb-qr-colorfor the modules (near-black),--sb-qr-backgroundbehind them (white) and, withaccent,--sb-qr-accentfor the three corner squares (unless set, your--sb-brandwith 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,modulesandcorners, for anything the tokens don't cover (they are SVG shapes, so they takefill), anderror, the box that replaces the code when the value doesn't fit (its text is theerrorslot). 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.jsone 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:
<sb-qr-code>
<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.
| File | Original | gzip | brotli |
|---|---|---|---|
qr-code.bundle.min.js | 12.2 kB | 4.9 kB | 4.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.
| File | Original | gzip | brotli | minified |
|---|---|---|---|---|
qr-code.js | 3.9 kB | 2.0 kB | 1.7 kB | 990 B |
vendor/uqr.mjs | 27.5 kB | 7.8 kB | 6.8 kB | 4.0 kB |
| Total | 31.4 kB | 9.8 kB | 8.5 kB | 5.0 kB |
API reference
Props
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | "" | 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. |
border | number | 2 | Quiet zone around the code, in modules (the standard asks for 4; 2 scans fine on a plain background). |
accent | boolean | false | Colour the three corner squares with --sb-qr-accent (default: the brand colour, darkened where it is too light to scan). |
label | string | "" | Accessible name (default: "QR code: " and the value). |
Slots
| Name | Description |
|---|---|
error | Replaces the "Too much data for a QR code" message, e.g. to translate it. |