SJ (Scott Jehl)

Building a QR Code HTML Web Component

photo of scott jehl profile headshot

By Scott Jehl

Recently, a friend asked if I knew of a Web Component that generates a QR Code from meaningful HTML. She said that there seemed to be plenty of options for creating QR codes in general, but none that worked by extending already-useful HTML instead of just configuring it all with JavaScript. I couldn't find any either, but thinking through these kinds of patterns is probably what I enjoy most about working on the web, so I thought I'd write up how I'd make one.

Gathering Materials

To make my web component, the first thing I set out to find was a good (read: tiny) JavaScript function for generating a QR code from string input. Admittedly, the part I just mentioned covers probably 99% of what my component will actually do, but to me it's the least interesting part of the pattern. There are plenty of good tools that can do that part well, so I want my web component to delegate that work so it can focus on using it in a resilient, declarative manner. As far as I'm concerned, somebody else can draw the rest of the owl, but I want to make sure the circles are drawn just right.

A bit of searching led me to find Lean QR, which only costs a few kilobytes in transfer size and is surprisingly full-featured. Lean QR works by turning an empty <canvas> element into a QR image, like so:

<canvas id="my-canvas"></canvas>
<script type="module">
  import { generate } from '/path/to/lean-qr.js';
  const code = generate('https://scottjehl.com');
  code.toCanvas(document.getElementById('my-canvas'));
</script>
<style>
  #my-canvas { width: 100%; image-rendering: pixelated; }
</style>

Clear enough! And as an aside, that was the first time I've seen image-rendering: pixelated, which turns out to be pretty interesting in itself.

As I had expected from the outset though, that initial HTML (that canvas element) is meaningless until the JavaScript loads and runs (and it's not meaningful afterwards either for that matter, from an accessibility standpoint).

My web component will aim to make this a little better.

Wiring things up.

For the HTML, I want to start with an ordinary link, which happens to fit the use case my friend was dealing with. Basically, she frequently speaks at conferences and typically displays helpful links on her slides. Pairing those links with a QR Code would make it easy for the audience to scan them and open them on their phone during the talk. I have been seeing this more and more at conferences and happen to think it's pretty cool.

Anyway, I'll start with a usable HTML link:

<a href="https://scottjehl.com">My Website</a>

To enhance that link further, next I'll wrap it with a custom element that I'll name <q-r>. Custom elements can be almost any name you want as long as they have at least one dash somewhere after the first letter, so I could've gone with <qr-code> I suppose, but <q-r> is concise..

On their own, custom elements are about as semantically meaningless as a <div>, but custom elements are special because they come with a built-in JavaScript lifecycle that I can use to extend their behavior and meaning to do whatever I'd like, much like if I were building a built-in HTML element!

Here's that custom element wrapping my link:

<q-r>
    <a href="https://scottjehl.com">My Website</a>
</q-r>

So far, that q-r element doesn't add any value to the user experience, so next I'll write a web component that extends it to append a generated QR Code. To do that, I'll add a JavaScript snippet that takes the following steps:

  1. Import the Lean QR library
  2. Define my q-r element by calling customElements.define, passing the element's name and a class that defines it (which always needs to extend the built-in HTMLElement class that provides the default DOM properties and such for all HTML elements - including undefined custom elements).
  3. Use the connectedCallback lifecycle method to gather the URL from the HTML, generate the canvas, add a role and description to it to improve its accessibility, and append it as soon as the browser discovers a q-r element in the DOM.

Here's that code in its entirety:

<script type="module">
import { generate } from '/assets/q-r/leanqr.js';
customElements.define('q-r', class extends HTMLElement {
  connectedCallback(){
      let destination = this.querySelector('a').href;
      const qrCode = generate(destination);
      this.canvas = document.createElement('canvas');
      this.canvas.setAttribute('role', 'img');
      this.canvas.setAttribute('aria-label', 'Scannable QR Code for the link that follows');
      qrCode.toCanvas(this.canvas);
      this.prepend(this.canvas);
  }
});
</script>

And here it is in action:

My Website

Taking things further

This is a simple pattern that could probably improved in a number of ways. Perhaps the final HTML would be more meaningful if it used a figure and figcaption, for example. Maybe it'd also be nice to update the canvas if the href attribute ever changes dynamically. Also, in a real implementation, I'd likely move the script to an external file so I could reuse it across many pages, which I could do by keeping that type=module attribute and adding a src to reference a file that contains the scripting that the element formerly contained. If you have thoughts or ideas, feel free to reach out on Bluesky or Masto. Thanks for reading.

Oh and hey! If the ideas in this article interest you, perhaps you'd also be interested in my course, Web Components Demystified:

Web Components Demystified: A COMPREHENSIVE, PREMIUM COURSE ABOUT BUILDING DYNAMIC, FAST, RESILIENT APPS WITH WEB STANDARDS.