Dice UI
Components

QR Code

A flexible QR code component for generating and displaying QR codes with customization options.

API
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 VariableDescriptionDefault
--qr-code-sizeThe 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

KeyDescription
EnterActivates the download button when focused.
SpaceActivates 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