QR Code
A flexible QR code component for generating and displaying QR codes with customization options.
import { QRCode, QRCodeCanvas } from "@/components/ui/qr-code";
export function QRCodeDemo() {
return (
<QRCode value="https://diceui.com" size={200}>
<QRCodeCanvas />
</QRCode>
);
}
Installation
CLI
npx shadcn@latest add "https://diceui.com/r/qr-code"
Manual
Install the following dependencies:
npm install @radix-ui/react-slot qrcode
Install the type definitions:
npm install @types/qrcode
Copy and paste the following code into your project.
"use client";
import { Slot } from "@radix-ui/react-slot";
import * as React from "react";
import { useComposedRefs } from "@/lib/compose-refs";
import { cn } from "@/lib/utils";
const ROOT_NAME = "QRCode";
const IMAGE_NAME = "QRCodeImage";
const CANVAS_NAME = "QRCodeCanvas";
const SVG_NAME = "QRCodeSvg";
type QRCodeLevel = "L" | "M" | "Q" | "H";
interface QRCodeCanvasOpts {
errorCorrectionLevel: QRCodeLevel;
type?: "image/png" | "image/jpeg" | "image/webp";
quality?: number;
margin?: number;
color?: {
dark: string;
light: string;
};
width?: number;
rendererOpts?: {
quality?: number;
};
}
function useLazyRef<T>(fn: () => T) {
const ref = React.useRef<T | null>(null);
if (ref.current === null) {
ref.current = fn();
}
return ref as React.RefObject<T>;
}
interface StoreState {
dataUrl: string | null;
svgString: string | null;
isGenerating: boolean;
error: Error | null;
generationKey: string;
}
interface Store {
subscribe: (callback: () => void) => () => void;
getState: () => StoreState;
setState: <K extends keyof StoreState>(key: K, value: StoreState[K]) => void;
setStates: (updates: Partial<StoreState>) => void;
notify: () => void;
}
interface QRCodeContextValue {
value: string;
size: number;
margin: number;
level: QRCodeLevel;
backgroundColor: string;
foregroundColor: string;
canvasRef: React.RefObject<HTMLCanvasElement | null>;
}
function createStore(
listenersRef: React.RefObject<Set<() => void>>,
stateRef: React.RefObject<StoreState>,
): Store {
const store: Store = {
subscribe: (cb) => {
listenersRef.current.add(cb);
return () => listenersRef.current.delete(cb);
},
getState: () => stateRef.current,
setState: (key, value) => {
if (Object.is(stateRef.current[key], value)) return;
stateRef.current[key] = value;
store.notify();
},
setStates: (updates) => {
let hasChanged = false;
for (const key of Object.keys(updates) as Array<keyof StoreState>) {
const value = updates[key];
if (value !== undefined && !Object.is(stateRef.current[key], value)) {
Object.assign(stateRef.current, { [key]: value });
hasChanged = true;
}
}
if (hasChanged) {
store.notify();
}
},
notify: () => {
for (const cb of listenersRef.current) {
cb();
}
},
};
return store;
}
const StoreContext = React.createContext<Store | null>(null);
function useStoreContext(consumerName: string) {
const store = React.useContext(StoreContext);
if (!store) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return store;
}
function useStore<T>(selector: (state: StoreState) => T): T {
const store = useStoreContext("useStore");
const getSnapshot = React.useCallback(
() => selector(store.getState()),
[store, selector],
);
return React.useSyncExternalStore(store.subscribe, getSnapshot, getSnapshot);
}
const QRCodeContext = React.createContext<QRCodeContextValue | null>(null);
function useQRCodeContext(consumerName: string) {
const context = React.useContext(QRCodeContext);
if (!context) {
throw new Error(`\`${consumerName}\` must be used within \`${ROOT_NAME}\``);
}
return context;
}
interface QRCodeRootProps extends Omit<React.ComponentProps<"div">, "onError"> {
value: string;
size?: number;
level?: QRCodeLevel;
margin?: number;
quality?: number;
backgroundColor?: string;
foregroundColor?: string;
onError?: (error: Error) => void;
onGenerated?: () => void;
asChild?: boolean;
}
function QRCodeRoot(props: QRCodeRootProps) {
const {
value,
size = 200,
level = "M",
margin = 1,
quality = 0.92,
backgroundColor = "#ffffff",
foregroundColor = "#000000",
onError,
onGenerated,
className,
style,
asChild,
...rootProps
} = props;
const listenersRef = useLazyRef(() => new Set<() => void>());
const canvasRef = React.useRef<HTMLCanvasElement>(null);
const stateRef = useLazyRef<StoreState>(() => ({
dataUrl: null,
svgString: null,
isGenerating: false,
error: null,
generationKey: "",
}));
const store = React.useMemo(
() => createStore(listenersRef, stateRef),
[listenersRef, stateRef],
);
const canvasOpts = React.useMemo<QRCodeCanvasOpts>(
() => ({
errorCorrectionLevel: level,
type: "image/png",
quality,
margin,
color: {
dark: foregroundColor,
light: backgroundColor,
},
width: size,
}),
[level, margin, foregroundColor, backgroundColor, size, quality],
);
const generationKey = React.useMemo(() => {
if (!value) return "";
return JSON.stringify({
value,
size,
level,
margin,
quality,
foregroundColor,
backgroundColor,
});
}, [value, level, margin, foregroundColor, backgroundColor, size, quality]);
const onQRCodeGenerate = React.useCallback(
async (targetGenerationKey: string) => {
if (!value || !targetGenerationKey) return;
const currentState = store.getState();
if (
currentState.isGenerating ||
currentState.generationKey === targetGenerationKey
)
return;
store.setStates({
isGenerating: true,
error: null,
});
try {
const QRCode = (await import("qrcode")).default;
let dataUrl: string | null = null;
try {
dataUrl = await QRCode.toDataURL(value, canvasOpts);
} catch {
dataUrl = null;
}
if (canvasRef.current) {
await QRCode.toCanvas(canvasRef.current, value, canvasOpts);
}
const svgString = await QRCode.toString(value, {
errorCorrectionLevel: canvasOpts.errorCorrectionLevel,
margin: canvasOpts.margin,
color: canvasOpts.color,
width: canvasOpts.width,
type: "svg",
});
store.setStates({
dataUrl,
svgString,
isGenerating: false,
generationKey: targetGenerationKey,
});
onGenerated?.();
} catch (error) {
const parsedError =
error instanceof Error
? error
: new Error("Failed to generate QR code");
store.setStates({
error: parsedError,
isGenerating: false,
});
onError?.(parsedError);
}
},
[value, canvasOpts, store, onError, onGenerated],
);
const contextValue = React.useMemo<QRCodeContextValue>(
() => ({
value,
size,
level,
margin,
backgroundColor,
foregroundColor,
canvasRef,
}),
[value, size, backgroundColor, foregroundColor, level, margin],
);
React.useLayoutEffect(() => {
if (generationKey) {
const rafId = requestAnimationFrame(() => {
onQRCodeGenerate(generationKey);
});
return () => cancelAnimationFrame(rafId);
}
}, [generationKey, onQRCodeGenerate]);
const RootPrimitive = asChild ? Slot : "div";
return (
<StoreContext.Provider value={store}>
<QRCodeContext.Provider value={contextValue}>
<RootPrimitive
data-slot="qr-code"
{...rootProps}
className={cn(className, "flex flex-col items-center gap-2")}
style={
{
"--qr-code-size": `${size}px`,
...style,
} as React.CSSProperties
}
/>
</QRCodeContext.Provider>
</StoreContext.Provider>
);
}
interface QRCodeImageProps extends React.ComponentProps<"img"> {
asChild?: boolean;
}
function QRCodeImage(props: QRCodeImageProps) {
const { alt = "QR Code", asChild, className, ...imageProps } = props;
const context = useQRCodeContext(IMAGE_NAME);
const dataUrl = useStore((state) => state.dataUrl);
if (!dataUrl) return null;
const ImagePrimitive = asChild ? Slot : "img";
return (
<ImagePrimitive
data-slot="qr-code-image"
{...imageProps}
src={dataUrl}
alt={alt}
width={context.size}
height={context.size}
className={cn("max-h-(--qr-code-size) max-w-(--qr-code-size)", className)}
/>
);
}
interface QRCodeCanvasProps extends React.ComponentProps<"canvas"> {
asChild?: boolean;
}
function QRCodeCanvas(props: QRCodeCanvasProps) {
const { asChild, className, ref, ...canvasProps } = props;
const context = useQRCodeContext(CANVAS_NAME);
const composedRef = useComposedRefs(ref, context.canvasRef);
const CanvasPrimitive = asChild ? Slot : "canvas";
return (
<CanvasPrimitive
data-slot="qr-code-canvas"
{...canvasProps}
ref={composedRef}
width={context.size}
height={context.size}
className={cn("max-h-(--qr-code-size) max-w-(--qr-code-size)", className)}
/>
);
}
interface QRCodeSvgProps extends React.ComponentProps<"div"> {
asChild?: boolean;
}
function QRCodeSvg(props: QRCodeSvgProps) {
const { asChild, className, ...svgProps } = props;
const context = useQRCodeContext(SVG_NAME);
const svgString = useStore((state) => state.svgString);
if (!svgString) return null;
const SvgPrimitive = asChild ? Slot : "div";
return (
<SvgPrimitive
data-slot="qr-code-svg"
{...svgProps}
className={cn("max-h-(--qr-code-size) max-w-(--qr-code-size)", className)}
style={{ width: context.size, height: context.size, ...svgProps.style }}
dangerouslySetInnerHTML={{ __html: svgString }}
/>
);
}
interface QRCodeDownloadProps extends React.ComponentProps<"button"> {
filename?: string;
format?: "png" | "svg";
asChild?: boolean;
}
function QRCodeDownload(props: QRCodeDownloadProps) {
const {
filename = "qrcode",
format = "png",
asChild,
className,
children,
...buttonProps
} = props;
const dataUrl = useStore((state) => state.dataUrl);
const svgString = useStore((state) => state.svgString);
const onClick = React.useCallback(
(event: React.MouseEvent<HTMLButtonElement>) => {
buttonProps.onClick?.(event);
if (event.defaultPrevented) return;
const link = document.createElement("a");
if (format === "png" && dataUrl) {
link.href = dataUrl;
link.download = `${filename}.png`;
} else if (format === "svg" && svgString) {
const blob = new Blob([svgString], { type: "image/svg+xml" });
link.href = URL.createObjectURL(blob);
link.download = `${filename}.svg`;
} else {
return;
}
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
if (format === "svg" && svgString) {
URL.revokeObjectURL(link.href);
}
},
[dataUrl, svgString, filename, format, buttonProps.onClick],
);
const ButtonPrimitive = asChild ? Slot : "button";
return (
<ButtonPrimitive
type="button"
data-slot="qr-code-download"
{...buttonProps}
className={cn("max-w-(--qr-code-size)", className)}
onClick={onClick}
>
{children ?? `Download ${format.toUpperCase()}`}
</ButtonPrimitive>
);
}
export {
QRCodeRoot as Root,
QRCodeImage as Image,
QRCodeCanvas as Canvas,
QRCodeSvg as Svg,
QRCodeDownload as Download,
//
QRCodeRoot as QRCode,
QRCodeImage,
QRCodeCanvas,
QRCodeSvg,
QRCodeDownload,
//
useStore as useQRCode,
//
type QRCodeRootProps as QRCodeProps,
};
Layout
Import the parts, and compose them together.
import { QRCode } from "@/components/ui/qr-code";
<QRCode value="https://example.com">
<QRCode.Canvas />
</QRCode>
Examples
Different Formats
Render QR codes as Canvas, SVG, or Image elements.
import { Button } from "@/components/ui/button";
import {
QRCode,
QRCodeCanvas,
QRCodeDownload,
QRCodeImage,
QRCodeSvg,
} from "@/components/ui/qr-code";
const value = "https://diceui.com";
export function QRCodeFormatsDemo() {
return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div className="flex flex-col items-center gap-2">
<QRCode value={value} size={120}>
<QRCodeCanvas />
<QRCodeDownload format="png" filename="qr-canvas" asChild>
<Button size="sm">Download PNG</Button>
</QRCodeDownload>
</QRCode>
<p className="text-muted-foreground text-sm">Rendered as canvas</p>
</div>
<div className="flex flex-col items-center gap-2">
<QRCode value={value} size={120}>
<QRCodeSvg />
<QRCodeDownload format="svg" filename="qr-svg" asChild>
<Button size="sm">Download SVG</Button>
</QRCodeDownload>
</QRCode>
<p className="text-muted-foreground text-sm">Rendered as SVG</p>
</div>
<div className="flex flex-col items-center gap-2">
<QRCode value={value} size={120}>
<QRCodeImage alt="DiceUI QR Code" />
<QRCodeDownload format="png" filename="qr-image" asChild>
<Button size="sm">Download PNG</Button>
</QRCodeDownload>
</QRCode>
<p className="text-muted-foreground text-sm">Rendered as image</p>
</div>
</div>
);
}
Customization
Customize colors, size, and error correction levels.
import { QRCode, QRCodeCanvas } from "@/components/ui/qr-code";
export function QRCodeCustomizationDemo() {
return (
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
<div className="flex flex-col items-center gap-2">
<QRCode
value="https://diceui.com"
size={150}
foregroundColor="#3b82f6"
backgroundColor="#f1f5f9"
>
<QRCodeCanvas />
</QRCode>
<p className="text-muted-foreground text-sm">Custom Colors</p>
</div>
<div className="flex flex-col items-center gap-2">
<QRCode
value="https://diceui.com"
size={150}
level="H"
foregroundColor="#dc2626"
>
<QRCodeCanvas />
</QRCode>
<p className="text-muted-foreground text-sm">High Error Correction</p>
</div>
</div>
);
}
API Reference
Root
The main container component that provides context for QR code generation.
Prop
Type
CSS Variable | Description | Default |
---|---|---|
--qr-code-size | The size of the QR code in pixels. Used to constrain child elements to the QR code dimensions. | Based on size prop (e.g., 200px) |
Image
Renders the QR code as an HTML image element.
Prop
Type
Canvas
Renders the QR code using HTML5 Canvas.
Prop
Type
Svg
Renders the QR code as an SVG element.
Prop
Type
Download
A button component for downloading the QR code.
Prop
Type
Accessibility
Keyboard Interactions
Key | Description |
---|---|
Enter | Activates the download button when focused. |
Space | Activates the download button when focused. |
Error Correction Levels
QR codes support different error correction levels that determine how much of the code can be damaged while still being readable:
- L (Low): ~7% of data can be restored
- M (Medium): ~15% of data can be restored (default)
- Q (Quartile): ~25% of data can be restored
- H (High): ~30% of data can be restored
Higher error correction levels result in denser QR codes but provide better resilience to damage or distortion.
Usage Notes
- The component uses dynamic imports to avoid SSR issues with the QR code library
- Canvas rendering provides the best performance for static QR codes
- SVG rendering is ideal for scalable, print-ready QR codes
- The download functionality works in all modern browsers
- QR codes are generated client-side for privacy and performance
- Child elements are automatically constrained by the
--qr-code-size
CSS variable to prevent layout issues