Add a QRCode component
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 5/5
- Tiempo estimado
- Más de una semana
- Aptitud para principiantes
- 15/100
- Tipo de issue
- Nueva funcionalidad
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- react, tailwindcss, typescript
- Área
- frontend
Línea de trabajo
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.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
- 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.
- Lenguaje dominante
- TypeScript
- Estrellas
- 2.1k
- Forks
- 502
- Merge medio
- 19 h 16 min
- PR fusionados (30 d)
- 2
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Tiene una plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de themesberg/flowbite-react
-
fix(Carousel): can't give dynamic children to CarouselPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto:bug: bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 62/100
themesberg/flowbite-react#1469 · 1 comentario ·
-
Pagination is broken if there is only one pagePosiblemente ocupada @Mukilan1600 la tomó hace 1019 días. Abierto:bug: bug confirmed good first issue
Dificultad 2/5 1-3 horas Aptitud para principiantes 65/100
themesberg/flowbite-react#1048 · 7 comentarios · 1 reacción ·
-
:books: documentation help wanted
Dificultad 5/5 Más de una semana Aptitud para principiantes 35/100
themesberg/flowbite-react#1696 · 2 comentarios ·
-
NavbarCollapse closes when toggling a MegaMenuDropdown on mobileQuizá libre de nuevo @sarthak-11231 la tomó hace 43 días y no hay ningún pull request abierto. Abierto
Dificultad 3/5 1-2 días Aptitud para principiantes 58/100
themesberg/flowbite-react#1686 · 2 comentarios ·
-
Dificultad 3/5 1-2 días Aptitud para principiantes 48/100
themesberg/flowbite-react#1677 ·
Todos los issues de themesberg/flowbite-react
Issues similares
-
unwrapDatabaseError masks real DB errors with TypeError: Cannot convert object to primitive valueAbierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 74/100
NEARBuilders/everything-dev#798 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
vercel-labs/durable-harness#53 ·
Los mantenedores suelen responder en 1 día
-
[Bug]: Redesign tab bars ignore clicks on Linux: hidden chat layers keep window drag regionsPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
experiência: a doc manda subir para o Micro do Supabase, mas não diz que exige plano pago nem quanto custaPosiblemente ocupada Un pull request vinculado a esta issue está abierto o ya se fusionó. Abiertoarea/docs area/ui enhancement
Dificultad 1/5 1-3 horas Aptitud para principiantes 88/100
melgarafael/DeskcommCRM#4098 ·
Los mantenedores suelen responder en 1 día
-
dependencies platform: ios
Dificultad 1/5 1-3 horas Aptitud para principiantes 78/100
software-mansion/react-native-executorch#1617 ·
Los mantenedores suelen responder en 1 día