Heart-module codes, explained
A heart-module code is an ordinary QR code whose dark cells are drawn as hearts instead of squares. That sentence hides two decisions the engine makes for you and one gate it refuses to open. Every figure below is computed from the engine when the page is built: the example is rendered by the code behind the tools, and the colour verdicts come from the renderer itself.
Which keepsakes start on hearts
The generator calls the style "Heart modules". Of the occasions with a page of their own, 2 start on it: Valentine and Pet tag . The Proposal preset uses hearts a second way — heart modules set inside the heart frame — so the rules in the framed-codes guide apply on top.
A worked example, rendered by the real engine
The Valentine preset's example, rendered at build time by the heart-module engine. The note is fictional. The code decodes to:
Every love story is beautiful, but ours is my favorite. Happy Valentine’s Day, A.
Version 8 at 49×49 modules, error correction H; 57×57 modules with the quiet zone. Hearts at 97% cell coverage in #9f1239.
What becomes a heart, and what never does
Before drawing anything, the renderer classifies every module into one of 4 zones: the finder patterns (the three large corner squares a scanner locks onto), the alignment patterns (the smaller inner squares that orient denser codes), the 2 timing tracks, and the data area. Finder and alignment modules are drawn as classic squares, always — in this engine's testing, restyling alignment patterns broke decoding outright, so landmarks are never decorated in any style. Timing and data modules become hearts.
Counted on the example: of its 1263 dark modules, 201 sit in the finder patterns (99) or its 6 alignment patterns (102) and stay square; the other 1062 — 28 on the timing tracks, 1034 in the data area — are hearts. The 4-module quiet zone is drawn as part of the image, never left for you to add. The anatomy guide covers why landmarks matter in every decorated style.
The 0.97 rule
Every heart is the same glyph — a closed path of cubic curves in a unit cell — scaled about the
centre of its cell by one factor, HEART_CELL_COVERAGE, which is 0.97. That is a measured rule of this engine, not a property of QR codes in
general. A heart spanning 97% of its cell is large enough that a scanner's
sampling grid still reads the cell as decisively dark, and small enough that neighbouring hearts
read as separate shapes to the eye. The figure is pinned by the test suite's
render-rasterise-decode loop — several sizes, rotated, after lossy JPEG — so a coverage change
that broke decoding would fail the build rather than reach this page.
Hearts always encode at level H
QR codes carry redundancy so damaged modules can be reconstructed, and every style here that redraws modules — hearts and the photo halftone — spends some of it on decoration. The heart renderer therefore refuses any symbol not encoded at the highest level, H — roughly 30% recovery — so a heart code is never smaller than the level-H version of its content. Only a framed code without heart modules may opt in to level Q, because its frame leaves the symbol untouched.
The contrast gate: refused, not warned
The second decision is colour. Scanners binarise the image, and a washed-out pair measurably hurts decoding; because the hearts already spend decode margin, the engine does not warn about weak colour, it rejects the render. The two colours must measure at least 4.5:1 by the WCAG 2.x contrast formula, and the hearts must be darker than the background, because inverted codes are rejected outright. The table shows 8 pairs run through the heart renderer when this page was built: 4 pass and 4 are refused.
| Pair | Hearts | Background | Ratio | Engine verdict |
|---|---|---|---|---|
| Rose on white (the Valentine preset) | #9f1239 | #ffffff | 8.02:1 | Accepted |
| Slate on white (the Pet tag preset) | #1f2937 | #ffffff | 14.68:1 | Accepted |
| Deep rose on white (the engine default, the Proposal preset) | #881337 | #ffffff | 9.57:1 | Accepted |
| Deep rose on blush | #881337 | #ffe4e6 | 7.97:1 | Accepted |
| Crimson on pink | #e11d48 | #fda4af | 2.48:1 | Refused — low contrast |
| Pink on white | #f472b6 | #ffffff | 2.65:1 | Refused — low contrast |
| Mid grey on white | #999999 | #ffffff | 2.85:1 | Refused — low contrast |
| White on near-black (inverted) | #ffffff | #111111 | 18.88:1 | Refused — inverted |
The refusals are the engine's own words. For the grey pair: Artistic styles require at least 4.5:1 contrast (got 2.85:1) — decoration already spends decode margin. For the inverted pair: Artistic styles require dark modules on a light background — inverted codes are rejected. A tinted background is fine so long as the hearts stay deep: deep rose on blush passes at 7.97:1, while crimson on a saturated pink fails at 2.48:1.
What a heart code can carry — and cannot
Hearts are drawn after encoding, so a heart code carries whatever the preset's payload builder produces, unchanged: the Valentine preset encodes bare text (the example above). The Pet tag preset encodes a contact card in vCard format; its example runs to 101 characters and encodes at version 10, 57×57 modules. The Proposal preset encodes a link and sets its hearts inside the heart frame, where its example comes out at version 4.
What it cannot carry is a short list, each item an engine rule:
- A logo. The heart renderer's options are a module size, a heart colour and a background colour; there is no logo option.
- A photo. Weaving a picture into the modules is the photo halftone style, with its own limits (capped at version 4, for one); see the halftone guide. The two do not stack.
- A caption, on its own. The bare style draws the symbol and its quiet zone, nothing else; hearts inside a framed style gain the frame's caption area, up to 200 characters on at most 4 lines.
- A lower error-correction level, a weak colour pair, or light hearts on a dark ground — each refused as described above.
There is no cap on a heart code's length beyond what any QR code holds, but length has a visible cost. At build time 3 fictional notes were encoded twice each: as an offline note (the words in the code) and as a reveal link (a URL to this site's reveal page with the message in the fragment, in the uncompressed form the encoder produces when no compressor is supplied). A reveal message is accepted up to 300 characters.
| Fictional note | Characters | Offline note | Reveal link |
|---|---|---|---|
| Still yes. | 10 | v2, 25×25 | 47 chars → v6, 41×41 |
| Meet me at the blue door at six. Bring the um… | 70 | v8, 49×49 | 143 chars → v12, 65×65 |
| You once asked what I would carry out of a fi… | 214 | v15, 77×77 | 374 chars → v20, 97×97 |
A higher version divides the same printed square into more cells, so each heart gets smaller: the 10-character note draws 25×25 cells, the 214-character one 77×77. The reveal form is longer because it also carries the address of the reveal page.
What the reader's phone does with it
Hearts change nothing about how a scan is handled, so a heart code made from a message inherits the message rules exactly. A QR code containing bare text carries no scheme, so a phone has nothing to act on. Apple's Camera and Google's ML Kit both recognise links, phone numbers, email addresses, contact cards, calendar events and Wi-Fi joins — and no plain-text type. Faced with bare text, an iPhone offers to copy or search it and Google Lens simply runs a web search, so the message is the one thing the reader never sees. Samsung phones and dedicated scanner apps do display it, but a keepsake cannot depend on which phone opens it.
So a message keepsake offers two modes. A reveal link encodes a normal URL to a page on this
site with the message in the URL's fragment after the #; per RFC 3986 a fragment is
stripped before a request is made, so the message never reaches a server, a CDN or a log, and is
decoded on the reader's device. An offline note writes the words into the code itself, needs no
connection, and accepts the display caveat above. In both modes, on a framed keepsake with no
caption of your own, the message is also printed under the code as the caption; on the bare Heart modules style the words are in the code alone unless the style is switched to a frame.
Contact cards are among the types those dispatch sentences list. The message-keepsakes guide works through the
fragment format.
What verification proves, and what it cannot
After every render, your browser rasterises the finished heart artwork and runs a real QR decoder, jsQR, on the pixels at 3 raster sizes — 800, 1200, 1600 px — and compares the decoded bytes to your exact content. A render can fail at exactly one raster size through sampling resonance and decode cleanly at the others, so no single size is trusted: the "Verified scannable" badge is earned when at least 2 of the 3 sizes match. Anything else shows a warning and keeps the downloads locked. The decode-verification guide goes through each verdict. The badge cannot see past the screen — your printer, your paper, or the phone that will do the scanning — and the contrast gate judges the colours you chose, not the ink that comes out.
Printing and testing a heart code
Print at least 2 × 2 cm for arm's-length scanning, or the engine's floor for your code if that is larger: the working minimum is 0.5 mm per module, quiet zone included, so the example — 57 modules across — must print at least 28.5 mm (1.12 in) wide. At that width each heart spans about 0.485 mm. Keep the quiet zone uncropped, and scan a test print with a real phone before the moment that matters. Sizing in general is in the printing guide.
Common misreadings
- Cropping to the hearts. The 4-module clear margin is part of the code, not padding, and the methodology's print guidance is to keep it uncropped.
- Expecting bare text to appear on screen. An offline note is still bare text. If the words must show on the reader's phone, use a reveal link.
- Choosing hearts for their colour first. Pale hearts such as the pink in the table (2.65:1) fail the 4.5:1 gate and are refused, not nudged. Choose a deep heart colour and tint the background instead.
- Light hearts on a dark card. Inverted codes are rejected regardless of ratio — the inverted pair in the table measures 18.88:1 and is still refused.
Heart-module FAQ
Why are the corner squares not hearts too?
The corner squares are the finder patterns a scanner locks onto, and the smaller inner squares are alignment patterns. In this engine's testing, restyling alignment patterns broke decoding outright, so both always stay classic squares. Only the timing tracks and the data area become hearts: in the Valentine example on this page, 201 of 1263 dark modules stay square and 1062 are hearts.
Can I have pink or pastel hearts?
Only if the pair still measures at least 4.5:1 in WCAG contrast with the hearts darker than the background; the engine refuses weaker pairs rather than warning. Pink hearts on white (#f472b6 on #ffffff) measure 2.65:1 and are refused; deep rose on a blush background (#881337 on #ffe4e6) measures 7.97:1 and passes.
Can a heart code carry a logo or a photo?
No. The heart renderer accepts a module size, a heart colour and a background colour and nothing else — there is no logo option. A photo woven into the code is the separate halftone style, and the two do not combine.
Does a heart code hold less than a plain code?
Not compared with a plain code at level H: the hearts are drawn after encoding by the same encoder, so version and size are identical. Compared with a plain code left at the encoder's default level, it can be several versions larger, because the hearts require H; how many depends on the content, and for the 3 fictional notes on this page the engine gives 1 version for the 10-character note (v1 at Q — boosted from the default because that cost no version — to v2 at H), 3 versions for the 70-character note (v5 at M to v8 at H) and 4 versions for the 214-character note (v11 at M to v15 at H). At H the 10-character note is 25×25 modules and the 214-character note is 77×77.
Will the hearts show my message when someone scans them?
The hearts change nothing about that. Bare text carries no scheme, so a phone has nothing to act on: an iPhone offers to copy or search it and Google Lens runs a web search, while Samsung phones and dedicated scanner apps do display it. A reveal link encodes a normal URL with the message in the fragment, decoded on the reader's device and never sent to a server; an offline note keeps the words in the code and accepts that caveat. On a framed keepsake with no caption of your own the message is also printed under the code; on the bare heart style the words are in the code alone unless the style is switched to a frame.
What does the "Verified scannable" badge prove for a heart code?
That your browser rasterised this exact artwork at 800, 1200, 1600 px, ran a real QR decoder on the pixels, and got your content back byte-for-byte at no fewer than 2 of those 3 sizes. It proves the hearts decode at screen resolution; it cannot see your printer, paper or scanning phone.
Every render is decode-verified in your browser before a download unlocks. The full set of measured rules behind this guide is on the methodology page; the other guides are listed at /guides/.