Working with Signatures

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

Signature Modes

DocCentral 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 '@doccentral/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 '@doccentral/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
type*"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
type*"signature"-Value type identifier.
imageDatastring-Base64 encoded PNG image of the signature.
pathDatastring-SVG path data for vector representation.
typedTextstring-Typed 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 '@doccentral/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