QR Code
A compound component for generating QR codes with download functionality.
Installation
Install the following dependencies:
We use qrcode
for the QR code generation.
Make sure that the following shadcn/ui components are present in your project:
Copy and paste the following code into your project.
'use client';
import { cn } from '@kit/shared';
import { Button } from '@kit/ui/button';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { DownloadIcon } from 'lucide-react';
import QRCodeLib from 'qrcode';
import React, { createContext, useCallback, useContext, useMemo } from 'react';
// Generate QR matrix using the `qrcode` dependency
const generateQRMatrix = (text: string, errorCorrectionLevel: 'L' | 'M' | 'Q' | 'H' = 'M'): boolean[][] => {
try {
const qr = QRCodeLib.create(text, { errorCorrectionLevel });
const size: number = qr.modules.size;
const data: Array<boolean | 0 | 1> = qr.modules.data as any;
const matrix: boolean[][] = new Array(size)
.fill(false)
.map((_, row) => new Array(size).fill(false).map((__, col) => Boolean(data[row * size + col])));
return matrix;
} catch (e) {
// Fallback to empty matrix on error
return [];
}
};
interface QRCodeContextType {
value: string;
matrix: boolean[][];
size: number;
errorCorrectionLevel: 'L' | 'M' | 'Q' | 'H';
}
const QRCodeContext = createContext<QRCodeContextType | null>(null);
const useQRCode = () => {
const context = useContext(QRCodeContext);
if (!context) {
throw new Error('QR Code components must be used within QRCodeRoot');
}
return context;
};
export interface QRCodeRootProps {
/**
* For composition, for more information see [Radix Slot](https://www.radix-ui.com/primitives/docs/utilities/slot)
*
* @default false
*/
asChild?: boolean;
/**
* The value of the QR code.
*/
value?: string;
defaultValue?: string;
/**
* The error correction level of the QR code.
* @default 'M'
*/
errorCorrectionLevel?: 'L' | 'M' | 'Q' | 'H';
/**
* Size of each pixel of the QR code.
* @default 4
*/
pixelSize?: number;
}
const QRCodeRoot: React.FC<QRCodeRootProps & React.HTMLAttributes<HTMLDivElement>> = ({
asChild = false,
value,
defaultValue = '',
errorCorrectionLevel = 'M',
pixelSize = 4,
className,
children,
...props
}) => {
const Comp = asChild ? Slot : 'div';
const qrValue = value ?? defaultValue;
const matrix = useMemo(() => {
if (!qrValue) return [];
return generateQRMatrix(qrValue, errorCorrectionLevel);
}, [qrValue, errorCorrectionLevel]);
const contextValue = useMemo(
() => ({
value: qrValue,
matrix,
size: matrix.length,
errorCorrectionLevel,
}),
[qrValue, matrix, errorCorrectionLevel]
);
return (
<QRCodeContext.Provider value={contextValue}>
<Comp className={cn('flex w-48 flex-col gap-4', className)} {...props}>
{children}
</Comp>
</QRCodeContext.Provider>
);
};
QRCodeRoot.displayName = 'QRCodeRoot';
export interface QRCodeFrameProps {
/**
* @default false
*/
asChild?: boolean;
}
const QRCodeFrame = React.forwardRef<HTMLDivElement, QRCodeFrameProps & React.HTMLAttributes<HTMLDivElement>>(
({ asChild = false, className, children, ...props }, ref) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp
ref={ref}
className={cn(
'relative inline-block aspect-square size-full border-8 border-white bg-white shadow-sm',
className
)}
{...props}
>
{children}
</Comp>
);
}
);
QRCodeFrame.displayName = 'QRCodeFrame';
const QRCodePattern = React.forwardRef<SVGSVGElement, Omit<React.SVGAttributes<SVGSVGElement>, 'children'>>(
({ className, ...props }, ref) => {
const { matrix, size } = useQRCode();
if (!matrix.length) return null;
return (
<svg
ref={ref}
className={cn('block aspect-square size-full', className)}
width={size}
height={size}
viewBox={`0 0 ${size} ${size}`}
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
{matrix.map((row, y) =>
row.map((filled, x) =>
filled ? (
<rect
key={`${y}-${x}`}
x={x}
y={y}
width={1}
height={1}
fill="currentColor"
className="text-black"
/>
) : null
)
)}
</svg>
);
}
);
QRCodePattern.displayName = 'QRCodePattern';
const qrCodeOverlayVariants = cva(
'absolute top-1/2 left-1/2 flex -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-full border-2 border-white shadow-lg',
{
variants: {
size: {
xs: 'size-5 [&>img]:size-4 [&>svg]:size-4',
sm: 'size-7 [&>img]:size-5 [&>svg]:size-5',
md: 'size-9 [&>img]:size-7 [&>svg]:size-7',
lg: 'size-12 [&>img]:size-10 [&>svg]:size-10',
xl: 'size-14 [&>img]:size-12 [&>svg]:size-12',
},
},
defaultVariants: {
size: 'md',
},
}
);
export interface QRCodeOverlayProps {
/**
* @default false
*/
asChild?: boolean;
/**
* The size of the QR code overlay.
* @default 'md'
*/
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
}
const QRCodeOverlay = React.forwardRef<
HTMLDivElement,
QRCodeOverlayProps & React.HTMLAttributes<HTMLDivElement>
>(({ asChild = false, size = 'md', className, children, ...props }, ref) => {
const Comp = asChild ? Slot : 'div';
return (
<Comp ref={ref} className={cn(qrCodeOverlayVariants({ size }), className)} {...props}>
{children}
</Comp>
);
});
QRCodeOverlay.displayName = 'QRCodeOverlay';
export interface QRCodeDownloadTriggerProps {
/**
* For composition, for more information see [Radix Slot](https://www.radix-ui.com/primitives/docs/utilities/slot)
*
* @default false
*/
asChild?: boolean;
/**
* The file name of the QR code.
* @default 'qr-code.png'
*/
fileName?: string;
/**
* The mime type of the QR code.
* @default 'image/png'
*/
mimeType?: 'image/png' | 'image/jpeg' | 'image/webp';
/**
* The quality of the QR code.
* @default 1
*/
quality?: number;
}
const QRCodeDownloadTrigger = React.forwardRef<
HTMLButtonElement,
QRCodeDownloadTriggerProps & Omit<React.ComponentPropsWithoutRef<typeof Button>, 'aria-label'>
>(
(
{
asChild = false,
fileName = 'qr-code.png',
mimeType = 'image/png',
quality = 1,
onClick,
className,
children,
...props
},
ref
) => {
const { matrix, size } = useQRCode();
const Comp = asChild ? Slot : Button;
const handleDownload = useCallback(
async (event: React.MouseEvent<HTMLButtonElement>) => {
event.preventDefault();
if (!matrix.length) return;
// Create a canvas to generate the image
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
if (!ctx) return;
const pixelSize = 8; // Higher resolution for download
canvas.width = size * pixelSize;
canvas.height = size * pixelSize;
// Fill background
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Draw QR pattern
ctx.fillStyle = 'black';
matrix.forEach((row, y) => {
row.forEach((filled, x) => {
if (filled) {
ctx.fillRect(x * pixelSize, y * pixelSize, pixelSize, pixelSize);
}
});
});
// Convert to blob and download
canvas.toBlob(
(blob) => {
if (!blob) return;
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
},
mimeType,
quality
);
onClick?.(event);
},
[matrix, size, fileName, mimeType, quality, onClick]
);
return (
<Comp
ref={ref}
onClick={handleDownload}
aria-label="Download QR Code"
className={cn('w-full', className)}
{...props}
>
{children ?? (
<>
<DownloadIcon className="size-4" />
Download
</>
)}
</Comp>
);
}
);
QRCodeDownloadTrigger.displayName = 'QRCodeDownloadTrigger';
export { QRCodeDownloadTrigger, QRCodeFrame, QRCodeOverlay, QRCodePattern, QRCodeRoot as QRCode };
Update the import paths to match your project setup.
Anatomy

QR Code component anatomy
Usage
import { QRCodeRoot, QRCodeFrame, QRCodePattern, QRCodeDownloadTrigger } from '@kit/ui/qr-code';
<QRCode value={'https://example.com'} pixelSize={4} errorCorrectionLevel="M">
<QRCodeFrame>
<QRCodePattern />
</QRCodeFrame>
<QRCodeDownloadTrigger />
</QRCode>
Features
- Radix component architecture
- Controlled and uncontrolled value support
- Error correction level configuration (L, M, Q, H)
- Download functionality with multiple image formats (PNG, JPEG, WebP)
- TypeScript support with proper prop types
Examples
With an overlay
<QRCode value={routes.url} pixelSize={4} errorCorrectionLevel="M">
<QRCodeFrame>
<QRCodePattern />
<QRCodeOverlay className="bg-primary border-4 inset-shadow-2xs">
<Image src="path/to/your/logo.svg" width={16} height={16} alt="Your Logo" />
</QRCodeOverlay>
</QRCodeFrame>
<QRCodeDownloadTrigger />
</QRCode>
Troubleshooting
It may be possible that your url is too long, or the overlay is too big. In that case try to increase the error correction level.
As mentioned in the QR Code library documentation, a higher levels offer a better error resistance but reduce the symbol's capacity.
Level | Error resistance |
---|---|
L | ~7% |
M | ~15% |
Q | ~25% |
H | ~30% |
API Reference
Root
Contains the context provider and a div element.
We advise you to set the width of the QR code in this component using the className
prop.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
value | string | |
defaultValue | string | |
errorCorrectionLevel | "L" | "M" | "Q" | "H" | 'M' |
pixelSize | number | 4 |
Frame
Contains the QR code frame.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
Pattern
Contains the QR code SVG element.
Props: Omit<React.SVGAttributes<SVGSVGElement>, 'children'>
Overlay
Add an overlay to make your QR code more attractive.
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
size | "xs" | "sm" | "md" | "lg" |... | 'md' |
Download Trigger
Prop | Type | Default |
---|---|---|
asChild | boolean | false |
fileName | string | 'qr-code.png' |
mimeType | "image/png" | "image/jpeg" |... | 'image/png' |
quality | number | 1 |
A progression component to display scroll progress through content.
Avoid form redundancy by using a quick form component.
How is this guide?
Last updated on 10/17/2025