Leave a star on GitHub! ⭐


Docs
Signature

Signature

Component through which we can have digital signatures

Just sign here

Installation

pnpm add @uiw/react-signature lucide-react

Copy and paste the following code into your project.

components/eldoraui/signature.tsx

"use client";
import Signature, { type SignatureRef } from "@uiw/react-signature";
import {
  CheckIcon,
  CopyIcon,
  DownloadIcon,
  Eraser,
  RefreshCcwIcon,
} from "lucide-react";
import { type ComponentProps, useRef, useState } from "react";
import { useCopyToClipboard } from "@/components/hooks/copytoclipboard";
import { cn } from "@/lib/utils";
 
export function ReactSignature({
  className,
  ...props
}: ComponentProps<typeof Signature>) {
  const [readonly, setReadonly] = useState(false);
  const $svg = useRef<SignatureRef>(null);
 
  const handleClear = () => $svg.current?.clear();
 
  const handleValidate = () => {
    if (readonly) {
      $svg.current?.clear();
      setReadonly(false);
    } else {
      setReadonly(true);
    }
  };
 
  return (
    <div className="flex flex-col gap-2">
      <p className="text-sm tracking-tight text-neutral-500">Just sign here</p>
      <Signature
        className={cn(
          "h-28 w-80 rounded-lg border border-neutral-500/20 bg-neutral-500/10",
          readonly
            ? "cursor-not-allowed fill-neutral-500"
            : "fill-neutral-800 dark:fill-neutral-200",
          className,
        )}
        options={{
          smoothing: 0,
          streamline: 0.8,
          thinning: 0.7,
        }}
        readonly={readonly}
        {...props}
        ref={$svg}
      />
      <div className="flex justify-end gap-1 text-neutral-700 dark:text-neutral-200">
        <ValidateButton onClick={handleValidate} readonly={readonly} />
        {readonly && (
          <>
            <DownloadButton svgElement={$svg.current?.svg} />
            <CopySvgButton svgElement={$svg.current?.svg} />
          </>
        )}
        {!readonly && <ClearButton onClick={handleClear} />}
      </div>
    </div>
  );
}
 
function prepareSvgElement(svgElement: SVGSVGElement) {
  const svgelm = svgElement.cloneNode(true) as SVGSVGElement;
  const clientWidth = svgElement.clientWidth;
  const clientHeight = svgElement.clientHeight;
  svgelm.removeAttribute("style");
  svgelm.setAttribute("width", `${clientWidth}px`);
  svgelm.setAttribute("height", `${clientHeight}px`);
  svgelm.setAttribute("viewBox", `0 0 ${clientWidth} ${clientHeight}`);
  return { svgelm, clientWidth, clientHeight };
}
 
function ValidateButton({
  readonly,
  onClick,
}: Readonly<{
  readonly: boolean;
  onClick: () => void;
}>) {
  return (
    <button
      className="inline-grid size-8 place-content-center rounded-md border border-neutral-500/10 bg-neutral-500/10 hover:bg-neutral-500/20"
      onClick={onClick}
      type="button"
    >
      {readonly ? (
        <>
          <RefreshCcwIcon className="size-5" />
          <span className="sr-only">Reset</span>
        </>
      ) : (
        <>
          <CheckIcon className="size-5" />
          <span className="sr-only">Validate</span>
        </>
      )}
    </button>
  );
}
 
function DownloadButton({
  svgElement,
}: Readonly<{
  svgElement: SVGSVGElement | undefined | null;
}>) {
  const handleDownloadImage = () => {
    if (!svgElement) {
      return;
    }
 
    const { svgelm, clientWidth, clientHeight } = prepareSvgElement(svgElement);
 
    const data = new XMLSerializer().serializeToString(svgelm);
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    const img = new Image();
    img.onload = () => {
      canvas.width = clientWidth ?? 0;
      canvas.height = clientHeight ?? 0;
      ctx?.drawImage(img, 0, 0);
      const a = document.createElement("a");
      a.download = "signature.png";
      a.href = canvas.toDataURL("image/png");
      a.click();
    };
    img.src = `data:image/svg+xml;base64,${window.btoa(
      decodeURIComponent(encodeURIComponent(data)),
    )}`;
  };
 
  return (
    <button
      className="inline-grid size-8 place-content-center rounded-md border border-neutral-500/10 bg-neutral-500/10 hover:bg-neutral-500/20"
      onClick={handleDownloadImage}
      type="button"
    >
      <DownloadIcon className="size-5" />
      <span className="sr-only">Download</span>
    </button>
  );
}
 
function CopySvgButton({
  svgElement,
}: Readonly<{
  svgElement: SVGSVGElement | undefined | null;
}>) {
  const [_, copyText, isCopied] = useCopyToClipboard();
 
  const handleCopySvg = () => {
    if (!svgElement) {
      return;
    }
 
    const { svgelm } = prepareSvgElement(svgElement);
    copyText(svgelm.outerHTML);
  };
 
  return (
    <button
      className="inline-flex items-center gap-1 rounded-md border border-neutral-500/10 bg-neutral-500/10 px-1 text-sm tracking-tight hover:bg-neutral-500/20"
      onClick={handleCopySvg}
      type="button"
    >
      {isCopied ? (
        <>
          <span>Copied</span>
          <CheckIcon className="size-5" />
        </>
      ) : (
        <>
          <span>Copy to SVG</span>
          <CopyIcon className="size-5" />
        </>
      )}
    </button>
  );
}
 
function ClearButton({ onClick }: Readonly<{ onClick: () => void }>) {
  return (
    <button
      className="inline-grid size-8 place-content-center rounded-md border border-neutral-500/10 bg-neutral-500/10 hover:bg-neutral-500/20"
      onClick={onClick}
      type="button"
    >
      <Eraser className="size-5" />
      <span className="sr-only">Clear</span>
    </button>
  );
}

Copy and paste the following code into your project.

components/eldoraui/hooks/copytoclipboard.tsx

"use client";
import { useCallback, useEffect, useState } from "react";
 
type CopiedValue = string | null;
 
type CopyFn = (text: string) => Promise<boolean>;
 
export function useCopyToClipboard({
  isCopiedDelay = 2000,
}: {
  isCopiedDelay?: number;
} = {}): [CopiedValue, CopyFn, boolean] {
  const [copiedText, setCopiedText] = useState<CopiedValue>(null);
  const [isCopied, setIsCopied] = useState(false);
 
  useEffect(() => {
    if (!isCopied) {
      return;
    }
    setTimeout(() => {
      setIsCopied(false);
    }, isCopiedDelay);
  }, [isCopied, isCopiedDelay]);
 
  const copy: CopyFn = useCallback(async (text) => {
    if (!navigator?.clipboard) {
      return false;
    }
 
    // Try to save to clipboard then save it in the state if worked
    try {
      await navigator.clipboard.writeText(text);
      setCopiedText(text);
      setIsCopied(true);
      return true;
    } catch (_error) {
      setCopiedText(null);
      return false;
    }
  }, []);
 
  return [copiedText, copy, isCopied];
}

Update the import paths to match your project setup.

Props

ReactSignature Component Props

Prop NameTypeDefaultDescription
classNamestring-Optional class name for custom styling.
readonlybooleanfalseWhen true, the signature pad becomes read-only and disables signing.
optionsobject-Signature pad options for smoothing, thinning, and streamline. Pass an object with properties like smoothing, streamline, and thinning.
onChange(data: string) => void-Callback function when the signature data changes. It receives the signature data as a string.