Add a QRCode component
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 5/5
- Tempo stimato
- Più di una settimana
- Idoneità per principianti
- 15/100
- Tipo di issue
- Funzionalità
- Chiarezza
- Abbastanza chiara
- Stato di attività
- Attiva
- Stack tecnologico
- react, tailwindcss, typescript
- Ambito
- frontend
Direzione di ricerca
Nothing is actionable until maintainers answer the two questions in the thread: whether a QRCode component belongs in the library at all, and whether the flat prop shape is acceptable. If approved, work lands in packages/ui/src/components/QRCode/ with registration in src/index.ts, src/theme/index.ts and src/types/index.ts, a subpath entry in packages/ui/rollup.config.js, a regenerated schema.json, plus apps/web docs and apps/storybook/src/QRCode.stories.tsx. Read the fork's preview builds and the 18 tests before touching anything; done means those files exist and lint, format:check, typecheck and build pass.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
- I have searched the Issues to see if this bug has already been reported
- I have tested the latest version
Summary
Flowbite has a QR code component,
added in themesberg/flowbite#1109, where the examples are static SVG pasted into
the page. Flowbite React has no equivalent, so anyone who needs to turn a value
into a symbol reaches outside the design system for it.
I would like to contribute <QRCode>, matching the Flowbite page and going
past it on styling, which the static SVG examples there cannot offer.
It is written and passing the repo's checks, but I have not pushed the branch or
opened a PR. Two things are yours to decide first: whether you want it at all,
and whether the prop shape suits you. Both are spelled out below, and answering
them is enough for me to open the PR.
It is deployed from that branch already, so it can be tried before any code is
read:
- Docs page: https://flowbite-react-qrcode.vercel.app/docs/components/qr-code
- Storybook, with a control for every prop: https://flowbite-react-qrcode-storybook.vercel.app/?path=/story/components-qrcode--default
Both are preview builds of a fork on my own account, not anything official.
import { QRCode } from "flowbite-react";
<QRCode value="https://flowbite-react.com" />
<QRCode value="..." dotStyle="rounded" cornerStyle="extra-rounded" cornerColor="#14b8a6" />
<QRCode value="..." logo={{ src: "/logo.svg", size: 0.6, radius: 1, hideDots: true }} />
| Prop | Type | Default |
|---|---|---|
value |
string |
required |
size |
number |
256 |
margin |
number |
4 |
dotStyle |
"square" | "circle" | "rounded" |
"square" |
dotColor |
string |
"currentColor" |
cornerStyle |
"square" | "rounded" | "extra-rounded" | "circle" |
"square" |
cornerColor |
string |
follows dotColor |
cornerDotStyle |
"square" | "rounded" | "circle" |
follows cornerStyle |
cornerDotColor |
string |
follows dotColor |
backgroundColor |
string |
"transparent" |
errorCorrectionLevel |
"L" | "M" | "Q" | "H" |
"M", or raised to fit logo.size |
logo |
{ src, size?, margin?, radius?, aspectRatio?, hideDots? } |
none |
title |
string |
QR code: {value} |
Theme key qrCode, one root.base slot. Defaulting the colours to
currentColor on a transparent background means the symbol follows the text
colour, so dark mode works through the existing theme with no extra prop, and a
utility class such as text-purple-700 recolours it.
The component is a plain render: useId and a memo, no effects, no state,
nothing to measure. It server renders without a DOM.
Context
QR codes show up in ordinary product work rather than in a niche: two factor
enrolment, payment strings, share and invite links, event tickets, restaurant
menus, device pairing. The Flowbite page lists much the same set.
Beyond parity, the styling is the part people currently leave the design system
for. A symbol that can take the brand colour, rounded modules and a centre logo
is the common ask, and the usual answer, qr-code-styling, weighs more than
this whole proposal adds and cannot server render.
What it costs
Importing QRCode next to a component already in use adds 8,904 bytes gzip, and
a bundle that never imports it gains nothing: esbuild, minified, gzip -9, React
external.
The geometry comes from @ttsalpha/qrcode/core, which I wrote, so take the
rest of this section with that in mind: MIT, zero runtime dependencies, ESM and
CJS, no React and no DOM, 8,185 bytes gzip on its own. Flowbite React keeps the
rendering, so the theme, the twMerge handling and the markup stay yours.
| Library | minified | gzip |
|---|---|---|
qrcode.react 4.2.0 |
16,631 | 6,253 |
@ttsalpha/qrcode/core 3.1.0 |
18,301 | 8,185 |
react-qr-code 2.2.0 |
24,789 | 9,129 |
qr-code-styling 1.9.2 |
48,499 | 14,857 |
To be straight about it: qrcode.react is smaller and would stay smaller. It
draws square modules in one colour and nothing else, so it cannot cover what
this proposal is for. Among the libraries that can style a symbol, this is the
smallest.
On the risk of taking it on: 13 releases since May 2026 under semver with a
Keep a Changelog history, no runtime dependencies so nothing transitive comes
with it, and the component imports the /core subpath only, which carries
neither React nor DOM code. The whole call surface is one file,
QRCode/engine.ts, re-exporting buildQR and its types, so if the dependency
ever has to change the rest of the component does not.
The styling is not decoration that quietly stops scanning: the 20 variants
covering every style the docs show are rendered, rasterised and decoded back,
including a logo at the maximum safe size and the inverted symbol dark mode
produces.
Render speed, the numbers in full and the scripts behind them are at the end.
What the PR would contain
packages/ui/src/components/QRCode/with component, theme, barrel and 18 tests- Registration in
src/index.ts,src/theme/index.ts,src/types/index.ts - The dependency in
packages/ui/package.jsonandbun.lock, a three line
lockfile diff - One line in
packages/ui/rollup.config.js, adding the subpath toexternal
alongside the entries already there such asklona/json packages/ui/schema.json, regeneratedapps/webdocs page, six examples, sidebar entryapps/storybook/src/QRCode.stories.tsx- A changeset
It passes lint, format:check, typecheck and build. The test suite passes
too, apart from one DarkThemeToggle test that already fails on a clean
checkout of main, so it is not something this adds.
The two decisions:
- Is a QR code component something you want in Flowbite React at all, or would
you rather it stayed outside the library? - Does the prop shape suit you? It is flat, with
logoas the only object, and
the theme has a singleroot.baseslot. I can reshape either.
Measurements
The numbers, and the scripts that produced them
Run on an Apple M4. renderToString, React 19.2.0 in its production build,
median of 3 rounds of 2000 renders:
| Library | renders/s |
|---|---|
proposed <QRCode> |
12,776 |
proposed <QRCode>, styled |
9,426 |
qrcode.react 4.2.0 |
2,868 |
react-qr-code 2.2.0 |
1,721 |
qr-code-styling 1.9.2 is absent because it needs a DOM and does not survive
renderToString.
Bundle, esbuild, minified, gzip -9, React external:
| Bundle | gzip |
|---|---|
Progress |
15,437 |
Progress + QRCode |
24,341 |
| delta | 8,904 |
Separately, and measuring the library rather than the component proposed here,
there is a published benchmark of the encoder on CI: ubuntu-24.04, Node
v24.21.0, ECL pinned to M, median of 3 runs,
https://qrcode.ttsalpha.com/benchmark.
| Library | renders/s |
|---|---|
@ttsalpha/qrcode, toSVGString |
7,817 |
qrcode, headless |
4,527 |
qrcode.react |
1,688 |
react-qr-code |
991 |
It is worth quoting only as calibration between two different machines. That
runner is about 1.7x slower than mine on qrcode.react and react-qr-code, and
once that is taken out the proposed component lands where toSVGString lands
rather than where the library's own React component lands, which is what a
render with no effects and no state should do.
Run against a built packages/ui. React, qrcode.react, react-qr-code and
qr-code-styling are installed in a throwaway folder next to the repo.
render-bench.mjs
// Measures the path flowbite-react would actually ship: buildQR geometry
// rendered by a flowbite-react component, against the React QR libraries a
// maintainer would reach for instead. Server rendering, so the numbers are not
// clouded by jsdom.
import { createElement } from "react";
import { renderToString } from "react-dom/server";
import { QRCode } from "../flowbite-react/packages/ui/dist/index.js";
import { QRCodeSVG } from "qrcode.react";
import ReactQRCode from "react-qr-code";
const ITERATIONS = 2000;
const ROUNDS = 3;
const subjects = [
{
name: "flowbite-react QRCode",
render: (value) => renderToString(createElement(QRCode, { value })),
},
{
name: "flowbite-react QRCode (styled)",
render: (value) =>
renderToString(
createElement(QRCode, {
value,
dotStyle: "rounded",
cornerStyle: "extra-rounded",
cornerColor: "#14b8a6",
}),
),
},
{
name: "qrcode.react",
render: (value) => renderToString(createElement(QRCodeSVG, { value })),
},
{
name: "react-qr-code",
render: (value) => renderToString(createElement(ReactQRCode, { value })),
},
];
function time(subject) {
for (let i = 0; i < 200; i++)
subject.render(`https://flowbite-react.com/warmup/${i}`);
const start = process.hrtime.bigint();
for (let i = 0; i < ITERATIONS; i++)
subject.render(`https://flowbite-react.com/docs/${i}`);
return Number(process.hrtime.bigint() - start) / 1e6;
}
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
console.log(
`node ${process.version} | ${ITERATIONS} renders x ${ROUNDS} rounds, median\n`,
);
console.log(
"library".padEnd(32) + "renders/s".padStart(12) + "ms/render".padStart(12),
);
for (const subject of subjects) {
const ms = median(Array.from({ length: ROUNDS }, () => time(subject)));
const perSecond = Math.round((ITERATIONS / ms) * 1000);
console.log(
subject.name.padEnd(32) +
String(perSecond).padStart(12) +
(ms / ITERATIONS).toFixed(3).padStart(12),
);
}
run-render-bench.sh
#!/bin/bash
# flowbite-react's dist resolves `react` from the fork's node_modules while the
# bench resolves its own, which React rejects as two copies. Bundling with one
# alias for each collapses them, and the production define keeps the numbers off
# React's development paths.
set -e
cd "$(dirname "$0")"
HERE=$(pwd)
./node_modules/.bin/esbuild render-bench.mjs --bundle --platform=node --format=cjs \
--define:process.env.NODE_ENV='"production"' \
--alias:react="$HERE/node_modules/react" \
--alias:react-dom="$HERE/node_modules/react-dom" \
--outfile=out-render-bench.cjs --log-level=error
node out-render-bench.cjs
measure-delta.sh
#!/bin/bash
# What a flowbite-react consumer pays for QRCode: the gzip difference between a
# bundle that imports it and one that does not, plus a check that a consumer who
# leaves it alone pays nothing.
set -e
cd "$(dirname "$0")"
UI=../flowbite-react/packages/ui/dist/index.js
build() {
echo "export { $2 } from \"$UI\";" > "tmp-$1.js"
./node_modules/.bin/esbuild "tmp-$1.js" --bundle --minify --format=esm \
--external:react --external:react-dom --external:react/jsx-runtime \
--outfile="out-$1.js" --log-level=error
rm "tmp-$1.js"
}
build baseline "Progress"
build withqr "Progress, QRCode"
base_min=$(wc -c < out-baseline.js)
base_gz=$(gzip -9 -c out-baseline.js | wc -c)
qr_min=$(wc -c < out-withqr.js)
qr_gz=$(gzip -9 -c out-withqr.js | wc -c)
printf '%-28s %10s %10s\n' bundle minified gzip
printf '%-28s %10s %10s\n' "Progress" "$base_min" "$base_gz"
printf '%-28s %10s %10s\n' "Progress + QRCode" "$qr_min" "$qr_gz"
printf '%-28s %10s %10s\n' "delta" "$((qr_min - base_min))" "$((qr_gz - base_gz))"
if grep -q "flowbite-qr-code" out-baseline.js; then
echo "tree-shaking: FAIL, QRCode reached a bundle that never imports it"
else
echo "tree-shaking: ok, no QRCode code in the bundle that never imports it"
fi
measure-size.sh
#!/bin/bash
# Minified + gzip -9 size of each QR library, React kept external.
set -e
cd "$(dirname "$0")"
printf '%-22s %10s %10s\n' library minified gzip
for entry in libs/*.js; do
name=$(basename "$entry" .js)
./node_modules/.bin/esbuild "$entry" --bundle --minify --format=esm \
--external:react --external:react-dom --external:react/jsx-runtime \
--outfile="out-lib-$name.js" --log-level=error
min=$(wc -c < "out-lib-$name.js")
gz=$(gzip -9 -c "out-lib-$name.js" | wc -c)
printf '%-22s %10s %10s\n' "$name" "$min" "$gz"
done
scan-check.mjs
// Renders every documented variant, rasterises it and decodes it back. A style
// that a decoder cannot read has no business being in the docs.
import { mkdirSync, writeFileSync } from "node:fs";
import { createElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { Resvg } from "@resvg/resvg-js";
import jsQR from "jsqr";
import { QRCode } from "../flowbite-react/packages/ui/dist/index.js";
const VALUE = "https://flowbite-react.com";
const LOGO =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E" +
"%3Crect width='48' height='48' rx='12' fill='%231a56db'/%3E" +
"%3Cpath d='M15 24h18M24 15v18' stroke='white' stroke-width='5' stroke-linecap='round'/%3E%3C/svg%3E";
// `card` is what sits behind the symbol when it is drawn on a transparent
// background, which is how the docs render it: the page card shows through.
const LIGHT = "#ffffff";
const DARK = "#111827";
const variants = [
["default", {}],
["dot-square", { dotStyle: "square" }],
["dot-circle", { dotStyle: "circle" }],
["dot-rounded", { dotStyle: "rounded" }],
["corner-square", { cornerStyle: "square" }],
["corner-rounded", { cornerStyle: "rounded" }],
["corner-extra-rounded", { cornerStyle: "extra-rounded" }],
["corner-circle", { cornerStyle: "circle", cornerDotStyle: "circle" }],
["colors", { dotColor: "#1a56db", backgroundColor: "#eff6ff" }],
[
"colors-split",
{
dotStyle: "rounded",
dotColor: "#111827",
cornerColor: "#14b8a6",
cornerDotColor: "#f43f5e",
},
],
["ecl-L", { errorCorrectionLevel: "L" }],
["ecl-M", { errorCorrectionLevel: "M" }],
["ecl-Q", { errorCorrectionLevel: "Q" }],
["ecl-H", { errorCorrectionLevel: "H" }],
["logo-default", { logo: { src: LOGO } }],
[
"logo-max",
{ logo: { src: LOGO, size: 1, margin: 1, radius: 1, hideDots: true } },
],
[
"logo-styled",
{
logo: { src: LOGO, size: 0.6, margin: 1, radius: 1, hideDots: true },
dotStyle: "rounded",
cornerStyle: "extra-rounded",
},
],
// Dark mode: currentColor turns the modules light and the card stays dark, so
// the symbol is inverted. Scanners have to cope with that for the theme story
// in the docs to hold.
["dark-mode", { dotColor: "#ffffff" }, DARK],
["dark-mode-rounded", { dotColor: "#ffffff", dotStyle: "rounded" }, DARK],
[
"dark-mode-corners",
{ dotColor: "#ffffff", cornerColor: "#14b8a6", cornerDotColor: "#f43f5e" },
DARK,
],
];
mkdirSync("scan-out", { recursive: true });
let failures = 0;
console.log("variant".padEnd(24) + "decoded".padStart(10));
for (const [name, props, card = LIGHT] of variants) {
const svg = renderToStaticMarkup(
createElement(QRCode, { value: VALUE, size: 512, ...props }),
);
writeFileSync(`scan-out/${name}.svg`, svg);
const png = new Resvg(svg, {
fitTo: { mode: "width", value: 512 },
background: card,
}).render();
writeFileSync(`scan-out/${name}.png`, png.asPng());
const { width, height, pixels } = png;
const decoded = jsQR(Uint8ClampedArray.from(pixels), width, height);
const ok = decoded?.data === VALUE;
if (!ok) failures++;
console.log(
name.padEnd(24) +
(ok ? "ok" : decoded ? "WRONG VALUE" : "FAILED").padStart(10),
);
}
console.log(
`\n${variants.length - failures}/${variants.length} variants decoded`,
);
process.exit(failures ? 1 : 0);
run-scan-check.sh
#!/bin/bash
# Same React duplication problem as the render bench, same fix.
set -e
cd "$(dirname "$0")"
HERE=$(pwd)
./node_modules/.bin/esbuild scan-check.mjs --bundle --platform=node --format=cjs \
--define:process.env.NODE_ENV='"production"' \
--alias:react="$HERE/node_modules/react" \
--alias:react-dom="$HERE/node_modules/react-dom" \
--external:@resvg/resvg-js \
--outfile=out-scan-check.cjs --log-level=error
node out-scan-check.cjs
Every number in this issue comes from one of those, so please check them rather
than take my word for any of it.
- Lingua principale
- TypeScript
- Stelle
- 2.1k
- Fork
- 502
- Merge medio
- 19h 16m
- PR unite (30g)
- 2
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di themesberg/flowbite-react
-
fix(Carousel): can't give dynamic children to CarouselForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta:bug: bug
Difficoltà 2/5 1-3 ore Idoneità per principianti 62/100
themesberg/flowbite-react#1469 · 1 commento ·
-
Pagination is broken if there is only one pageForse già presa @Mukilan1600 l’ha presa 1019 giorni fa. Aperta:bug: bug confirmed good first issue
Difficoltà 2/5 1-3 ore Idoneità per principianti 65/100
themesberg/flowbite-react#1048 · 7 commenti · 1 reazione ·
-
:books: documentation help wanted
Difficoltà 5/5 Più di una settimana Idoneità per principianti 35/100
themesberg/flowbite-react#1696 · 2 commenti ·
-
NavbarCollapse closes when toggling a MegaMenuDropdown on mobileForse di nuovo libera @sarthak-11231 l’ha presa 43 giorni fa e non c’è nessuna pull request aperta. Aperta
Difficoltà 3/5 1-2 giorni Idoneità per principianti 58/100
themesberg/flowbite-react#1686 · 2 commenti ·
-
Difficoltà 3/5 1-2 giorni Idoneità per principianti 48/100
themesberg/flowbite-react#1677 ·
Tutte le issue di themesberg/flowbite-react
Issue simili
-
unwrapDatabaseError masks real DB errors with TypeError: Cannot convert object to primitive valueAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 74/100
NEARBuilders/everything-dev#798 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
vercel-labs/durable-harness#53 ·
I maintainer di solito rispondono entro 1 giorno
-
[Bug]: Redesign tab bars ignore clicks on Linux: hidden chat layers keep window drag regionsForse già presa Una pull request collegata a questa issue è aperta o già unita. Aperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno
-
experiência: a doc manda subir para o Micro do Supabase, mas não diz que exige plano pago nem quanto custaForse già presa Una pull request collegata a questa issue è aperta o già unita. Apertaarea/docs area/ui enhancement
Difficoltà 1/5 1-3 ore Idoneità per principianti 88/100
melgarafael/DeskcommCRM#4098 ·
I maintainer di solito rispondono entro 1 giorno
-
dependencies platform: ios
Difficoltà 1/5 1-3 ore Idoneità per principianti 78/100
software-mansion/react-native-executorch#1617 ·
I maintainer di solito rispondono entro 1 giorno