Components QR code

Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

QR code

Server-rendered QR codes as inline SVG - no JavaScript, themeable, generated from a URL or any string.
Scan to open

Server-rendered SVG, no JavaScript. Every dark module is one path, so a dense code is still a single DOM node. Colour rides currentColor, size rides classes.

petal.build
rounded=0.6
rounded=1
heex
<div class="flex flex-wrap items-center justify-center gap-10">
<div class="text-center">
<.qr_code value="https://petal.build" class="size-36 text-gray-900 dark:text-white" />
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">petal.build</div>
</div>
<div class="text-center">
<.qr_code
value="https://petal.build"
rounded={0.6}
class="size-36 text-primary-600 dark:text-primary-400"
/>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">rounded={0.6}</div>
</div>
<div class="text-center">
<.qr_code
value="https://petal.build"
rounded={1}
class="size-36 text-gray-900 dark:text-white"
/>
<div class="mt-3 text-xs text-gray-500 dark:text-gray-400">rounded={1}</div>
</div>
</div>
Two-factor enrolment

The number one reason to reach for this. The secret is printed underneath as text too - a QR code must never be the only way in.

Scan with your authenticator

Open 1Password, Authy or Google Authenticator and point it at this code.

Can't scan? Enter this key by hand:
JBSW Y3DP EHPK 3PXP
heex
<div class="w-full max-w-sm p-6 mx-auto border border-gray-200 rounded-xl dark:border-gray-800">
<div class="text-base font-semibold text-gray-900 dark:text-white">
Scan with your authenticator
</div>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Open 1Password, Authy or Google Authenticator and point it at this code.
</p>
<div class="flex justify-center p-4 mt-5 bg-white rounded-lg">
<.qr_code
value="otpauth://totp/Petal:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=Petal"
background="white"
label="QR code to enrol your authenticator app"
class="size-44 text-gray-900"
/>
</div>
<div class="mt-4 text-xs text-gray-500 dark:text-gray-400">
Can't scan? Enter this key by hand:
</div>
<div class="px-3 py-2 mt-1 font-mono text-sm text-gray-900 rounded bg-gray-100 dark:bg-gray-800 dark:text-gray-100">
JBSW Y3DP EHPK 3PXP
</div>
</div>
On a dark surface

Scanners want dark modules on a light background. On the left, the safe default: a white card behind the code, readable by anything. On the right, inverted - it looks better, most modern phones cope, older scanners may not.

safe: dark on light
inverted: test your scanners
heex
<div class="grid w-full gap-6 p-6 bg-gray-900 sm:grid-cols-2 rounded-xl">
<div class="text-center">
<div class="inline-flex p-4 bg-white rounded-xl">
<.qr_code value="https://petal.build" background="white" class="size-32 text-gray-900" />
</div>
<div class="mt-3 text-xs text-gray-400">safe: dark on light</div>
</div>
<div class="text-center">
<div class="inline-flex p-4">
<.qr_code value="https://petal.build" class="size-32 text-white" />
</div>
<div class="mt-3 text-xs text-gray-400">inverted: test your scanners</div>
</div>
</div>

The logo slot knocks a hole in the middle and bumps error correction to :h, so the code survives the missing modules. Slot content is laid out in a 100x100 box scaled to fit the hole.

Share this page

Small enough to sit in a card footer next to a copy-link button. The URL stays visible as text.

Open on your phone
petal.build/components
heex
<div class="flex items-center w-full max-w-md gap-4 p-4 mx-auto border border-gray-200 rounded-xl dark:border-gray-800">
<div class="p-2 bg-white rounded-lg shrink-0">
<.qr_code
value="https://petal.build/components"
background="white"
label="QR code linking to the component library"
class="size-20 text-gray-900"
/>
</div>
<div class="min-w-0">
<div class="text-sm font-medium text-gray-900 dark:text-white">Open on your phone</div>
<div class="mt-1 text-xs text-gray-500 truncate dark:text-gray-400">
petal.build/components
</div>
<.button size="xs" variant="outline" label="Copy link" class="mt-3" />
</div>
</div>
Properties
Attribute Type Default Description
background string "transparent" fill behind the modules, quiet zone included; transparent by default. Set it (e.g. "white") when placing the code on a dark or busy surface
class any nil size + colour, e.g. "size-48 text-gray-900 dark:text-white"
color string "currentColor" module (dark) colour; currentColor by default so it themes with text-* classes and prints
error_correction atom :m error correction level, low to high; forced to :h when the logo slot is used
one of: :l, :m, :q, :h
label string "QR code" accessible name for the code. Never derived from value, which is often a secret
rest global
rounded any 0 0..1 module corner rounding: 0 = square modules, 1 = fully round dots
size any nil optional pixel size, setting width/height attributes; omit it and drive the size with classes instead, e.g. class="size-48"
value* string the string to encode (URL, otpauth:// URI, WiFi string, arbitrary text)
:logo slot slot optional centre logo, rendered in a knocked-out hole in the middle of the code. Forces error_correction to :h. Content is laid out in a 100x100 box scaled to fit the hole