Working with Signatures

Learn how to implement digital signatures using the SignaturePad component and signature fields in the PDFViewer.

Signature Modes

The MIMS SDK supports three signature input modes:

  • Draw - Users draw their signature with mouse or touch
  • Type - Users type their name and select a signature font
  • Upload - Users upload an image of their signature

Using SignaturePad Component

The SignaturePad component provides a standalone signature capture interface:

Standalone SignaturePadtsx
import { SignaturePad } from '@mims/sdk-react';
import { useState } from 'react';

function SignatureCapture() {
  const [signature, setSignature] = useState<string | null>(null);

  const handleSave = (imageData: string) => {
    setSignature(imageData);
    console.log('Signature captured:', imageData.substring(0, 50) + '...');
  };

  const handleClear = () => {
    setSignature(null);
  };

  return (
    <div className="signature-capture">
      <SignaturePad
        width={400}
        height={200}
        penColor="#000000"
        backgroundColor="#ffffff"
        onSave={handleSave}
        onClear={handleClear}
      />
      
      {signature && (
        <div className="preview mt-4">
          <p>Preview:</p>
          <img src={signature} alt="Signature preview" className="border" />
        </div>
      )}
    </div>
  );
}

Signature Fields in PDF

Add signature fields to documents for users to sign specific areas:

Adding Signature Fieldstsx
import { useDocumentStore } from '@mims/sdk-react';
import { nanoid } from 'nanoid';

function AddSignatureField() {
  const currentPage = useDocumentStore((s) => s.currentPage);
  const addField = useDocumentStore((s) => s.addField);

  const handleAddSignature = () => {
    addField({
      id: nanoid(),
      type: 'signature',
      page: currentPage,
      // Position at bottom of page (coordinates are 0-1 relative)
      x: 0.1,
      y: 0.75,
      width: 0.35,
      height: 0.12,
      required: true,
      editable: true,
      label: 'Your Signature',
      signatureMode: 'draw',
    });
  };

  return (
    <button onClick={handleAddSignature}>
      Add Signature Field
    </button>
  );
}

Signature Field Definition

PropTypeDefaultDescription
typerequired"signature"Field type identifier.
signatureMode"draw" | "type" | "upload""draw"How the user can input their signature.
requiredbooleanfalseWhether the signature is required for submission.

Signature Field Value

PropTypeDefaultDescription
typerequired"signature"Value type identifier.
imageDatastringBase64 encoded PNG image of the signature.
pathDatastringSVG path data for vector representation.
typedTextstringTyped signature text (when using type mode).

Custom Signature Modal

Create a custom modal for signature capture with multiple input options:

SignatureModal.tsxtsx
'use client';

import { useState } from 'react';
import { SignaturePad, useDocumentStore } from '@mims/sdk-react';

interface SignatureModalProps {
  fieldId: string;
  isOpen: boolean;
  onClose: () => void;
}

type SignatureTab = 'draw' | 'type' | 'upload';

export function SignatureModal({ fieldId, isOpen, onClose }: SignatureModalProps) {
  const [activeTab, setActiveTab] = useState<SignatureTab>('draw');
  const [typedName, setTypedName] = useState('');
  const [selectedFont, setSelectedFont] = useState('Dancing Script');
  const setFieldValue = useDocumentStore((s) => s.setFieldValue);

  if (!isOpen) return null;

  const handleDrawSave = (imageData: string) => {
    setFieldValue(fieldId, {
      type: 'signature',
      imageData,
    });
    onClose();
  };

  const handleTypeSave = () => {
    if (!typedName.trim()) return;
    
    // Generate signature image from typed text
    const canvas = document.createElement('canvas');
    canvas.width = 400;
    canvas.height = 150;
    const ctx = canvas.getContext('2d')!;
    
    ctx.fillStyle = '#ffffff';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    ctx.font = `48px "${selectedFont}"`;
    ctx.fillStyle = '#000000';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(typedName, canvas.width / 2, canvas.height / 2);
    
    const imageData = canvas.toDataURL('image/png');
    
    setFieldValue(fieldId, {
      type: 'signature',
      imageData,
      typedText: typedName,
    });
    onClose();
  };

  const handleUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (event) => {
      const imageData = event.target?.result as string;
      setFieldValue(fieldId, {
        type: 'signature',
        imageData,
      });
      onClose();
    };
    reader.readAsDataURL(file);
  };

  const fonts = [
    'Dancing Script',
    'Great Vibes', 
    'Pacifico',
    'Caveat',
  ];

  return (
    <div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
      <div className="bg-white rounded-lg w-full max-w-lg p-6">
        <div className="flex justify-between items-center mb-4">
          <h2 className="text-xl font-semibold">Add Your Signature</h2>
          <button onClick={onClose} className="text-neutral-500">×</button>
        </div>

        {/* Tabs */}
        <div className="flex gap-2 mb-4">
          {(['draw', 'type', 'upload'] as const).map((tab) => (
            <button
              key={tab}
              onClick={() => setActiveTab(tab)}
              className={`px-4 py-2 rounded ${
                activeTab === tab 
                  ? 'bg-emerald-500 text-white' 
                  : 'bg-neutral-100'
              }`}
            >
              {tab.charAt(0).toUpperCase() + tab.slice(1)}
            </button>
          ))}
        </div>

        {/* Draw Tab */}
        {activeTab === 'draw' && (
          <SignaturePad
            width={400}
            height={200}
            onSave={handleDrawSave}
          />
        )}

        {/* Type Tab */}
        {activeTab === 'type' && (
          <div className="space-y-4">
            <input
              type="text"
              value={typedName}
              onChange={(e) => setTypedName(e.target.value)}
              placeholder="Type your full name"
              className="w-full p-3 border rounded"
            />
            
            <div className="space-y-2">
              <label className="text-sm text-neutral-600">Select font:</label>
              <div className="grid grid-cols-2 gap-2">
                {fonts.map((font) => (
                  <button
                    key={font}
                    onClick={() => setSelectedFont(font)}
                    className={`p-3 border rounded text-2xl ${
                      selectedFont === font ? 'border-emerald-500 bg-emerald-50' : ''
                    }`}
                    style={{ fontFamily: font }}
                  >
                    {typedName || 'Preview'}
                  </button>
                ))}
              </div>
            </div>
            
            <button
              onClick={handleTypeSave}
              disabled={!typedName.trim()}
              className="w-full py-2 bg-emerald-500 text-white rounded disabled:opacity-50"
            >
              Apply Signature
            </button>
          </div>
        )}

        {/* Upload Tab */}
        {activeTab === 'upload' && (
          <div className="border-2 border-dashed rounded-lg p-8 text-center">
            <input
              type="file"
              accept="image/*"
              onChange={handleUpload}
              className="hidden"
              id="signature-upload"
            />
            <label htmlFor="signature-upload" className="cursor-pointer">
              <p className="text-neutral-600 mb-2">
                Click to upload or drag and drop
              </p>
              <p className="text-sm text-neutral-400">
                PNG, JPG up to 2MB
              </p>
            </label>
          </div>
        )}
      </div>
    </div>
  );
}

Signature Validation

Implement validation to ensure signatures meet requirements:

Signature Validationtsx
function validateSignature(value: SignatureFieldValue | null): string[] {
  const errors: string[] = [];
  
  if (!value) {
    errors.push('Signature is required');
    return errors;
  }

  // Must have either image data or typed text
  if (!value.imageData && !value.typedText) {
    errors.push('Please draw, type, or upload a signature');
  }

  // Check minimum image size (avoid blank signatures)
  if (value.imageData) {
    const img = new Image();
    img.src = value.imageData;
    
    // Simple check: base64 string should be substantial
    if (value.imageData.length < 1000) {
      errors.push('Signature appears to be blank');
    }
  }

  return errors;
}

// Use in submission
function validateBeforeSubmit() {
  const fields = useDocumentStore.getState().fields;
  
  fields.forEach((field, id) => {
    if (field.definition.type === 'signature' && field.definition.required) {
      const errors = validateSignature(field.value as SignatureFieldValue);
      if (errors.length > 0) {
        // Update field errors
        useDocumentStore.getState().updateFieldDefinition(id, { errors });
      }
    }
  });
}

Legal Considerations

Digital signatures may have legal requirements depending on your jurisdiction. Consult with legal counsel about compliance with e-signature laws like ESIGN, UETA, or eIDAS.

Signature Storage Format

When submitted, signatures are included in the payload as base64 images:

Submission Payload (signature field)json
{
  "id": "sig_abc123",
  "type": "signature",
  "page": 1,
  "position": {
    "x": 0.1,
    "y": 0.75,
    "width": 0.35,
    "height": 0.12
  },
  "value": {
    "type": "signature",
    "imageData": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUg...",
    "typedText": null
  }
}

Signature Appearance

Customize the appearance of signature fields:

Styled Signature Fieldtsx
function SignatureFieldOverlay({ field }: { field: FieldState }) {
  const value = field.value as SignatureFieldValue | null;
  const selectField = useDocumentStore((s) => s.selectField);

  const isEmpty = !value?.imageData && !value?.typedText;
  const isRequired = field.definition.required;

  return (
    <div
      onClick={() => selectField(field.definition.id)}
      className={`
        absolute border-2 rounded cursor-pointer transition-colors
        ${isEmpty 
          ? 'border-dashed border-neutral-300 bg-neutral-50/50' 
          : 'border-solid border-emerald-500 bg-emerald-50/30'
        }
        ${isRequired && isEmpty ? 'border-red-300 bg-red-50/30' : ''}
      `}
      style={{
        left: `${field.definition.x * 100}%`,
        top: `${field.definition.y * 100}%`,
        width: `${field.definition.width * 100}%`,
        height: `${field.definition.height * 100}%`,
      }}
    >
      {isEmpty ? (
        <div className="flex items-center justify-center h-full text-neutral-400">
          <span>Click to sign</span>
          {isRequired && <span className="text-red-500 ml-1">*</span>}
        </div>
      ) : (
        <img 
          src={value!.imageData} 
          alt="Signature" 
          className="w-full h-full object-contain"
        />
      )}
    </div>
  );
}

Related