QR Code
A flexible QR code component for generating and displaying QR codes with customization options.
import {
QRCode,
QRCodeCanvas,
QRCodeSkeleton,
} from "@/components/ui/qr-code";
export function QRCodeDemo() {
return (
<QRCode value="https://diceui.com" size={200}>
<QRCodeSkeleton />
<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 qrcodeInstall the type definitions:
npm install @types/qrcodeCopy the refs composition utilities into your lib/compose-refs.ts file.
/**
* @see https://github.com/radix-ui/primitives/blob/main/packages/react/compose-refs/src/compose-refs.tsx
*/
import * as React from "react";
type PossibleRef<T> = React.Ref<T> | undefined;
/**
* Set a given ref to a given value
* This utility takes care of different types of refs: callback refs and RefObject(s)
*/
function setRef<T>(ref: PossibleRef<T>, value: T) {
if (typeof ref === "function") {
return ref(value);
}
if (ref !== null && ref !== undefined) {
ref.current = value;
}
}
/**
* A utility to compose multiple refs together
* Accepts callback refs and RefObject(s)
*/
function composeRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
return (node) => {
let hasCleanup = false;
const cleanups = refs.map((ref) => {
const cleanup = setRef(ref, node);
if (!hasCleanup && typeof cleanup === "function") {
hasCleanup = true;
}
return cleanup;
});
// React <19 will log an error to the console if a callback ref returns a
// value. We don't use ref cleanups internally so this will only happen if a
// user's ref callback returns a value, which we only expect if they are
// using the cleanup functionality added in React 19.
if (hasCleanup) {
return () => {
for (let i = 0; i < cleanups.length; i++) {
const cleanup = cleanups[i];
if (typeof cleanup === "function") {
cleanup();
} else {
setRef(refs[i], null);
}
}
};
}
};
}
/**
* A custom hook that composes multiple refs
* Accepts callback refs and RefObject(s)
*/
function useComposedRefs<T>(...refs: PossibleRef<T>[]): React.RefCallback<T> {
// biome-ignore lint/correctness/useExhaustiveDependencies: we want to memoize by all values
return React.useCallback(composeRefs(...refs), refs);
}
export { composeRefs, useComposedRefs };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 CANVAS_NAME = "QRCodeCanvas";
const SVG_NAME = "QRCodeSvg";
const IMAGE_NAME = "QRCodeImage";
const SKELETON_NAME = "QRCodeSkeleton";
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>;
}
const StoreContext = React.createContext<Store | null>(null);
function useStore<T>(selector: (state: StoreState) => T): T {
const store = React.useContext(StoreContext);
if (!store) {
throw new Error(`\`useQRCode\` must be used within \`${ROOT_NAME}\``);
}
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 canvasRef = React.useRef<HTMLCanvasElement>(null);
const listenersRef = useLazyRef(() => new Set<() => void>());
const stateRef = useLazyRef<StoreState>(() => ({
dataUrl: null,
svgString: null,
isGenerating: false,
error: null,
generationKey: "",
}));
const store = React.useMemo<Store>(() => {
return {
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();
}
},
};
}, [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, "relative flex flex-col items-center gap-2")}
style={
{
"--qr-code-size": `${size}px`,
...style,
} as React.CSSProperties
}
/>
</QRCodeContext.Provider>
</StoreContext.Provider>
);
}
interface QRCodeCanvasProps extends React.ComponentProps<"canvas"> {
asChild?: boolean;
}
function QRCodeCanvas(props: QRCodeCanvasProps) {
const { asChild, className, ref, ...canvasProps } = props;
const context = useQRCodeContext(CANVAS_NAME);
const generationKey = useStore((state) => state.generationKey);
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(
"relative max-h-(--qr-code-size) max-w-(--qr-code-size)",
!generationKey && "invisible",
className,
)}
/>
);
}
interface QRCodeSvgProps extends React.ComponentProps<"div"> {
asChild?: boolean;
}
function QRCodeSvg(props: QRCodeSvgProps) {
const { asChild, className, style, ...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(
"relative max-h-(--qr-code-size) max-w-(--qr-code-size)",
className,
)}
style={{ width: context.size, height: context.size, ...style }}
dangerouslySetInnerHTML={{ __html: svgString }}
/>
);
}
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(
"relative max-h-(--qr-code-size) max-w-(--qr-code-size)",
className,
)}
/>
);
}
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>
);
}
interface QRCodeOverlayProps extends React.ComponentProps<"div"> {
asChild?: boolean;
}
function QRCodeOverlay(props: QRCodeOverlayProps) {
const { asChild, className, ...overlayProps } = props;
const OverlayPrimitive = asChild ? Slot : "div";
return (
<OverlayPrimitive
data-slot="qr-code-overlay"
{...overlayProps}
className={cn(
"-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 flex items-center justify-center rounded-sm bg-background",
className,
)}
/>
);
}
interface QRCodeSkeletonProps extends React.ComponentProps<"div"> {
asChild?: boolean;
}
function QRCodeSkeleton(props: QRCodeSkeletonProps) {
const { asChild, className, style, ...skeletonProps } = props;
const context = useQRCodeContext(SKELETON_NAME);
const dataUrl = useStore((state) => state.dataUrl);
const svgString = useStore((state) => state.svgString);
const generationKey = useStore((state) => state.generationKey);
const isLoaded = dataUrl || svgString || generationKey;
if (isLoaded) return null;
const SkeletonPrimitive = asChild ? Slot : "div";
return (
<SkeletonPrimitive
data-slot="qr-code-skeleton"
{...skeletonProps}
className={cn(
"absolute max-h-(--qr-code-size) max-w-(--qr-code-size) animate-pulse bg-accent",
className,
)}
style={{
width: context.size,
height: context.size,
...style,
}}
/>
);
}
export {
QRCodeRoot as Root,
QRCodeCanvas as Canvas,
QRCodeSvg as Svg,
QRCodeImage as Image,
QRCodeOverlay as Overlay,
QRCodeSkeleton as Skeleton,
QRCodeDownload as Download,
//
QRCodeRoot as QRCode,
QRCodeCanvas,
QRCodeSvg,
QRCodeImage,
QRCodeOverlay,
QRCodeSkeleton,
QRCodeDownload,
//
useStore as useQRCode,
//
type QRCodeRootProps as QRCodeProps,
};Layout
Import the parts, and compose them together.
import * as QRCode from "@/components/ui/qr-code";
return (
<QRCode.Root>
<QRCode.Canvas />
<QRCode.Svg />
<QRCode.Image />
<QRCode.Overlay />
<QRCode.Skeleton />
<QRCode.Download />
</QRCode.Root>
)Swap Canvas with Svg or Image to render the qr code in svg and image formats respectively.
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,
QRCodeSkeleton,
} 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 />
<QRCodeSkeleton />
</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 />
<QRCodeSkeleton />
</QRCode>
<p className="text-muted-foreground text-sm">High Error Correction</p>
</div>
</div>
);
}Overlay
Add logos, icons, or custom elements to the center of QR codes.
import { Dice4 } from "lucide-react";
import {
QRCode,
QRCodeCanvas,
QRCodeImage,
QRCodeOverlay,
QRCodeSkeleton,
QRCodeSvg,
} from "@/components/ui/qr-code";
export function QRCodeOverlayDemo() {
return (
<div className="grid grid-cols-2 gap-4 sm:grid-cols-3">
<div className="flex flex-col items-center gap-2">
<QRCode
value="https://diceui.com"
size={120}
level="H"
className="gap-4"
>
<QRCodeSkeleton />
<QRCodeCanvas />
<QRCodeOverlay className="rounded-full border-2 border-white p-2">
<Dice4 className="size-6" />
</QRCodeOverlay>
</QRCode>
<p className="text-center text-muted-foreground text-sm">
Canvas with Logo
</p>
</div>
<div className="flex flex-col items-center gap-2">
<QRCode
value="https://diceui.com"
size={120}
level="H"
className="gap-4"
>
<QRCodeSkeleton />
<QRCodeSvg />
<QRCodeOverlay className="rounded-full border-2 border-white bg-linear-to-br from-accent to-muted p-2">
<Dice4 className="size-6" />
</QRCodeOverlay>
</QRCode>
<p className="text-center text-muted-foreground text-sm">
SVG with Logo
</p>
</div>
<div className="flex flex-col items-center gap-2">
<QRCode
value="https://diceui.com"
size={120}
level="H"
className="gap-4"
>
<QRCodeSkeleton />
<QRCodeImage />
<QRCodeOverlay className="rounded-full border-2 border-white p-1.5">
<Dice4 className="size-6" />
</QRCodeOverlay>
</QRCode>
<p className="text-center text-muted-foreground text-sm">
Image with Logo
</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
Overlay
Overlays content (like logos or icons) in the center of the QR code.
Prop
Type
Skeleton
Displays a loading placeholder while the QR code is being generated. Automatically hides once the QR code is ready.
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-sizeCSS variable to prevent layout issues - When using the Overlay component, set
level="H"(High error correction) to ensure the QR code remains scannable with up to 30% coverage