Components
PreviousNext

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.

pnpm add qrcode

Make sure that the following shadcn/ui components are present in your project:

Copy and paste the following code into your project.

components/ui/qr-code.tsx
'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

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.

LevelError resistance
L~7%
M~15%
Q~25%
H~30%

API Reference

Root

Contains the context provider and a div element.

PropTypeDefault
asChild
boolean
false
value
string
defaultValue
string
errorCorrectionLevel
"L" | "M" | "Q" | "H"
'M'
pixelSize
number
4

Frame

Contains the QR code frame.

PropTypeDefault
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.

PropTypeDefault
asChild
boolean
false
size
"xs" | "sm" | "md" | "lg" |...
'md'

Download Trigger

PropTypeDefault
asChild
boolean
false
fileName
string
'qr-code.png'
mimeType
"image/png" | "image/jpeg" |...
'image/png'
quality
number
1

How is this guide?

Last updated on 10/17/2025