Dice UI
Components

QR Code

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

API
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 qrcode

Install the type definitions:

npm install @types/qrcode

Copy 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 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

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

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
  • When using the Overlay component, set level="H" (High error correction) to ensure the QR code remains scannable with up to 30% coverage