Components / Display & Media / QR Code

QR Code beta

A real QR code, encoded in your page by a zero-dependency script written from the spec — no library, no image service.

Preview

kata-ui.dev

Encoded on this page, by the part itself — scan it and check. The URL is printed beside the code, because a code should never be the only way through.

<div style="display:flex;gap:18px;align-items:center;flex-wrap:wrap">
  <div class="k-qr" data-qr="https://kata-ui.dev" data-qr-label="QR code linking to kata-ui.dev" style="width:168px"></div>
  <div style="max-width:250px">
    <p style="font-size:14.5px;font-weight:600">kata-ui.dev</p>
    <p style="font-size:12.5px;color:var(--k-ink-2);line-height:1.75;margin-top:5px">Encoded on this page, by the part itself — scan it and check. The URL is printed beside the code, because a code should never be the only way through.</p>
  </div>
</div>
/* KATA QR Code — the code is drawn by the part's own encoder. */
.k-qr{display:inline-block;background:var(--k-on-color);border:1px solid var(--k-line);border-radius:var(--k-radius-lg);padding:12px;line-height:0}
.k-qr svg{width:100%;height:auto;display:block}
.k-qr .qr-fallback{font-size:12.5px;line-height:1.6;color:var(--k-danger-ink);max-width:22ch}
// KATA QR Code — a zero-dependency encoder written from ISO/IEC 18004.
// Byte mode, error-correction level M, versions 1–6 (14–106 bytes).
// The mask is chosen by the standard's four penalty rules.

// ---- Galois field GF(256), primitive polynomial 0x11d ----
var KQR_EXP = [], KQR_LOG = [];
(function () {
  var x = 1;
  for (var i = 0; i < 255; i++) { KQR_EXP.push(x); KQR_LOG[x] = i; x <<= 1; if (x & 0x100) x ^= 0x11d; }
})();
function kqrMul(a, b) { return (a && b) ? KQR_EXP[(KQR_LOG[a] + KQR_LOG[b]) % 255] : 0; }
function kqrPolyMul(p, q) {
  var r = new Array(p.length + q.length - 1).fill(0);
  for (var i = 0; i < p.length; i++) for (var j = 0; j < q.length; j++) r[i + j] ^= kqrMul(p[i], q[j]);
  return r;
}
function kqrGenPoly(n) {
  var g = [1];
  for (var i = 0; i < n; i++) g = kqrPolyMul(g, [1, KQR_EXP[i]]);
  return g;
}
function kqrRemainder(data, gen) {
  var res = new Array(gen.length - 1).fill(0);
  data.forEach(function (b) {
    var factor = b ^ res.shift();
    res.push(0);
    for (var i = 0; i < res.length; i++) res[i] ^= kqrMul(gen[i + 1], factor);
  });
  return res;
}

// version -> [blocks, data codewords per block, EC per block, alignment center, remainder bits]
var KQR_V = [null,
  [1, 16, 10, 0, 0], [1, 28, 16, 18, 7], [1, 44, 26, 22, 7],
  [2, 32, 18, 26, 7], [2, 43, 24, 30, 7], [4, 27, 16, 34, 7]];

function kqrDataCodewords(bytes, ver) {
  var info = KQR_V[ver];
  var capBits = info[0] * info[1] * 8;
  var bits = [];
  function push(val, len) { for (var i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1); }
  push(0x4, 4); // byte mode
  push(bytes.length, 8);
  bytes.forEach(function (b) { push(b, 8); });
  push(0, Math.min(4, capBits - bits.length));
  while (bits.length % 8) bits.push(0);
  var out = [];
  for (var i = 0; i < bits.length; i += 8) {
    var v = 0; for (var j = 0; j < 8; j++) v = (v << 1) | bits[i + j];
    out.push(v);
  }
  var pads = [0xec, 0x11], total = info[0] * info[1];
  for (var k = 0; out.length < total; k++) out.push(pads[k % 2]);
  return out;
}

function kqrInterleave(dataCw, ver) {
  var info = KQR_V[ver], nb = info[0], per = info[1], gen = kqrGenPoly(info[2]);
  var blocks = [], ecs = [], i, j;
  for (i = 0; i < nb; i++) {
    var chunk = dataCw.slice(i * per, (i + 1) * per);
    blocks.push(chunk);
    ecs.push(kqrRemainder(chunk, gen));
  }
  var bits = [];
  function pushByte(v) { for (var k = 7; k >= 0; k--) bits.push((v >>> k) & 1); }
  for (j = 0; j < per; j++) for (i = 0; i < nb; i++) pushByte(blocks[i][j]);
  for (j = 0; j < info[2]; j++) for (i = 0; i < nb; i++) pushByte(ecs[i][j]);
  for (i = 0; i < info[4]; i++) bits.push(0);
  return bits;
}

function kqrFormatBits(mask) {
  var data = mask; // EC level M indicator bits are 00, so data = 0b00<<3 | mask
  var rem = data;
  for (var i = 0; i < 10; i++) rem = (rem << 1) ^ (((rem >>> 9) & 1) * 0x537);
  return ((data << 10) | rem) ^ 0x5412;
}

function kqrMatrix(text) {
  var bytes = Array.prototype.slice.call(new TextEncoder().encode(text));
  var ver = 0;
  for (var v = 1; v <= 6; v++) {
    if (bytes.length <= Math.floor((KQR_V[v][0] * KQR_V[v][1] * 8 - 12) / 8)) { ver = v; break; }
  }
  if (!ver) return null;
  var size = 17 + 4 * ver;
  var bits = kqrInterleave(kqrDataCodewords(bytes, ver), ver);
  var mod = [], fn = [], x, y;
  for (y = 0; y < size; y++) { mod.push(new Array(size).fill(0)); fn.push(new Array(size).fill(false)); }
  function setF(xx, yy, dark) { if (0 <= xx && xx < size && 0 <= yy && yy < size) { mod[yy][xx] = dark ? 1 : 0; fn[yy][xx] = true; } }
  function drawFinder(cx, cy) {
    for (var dy = -4; dy <= 4; dy++) for (var dx = -4; dx <= 4; dx++) {
      var dist = Math.max(Math.abs(dx), Math.abs(dy));
      setF(cx + dx, cy + dy, dist !== 2 && dist !== 4);
    }
  }
  for (var i = 0; i < size; i++) { setF(6, i, i % 2 === 0); setF(i, 6, i % 2 === 0); }
  drawFinder(3, 3); drawFinder(size - 4, 3); drawFinder(3, size - 4);
  var align = KQR_V[ver][3];
  if (align) {
    for (var dy2 = -2; dy2 <= 2; dy2++) for (var dx2 = -2; dx2 <= 2; dx2++)
      setF(align + dx2, align + dy2, Math.max(Math.abs(dx2), Math.abs(dy2)) !== 1);
  }
  setF(8, size - 8, 1); // dark module
  // reserve the format-information cells before data placement —
  // they are function modules even though their values wait for the mask
  (function reserveFormat() {
    for (var i = 0; i <= 5; i++) { setF(8, i, 0); setF(i, 8, 0); }
    setF(8, 7, 0); setF(8, 8, 0); setF(7, 8, 0);
    for (var i = 0; i < 8; i++) setF(size - 1 - i, 8, 0);
    for (var i = 8; i < 15; i++) setF(8, size - 15 + i, 0);
  })();
  // reserve format cells (values drawn after masking)
  function drawFormat(mask) {
    var bitsF = kqrFormatBits(mask);
    function bit(i) { return ((bitsF >>> i) & 1) === 1; }
    for (var i = 0; i <= 5; i++) setF(8, i, bit(i));
    setF(8, 7, bit(6)); setF(8, 8, bit(7)); setF(7, 8, bit(8));
    for (var i = 9; i < 15; i++) setF(14 - i, 8, bit(i));
    for (var i = 0; i < 8; i++) setF(size - 1 - i, 8, bit(i));
    for (var i = 8; i < 15; i++) setF(8, size - 15 + i, bit(i));
    setF(8, size - 8, 1);
  }
  // place data bits (zigzag), skipping function modules
  var bi = 0;
  for (var right = size - 1; right >= 1; right -= 2) {
    if (right === 6) right = 5;
    for (var vert = 0; vert < size; vert++) {
      for (var j = 0; j < 2; j++) {
        var xx = right - j;
        var upward = ((right + 1) & 2) === 0;
        var yy = upward ? size - 1 - vert : vert;
        if (!fn[yy][xx]) { mod[yy][xx] = bi < bits.length ? bits[bi] : 0; bi++; }
      }
    }
  }
  function maskBit(mask, xx, yy) {
    switch (mask) {
      case 0: return (xx + yy) % 2 === 0;
      case 1: return yy % 2 === 0;
      case 2: return xx % 3 === 0;
      case 3: return (xx + yy) % 3 === 0;
      case 4: return (Math.floor(yy / 2) + Math.floor(xx / 3)) % 2 === 0;
      case 5: return (xx * yy) % 2 + (xx * yy) % 3 === 0;
      case 6: return ((xx * yy) % 2 + (xx * yy) % 3) % 2 === 0;
      case 7: return ((xx + yy) % 2 + (xx * yy) % 3) % 2 === 0;
    }
    return false;
  }
  function penalties(m) {
    var score = 0, xx, yy, run, c;
    // rule 1: runs
    for (yy = 0; yy < size; yy++) {
      run = 1;
      for (xx = 1; xx < size; xx++) { if (m[yy][xx] === m[yy][xx - 1]) { run++; } else { if (run >= 5) score += run - 2; run = 1; } }
      if (run >= 5) score += run - 2;
    }
    for (xx = 0; xx < size; xx++) {
      run = 1;
      for (yy = 1; yy < size; yy++) { if (m[yy][xx] === m[yy - 1][xx]) { run++; } else { if (run >= 5) score += run - 2; run = 1; } }
      if (run >= 5) score += run - 2;
    }
    // rule 2: 2x2 blocks
    for (yy = 0; yy < size - 1; yy++) for (xx = 0; xx < size - 1; xx++) {
      c = m[yy][xx];
      if (c === m[yy][xx + 1] && c === m[yy + 1][xx] && c === m[yy + 1][xx + 1]) score += 3;
    }
    // rule 3: finder-like patterns
    function checkLine(line) {
      var s = 0;
      for (var i = 0; i + 11 <= line.length; i++) {
        var w = line.slice(i, i + 11).join('');
        if (w === '10111010000' || w === '00001011101') s += 40;
      }
      return s;
    }
    for (yy = 0; yy < size; yy++) score += checkLine(m[yy]);
    for (xx = 0; xx < size; xx++) score += checkLine(m.map(function (row) { return row[xx]; }));
    // rule 4: balance
    var dark = 0;
    m.forEach(function (row) { row.forEach(function (v) { dark += v; }); });
    var k = Math.ceil(Math.abs(dark * 20 - size * size * 10) / (size * size)) - 1;
    if (k > 0) score += k * 10;
    return score;
  }
  var best = null, bestScore = Infinity, bestMask = 0;
  for (var mask = 0; mask < 8; mask++) {
    var trial = mod.map(function (row, yy) {
      return row.map(function (v, xx) { return (!fn[yy][xx] && maskBit(mask, xx, yy)) ? v ^ 1 : v; });
    });
    // format cells in the trial: temporarily draw, score, keep matrix pure
    var saved = [];
    for (y = 0; y < size; y++) for (x = 0; x < size; x++) if (fn[y][x] && ((x === 8) || (y === 8))) saved.push([x, y, trial[y][x]]);
    // draw format on the trial via a shim: reuse setF against trial
    (function () {
      var bitsF = kqrFormatBits(mask);
      function bit(i2) { return ((bitsF >>> i2) & 1) === 1 ? 1 : 0; }
      function s2(xx, yy, val) { if (0 <= xx && xx < size && 0 <= yy && yy < size) trial[yy][xx] = val; }
      for (var i = 0; i <= 5; i++) s2(8, i, bit(i));
      s2(8, 7, bit(6)); s2(8, 8, bit(7)); s2(7, 8, bit(8));
      for (var i = 9; i < 15; i++) s2(14 - i, 8, bit(i));
      for (var i = 0; i < 8; i++) s2(size - 1 - i, 8, bit(i));
      for (var i = 8; i < 15; i++) s2(8, size - 15 + i, bit(i));
      s2(8, size - 8, 1);
    })();
    var sc = penalties(trial);
    if (sc < bestScore) { bestScore = sc; best = trial; bestMask = mask; }
  }
  return { size: size, version: ver, mask: bestMask, modules: best };
}

function kqrSVG(text, label) {
  var r = kqrMatrix(text);
  if (!r) return null;
  var n = r.size, d = '';
  for (var y = 0; y < n; y++) for (var x = 0; x < n; x++)
    if (r.modules[y][x]) d += 'M' + (x + 4) + ' ' + (y + 4) + 'h1v1h-1z';
  return '<svg viewBox="0 0 ' + (n + 8) + ' ' + (n + 8) + '" role="img" aria-label="' +
    (label || ('QR code: ' + text)) + '"><rect width="' + (n + 8) + '" height="' + (n + 8) + '" fill="#fff"/>' +
    '<path d="' + d + '" fill="#18181b"/></svg>';
}

document.querySelectorAll('[data-qr]').forEach(function (el) {
  var svg = kqrSVG(el.getAttribute('data-qr'), el.getAttribute('data-qr-label'));
  if (svg) { el.innerHTML = svg; }
  else { el.innerHTML = '<span class="qr-fallback">This text is too long for a version 6 code (106 bytes at level M).</span>'; }
});

Installation

CLI

node kata-ui.mjs add qr-code

Writes the part’s CSS and JS into your project. New to the CLI? See the CLI doc.

Manual

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Examples

On a ticket

ADMIT ONE

Catalog Launch Night

Seat A17 — the code holds the ticket number, nothing personal.

<div style="border:1px solid var(--k-line);border-radius:14px;padding:16px;display:flex;gap:16px;align-items:center;max-width:430px;background:var(--k-bg)">
  <div class="k-qr" data-qr="KATA-EVENT-2026-1102-A17" data-qr-label="QR code for ticket A17" style="width:104px;flex:none"></div>
  <div>
    <p style="font-size:13px;color:var(--k-ink-3);letter-spacing:.08em;font-weight:600">ADMIT ONE</p>
    <p style="font-size:15.5px;font-weight:600;margin-top:4px">Catalog Launch Night</p>
    <p style="font-size:12.5px;color:var(--k-ink-2);margin-top:4px;line-height:1.7">Seat A17 — the code holds the ticket number, nothing personal.</p>
  </div>
</div>

Usage

Give the element a data-qr with the text (a URL, up to 106 bytes at error level M, versions 1–6). The script encodes it — byte mode, Reed–Solomon, mask by the penalty rules — and draws an SVG. Because the encoder is the part, codes work offline and never phone a generator service.

Accessibility

The SVG is an image of data: give the element an aria-label with the destination in words ("Link to kata-ui.dev"), and never make the code the only way to reach it — print the URL beside it for everyone the camera fails.

Anatomy

The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.

.k-qrclass
.qr-fallbackclass
data-qrdata hook
data-qr-labeldata hook

Related parts