Components / Utility
Scan Code
A QR code drawn in SVG from a link, or from the page it is on, so a visitor on a computer can open it on their phone.
Props
#0E0E0D
#FFFFFF
Scan it with a phone, or type a link in the props.
In Framer, open Assets, add a new code file, paste this over everything in it and save. Scan Code shows up in the Insert menu, and its controls appear in the right panel.
// Scan Code · Layr Components
//
// A QR code drawn in SVG from a link, so someone reading on a computer can
// point their phone at the screen and open the page there. Nothing is fetched
// and no image is needed: change the link and the code redraws itself.
// Paste into a new code file in Framer and give it a square size. With no link it points at the page it is on; on the canvas it shows a stand-in code.
//
// QR matrix code adapted from Project Nayuki's QR Code generator library (MIT License),
// Copyright (c) Project Nayuki. https://www.nayuki.io/page/qr-code-generator-library
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
// - The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// - The Software is provided "as is", without warranty of any kind, express or
// implied, including but not limited to the warranties of merchantability,
// fitness for a particular purpose and noninfringement. In no event shall the
// authors or copyright holders be liable for any claim, damages or other
// liability, whether in an action of contract, tort or otherwise, arising from,
// out of or in connection with the Software or the use or other dealings in the
// Software.
import { addPropertyControls, ControlType, useIsStaticRenderer } from "framer"
import { useSyncExternalStore, type CSSProperties } from "react"
// QR encoder: byte mode, error correction level M, versions 1 to 10 (up to 213 bytes of link).
const EXP = new Uint8Array(512)
const LOG = new Uint8Array(256)
;(() => {
let x = 1
for (let i = 0; i < 255; i++) {
EXP[i] = x
LOG[x] = i
x <<= 1
if (x & 256) x ^= 0x11d
}
for (let i = 255; i < 512; i++) EXP[i] = EXP[i - 255]
})()
const mul = (a: number, b: number) => (a && b ? EXP[LOG[a] + LOG[b]] : 0)
function remainder(data: number[], ec: number) {
let gen = [1]
for (let i = 0; i < ec; i++) {
const next = new Array(gen.length + 1).fill(0)
for (let j = 0; j < gen.length; j++) {
next[j] ^= gen[j]
next[j + 1] ^= mul(gen[j], EXP[i])
}
gen = next
}
const res = new Array(ec).fill(0)
for (const d of data) {
const f = d ^ res[0]
res.shift()
res.push(0)
for (let j = 0; j < ec; j++) res[j] ^= mul(gen[j + 1], f)
}
return res
}
// version: [error correction codewords per block, blocks in group 1, data codewords in each, blocks in group 2]
const BLOCKS: number[][] = [[], [10, 1, 16, 0], [16, 1, 28, 0], [26, 1, 44, 0], [18, 2, 32, 0], [24, 2, 43, 0], [16, 4, 27, 0], [18, 4, 31, 0], [22, 2, 38, 2], [22, 3, 36, 2], [26, 4, 43, 1]]
const ALIGN: number[][] = [[], [], [6, 18], [6, 22], [6, 26], [6, 30], [6, 34], [6, 22, 38], [6, 24, 42], [6, 26, 46], [6, 28, 50]]
function qrMatrix(text: string): boolean[][] | null {
const bytes = Array.from(new TextEncoder().encode(text))
let version = 0
for (let v = 1; v <= 10; v++) {
const [, g1, d1, g2] = BLOCKS[v]
const capacity = g1 * d1 + g2 * (d1 + 1)
if (bytes.length + (v < 10 ? 2 : 3) <= capacity) {
version = v
break
}
}
if (!version) return null
const [ec, g1, d1, g2] = BLOCKS[version]
const capacity = g1 * d1 + g2 * (d1 + 1)
// bit stream
const bits: number[] = []
const push = (val: number, len: number) => {
for (let i = len - 1; i >= 0; i--) bits.push((val >>> i) & 1)
}
push(4, 4)
push(bytes.length, version < 10 ? 8 : 16)
for (const b of bytes) push(b, 8)
push(0, Math.min(4, capacity * 8 - bits.length))
while (bits.length % 8) bits.push(0)
const data: number[] = []
for (let i = 0; i < bits.length; i += 8) data.push(bits.slice(i, i + 8).reduce((a, b) => (a << 1) | b, 0))
for (let pad = 0xec; data.length < capacity; pad ^= 0xec ^ 0x11) data.push(pad)
// blocks and interleaving
const blocks: number[][] = []
const ecs: number[][] = []
let at = 0
for (let b = 0; b < g1 + g2; b++) {
const len = d1 + (b < g1 ? 0 : 1)
const blk = data.slice(at, at + len)
at += len
blocks.push(blk)
ecs.push(remainder(blk, ec))
}
const all: number[] = []
for (let i = 0; i <= d1; i++) for (const blk of blocks) if (i < blk.length) all.push(blk[i])
for (let i = 0; i < ec; i++) for (const e of ecs) all.push(e[i])
// matrix
const size = 17 + 4 * version
const m: boolean[][] = Array.from({ length: size }, () => new Array(size).fill(false))
const fn: boolean[][] = Array.from({ length: size }, () => new Array(size).fill(false))
const set = (x: number, y: number, dark: boolean) => {
if (x < 0 || y < 0 || x >= size || y >= size) return
m[y][x] = dark
fn[y][x] = true
}
for (let i = 0; i < size; i++) {
set(6, i, i % 2 === 0)
set(i, 6, i % 2 === 0)
}
const finder = (cx: number, cy: number) => {
for (let dy = -4; dy <= 4; dy++)
for (let dx = -4; dx <= 4; dx++) {
const d = Math.max(Math.abs(dx), Math.abs(dy))
set(cx + dx, cy + dy, d !== 2 && d !== 4)
}
}
finder(3, 3)
finder(size - 4, 3)
finder(3, size - 4)
const al = ALIGN[version]
for (let i = 0; i < al.length; i++)
for (let j = 0; j < al.length; j++) {
if ((i === 0 && j === 0) || (i === 0 && j === al.length - 1) || (i === al.length - 1 && j === 0)) continue
for (let dy = -2; dy <= 2; dy++) for (let dx = -2; dx <= 2; dx++) set(al[i] + dx, al[j] + dy, Math.max(Math.abs(dx), Math.abs(dy)) !== 1)
}
const format = (mask: number) => {
const d = mask // level M has format bits 00
let rem = d
for (let i = 0; i < 10; i++) rem = (rem << 1) ^ ((rem >>> 9) * 0x537)
const f = ((d << 10) | rem) ^ 0x5412
const bit = (i: number) => ((f >>> i) & 1) === 1
for (let i = 0; i <= 5; i++) set(8, i, bit(i))
set(8, 7, bit(6))
set(8, 8, bit(7))
set(7, 8, bit(8))
for (let i = 9; i < 15; i++) set(14 - i, 8, bit(i))
for (let i = 0; i < 8; i++) set(size - 1 - i, 8, bit(i))
for (let i = 8; i < 15; i++) set(8, size - 15 + i, bit(i))
set(8, size - 8, true)
}
format(0)
if (version >= 7) {
let rem = version
for (let i = 0; i < 12; i++) rem = (rem << 1) ^ ((rem >>> 11) * 0x1f25)
const v = (version << 12) | rem
for (let i = 0; i < 18; i++) {
const dark = ((v >>> i) & 1) === 1
const a = size - 11 + (i % 3)
const b = Math.floor(i / 3)
set(a, b, dark)
set(b, a, dark)
}
}
let k = 0
for (let right = size - 1; right >= 1; right -= 2) {
if (right === 6) right = 5
for (let vert = 0; vert < size; vert++)
for (let j = 0; j < 2; j++) {
const x = right - j
const up = ((right + 1) & 2) === 0
const y = up ? size - 1 - vert : vert
if (!fn[y][x] && k < all.length * 8) {
m[y][x] = ((all[k >>> 3] >>> (7 - (k & 7))) & 1) === 1
k++
}
}
}
const masks = [
(x: number, y: number) => (x + y) % 2 === 0,
(_x: number, y: number) => y % 2 === 0,
(x: number) => x % 3 === 0,
(x: number, y: number) => (x + y) % 3 === 0,
(x: number, y: number) => (Math.floor(x / 3) + Math.floor(y / 2)) % 2 === 0,
(x: number, y: number) => ((x * y) % 2) + ((x * y) % 3) === 0,
(x: number, y: number) => (((x * y) % 2) + ((x * y) % 3)) % 2 === 0,
(x: number, y: number) => (((x + y) % 2) + ((x * y) % 3)) % 2 === 0,
]
const apply = (mask: number) => {
for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) if (!fn[y][x] && masks[mask](x, y)) m[y][x] = !m[y][x]
}
const penalty = () => {
let p = 0
let dark = 0
for (let y = 0; y < size; y++) {
let runR = 1
let runC = 1
for (let x = 0; x < size; x++) {
if (m[y][x]) dark++
if (x > 0) {
runR = m[y][x] === m[y][x - 1] ? runR + 1 : 1
if (runR === 5) p += 3
else if (runR > 5) p++
runC = m[x][y] === m[x - 1][y] ? runC + 1 : 1
if (runC === 5) p += 3
else if (runC > 5) p++
}
if (x > 0 && y > 0 && m[y][x] === m[y][x - 1] && m[y][x] === m[y - 1][x] && m[y][x] === m[y - 1][x - 1]) p += 3
}
}
return p + Math.floor(Math.abs((dark * 20) / (size * size) - 10)) * 10
}
let best = 0
let low = Infinity
for (let mask = 0; mask < 8; mask++) {
apply(mask)
format(mask)
const p = penalty()
if (p < low) {
low = p
best = mask
}
apply(mask)
}
apply(best)
format(best)
return m
}
// The address of the page the code sits on, without query or hash. It is only known in the
// browser, so the server (and the first paint) has none; it is read again after a page change.
const subscribe = (change: () => void) => {
window.addEventListener("popstate", change)
window.addEventListener("hashchange", change)
const poll = window.setInterval(change, 1000)
return () => {
window.removeEventListener("popstate", change)
window.removeEventListener("hashchange", change)
window.clearInterval(poll)
}
}
const here = () => window.location.origin + window.location.pathname
const nowhere = () => ""
// Empty: this page. Starting with /: that page of this site. Anything else is used as it is.
function target(link: string, page: string) {
const l = String(link || "").trim()
if (!l) return page
if (l.startsWith("/")) return page ? new URL(page).origin + l : ""
return l
}
function pathOf(m: boolean[][], q: number) {
const n = m.length
let d = ""
for (let y = 0; y < n; y++) {
let x = 0
while (x < n) {
if (!m[y][x]) {
x++
continue
}
let w = 1
while (x + w < n && m[y][x + w]) w++
d += `M${x + q} ${y + q}h${w}v1h-${w}z`
x += w
}
}
return d
}
interface ScanCodeProps {
link: string
label: string
quiet: number
corner: number
codeColour: string
groundColour: string
style?: CSSProperties
}
/**
* @framerIntrinsicWidth 148
* @framerIntrinsicHeight 148
*
* @framerSupportedLayoutWidth fixed
* @framerSupportedLayoutHeight fixed
*/
export default function ScanCode(props: ScanCodeProps) {
const { link = "", label = "Scan to open this page on your phone", quiet = 2, corner = 0, codeColour = "#0C0E11", groundColour = "#FFFFFF", style } = props
const isStatic = useIsStaticRenderer()
const page = useSyncExternalStore(subscribe, here, nowhere)
// On the canvas the page's address is Framer's own, so a stand-in is drawn instead.
const text = target(link, isStatic ? "https://example.com" : page)
const q = Math.max(0, Math.round(quiet))
const box: CSSProperties = { ...style, display: "block", width: "100%", height: "100%", borderRadius: corner, background: groundColour }
if (!text) return <svg role="img" aria-label={label} viewBox="0 0 25 25" style={box} />
const m = qrMatrix(text)
if (!m) return <div style={{ ...box, display: "grid", placeItems: "center", fontSize: 12, textAlign: "center", color: codeColour }}>This link is too long for a scan code.</div>
const full = m.length + q * 2
return (
<svg role="img" aria-label={label} viewBox={`0 0 ${full} ${full}`} shapeRendering="crispEdges" style={box}>
<path d={pathOf(m, q)} fill={codeColour} />
</svg>
)
}
addPropertyControls(ScanCode, {
link: { type: ControlType.String, title: "Link", defaultValue: "", placeholder: "This page", description: "The address the code opens. Leave empty for the page it is on, or start with / for another page of this site." },
label: { type: ControlType.String, title: "Alt text", defaultValue: "Scan to open this page on your phone", description: "What the code is for, for screen readers." },
quiet: { type: ControlType.Number, title: "Margin", defaultValue: 2, min: 0, max: 6, step: 1, description: "Empty squares round the code. Phones read it more easily with two or more." },
corner: { type: ControlType.Number, title: "Corner", defaultValue: 0, min: 0, max: 40, step: 1, unit: "px" },
codeColour: { type: ControlType.Color, title: "Code colour", defaultValue: "#0C0E11" },
groundColour: { type: ControlType.Color, title: "Ground colour", defaultValue: "#FFFFFF" },
})
In this preview
No pictures needed.