npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ssi-lib/pad-signature

v1.0.2

Published

Pad de firma manuscrita para React: canvas táctil, theme por CSS vars y resultado en data URL / base64.

Readme

@ssi-lib/pad-signature

Pad de firma manuscrita para React (ESM + CJS + tipos). Canvas táctil con trazo Bézier (signature_pad), alto DPI, theme por CSS custom properties y resultado en data URL / base64.

El CSS se inyecta al importar el paquete — no hace falta import './styles.css'.

Instalación

npm i @ssi-lib/pad-signature

Peer deps: react / react-dom (>=18).

1. Lo más simple

Un pad vacío. Sin label, sin chrome extra: el usuario firma y onChange entrega el resultado.

import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';

export function FirmaMinima() {
  const [firma, setFirma] = useState<SignatureResult | null>(null);

  return <PadSignature onChange={setFirma} />;
}

onChange se dispara al terminar un trazo y al limpiar. Si no hay firma, result.empty === true y dataUrl es ''.

2. Label, ayuda y required

<PadSignature
  label="Firma"
  placeholder="Firme dentro del recuadro"
  required
  onChange={setFirma}
/>

Sin copy default: si no pasás label / placeholder, no se muestra texto. required solo pinta el * — la validación la hacés vos (ver ejemplo 5).

3. Mostrar la firma

import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';

export function FirmaConPreview() {
  const [firma, setFirma] = useState<SignatureResult | null>(null);

  return (
    <>
      <PadSignature
        label="Firma"
        placeholder="Firme dentro del recuadro"
        onChange={setFirma}
      />
      {firma && !firma.empty && <img src={firma.dataUrl} alt="Firma" />}
    </>
  );
}

4. Controlado

Pasá value (data URL) para que el padre mande. Útil para hidratar, resetear o sincronizar con un store.

import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';

export function FirmaControlada() {
  const [firma, setFirma] = useState<SignatureResult | null>(null);

  return (
    <>
      <PadSignature value={firma?.dataUrl ?? ''} onChange={setFirma} />
      <button type="button" onClick={() => setFirma(null)}>
        Reset
      </button>
    </>
  );
}

Sin value, el pad es no controlado. Para una firma inicial sin controlar después:

<PadSignature defaultValue={dataUrlGuardado} onChange={setFirma} />

5. Validación al continuar

El pad no bloquea solo: required es visual. El host decide cuándo mostrar el error.

import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';

export function FirmaConValidacion() {
  const [firma, setFirma] = useState<SignatureResult | null>(null);
  const [intentado, setIntentado] = useState(false);
  const vacia = !firma || firma.empty;

  return (
    <>
      <PadSignature
        label="Firma"
        placeholder="Firme dentro del recuadro"
        required
        error="La firma es obligatoria"
        showError={intentado && vacia}
        onChange={setFirma}
      />
      <button type="button" onClick={() => setIntentado(true)} disabled={vacia && intentado}>
        Continuar
      </button>
    </>
  );
}

showError pinta el mensaje y el borde --ssi-pad-error-color.

6. Disabled y i18n del botón limpiar

<PadSignature
  label="Signature"
  placeholder="Sign inside the box"
  disabled={readonly}
  clearLabel="Clear"
  onChange={setFirma}
/>

clearLabel no tiene i18n interno: pasalo ya traducido. El botón limpiar solo aparece cuando hay firma (showClear default true).

<PadSignature showClear={false} onChange={setFirma} />

7. JPEG, grosor y alto

<PadSignature
  label="Firma"
  height={220}
  mimeType="image/jpeg"
  quality={0.82}
  minWidth={2}
  maxWidth={4}
  onChange={setFirma}
/>

JPEG necesita fondo opaco (theme.background o el default #ffffff). PNG es el default. SVG: mimeType="image/svg+xml".

8. Form nativo (name)

El pad escribe un <input type="hidden"> con el data URL, para un <form> clásico.

export function FirmaEnForm() {
  return (
    <form method="post" action="/contratos">
      <PadSignature name="signature" label="Firma" />
      <button type="submit">Enviar</button>
    </form>
  );
}

9. Theme (objeto)

<PadSignature
  label="Firma"
  height={220}
  theme={{
    background: '#fbf9fd',
    pen: '#5c349e',
    border: '#e7e1ef',
    radius: '16px',
    labelColor: '#2d2d2c',
    hintColor: '#726c7c',
    clearColor: '#5c349e',
    errorColor: '#c0392b',
  }}
  onChange={setFirma}
/>

theme.background y theme.pen también van al motor del canvas (no son solo CSS). El resto es chrome.

10. Theme (CSS del host)

<PadSignature className="mi-firma" onChange={setFirma} />
.mi-firma {
  --ssi-pad-bg: #111;
  --ssi-pad-pen: #f2f2f2;
  --ssi-pad-border: #333;
  --ssi-pad-height: 200px;
  --ssi-pad-radius: 8px;
}

11. Design system propio (classNames)

<PadSignature
  label="Firma"
  classNames={{
    root: 'Field',
    label: 'Field-label',
    hint: 'Field-hint',
    surface: 'Field-control',
    canvas: 'Field-canvas',
    clear: 'Field-action',
    error: 'Field-error',
  }}
  onChange={setFirma}
/>

Las clases de la lib (ssi-pad-*) siguen ahí: las tuyas se suman. Estados en el root: .is-disabled, .is-invalid, .is-empty, .has-value.

12. Ref — leer / limpiar / hidratar

import { useRef } from 'react';
import { PadSignature, type PadSignatureHandle } from '@ssi-lib/pad-signature';

export function FirmaConRef() {
  const ref = useRef<PadSignatureHandle>(null);

  const onSubmit = () => {
    if (ref.current?.isEmpty()) return;
    const result = ref.current?.toResult();
    console.log(result?.dataUrl);
  };

  return (
    <>
      <PadSignature ref={ref} label="Firma" />
      <button type="button" onClick={() => ref.current?.clear()}>
        Limpiar
      </button>
      <button type="button" onClick={onSubmit}>
        Guardar
      </button>
    </>
  );
}

También: toDataURL('image/jpeg', 0.8) y await ref.current.fromDataURL(dataUrl).

13. Migrar desde omega-pad-signature

Omega guardaba el split item.type + item.value. Acá es el mismo split en SignatureResult.

import { joinSignature, PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';

export function FirmaDesdeOmega({ item }: { item: { type: string; value: string } }) {
  const inicial = joinSignature(item.type, item.value);

  const onChange = (result: SignatureResult) => {
    item.type = result.type;
    item.value = result.base64;
    item.validForm = !result.empty;
  };

  return <PadSignature defaultValue={inicial} label={item.lbl} onChange={onChange} />;
}

14. Hook sin chrome (useSignaturePad)

Si el layout lo armás vos:

import { useSignaturePad } from '@ssi-lib/pad-signature';

export function MiPad() {
  const pad = useSignaturePad({
    penColor: '#111',
    backgroundColor: '#fff',
    onChange: console.log,
  });

  return (
    <div>
      <canvas ref={pad.canvasRef} style={{ width: '100%', height: 180, touchAction: 'none' }} />
      <button type="button" onClick={pad.clear} disabled={pad.empty}>
        Limpiar
      </button>
    </div>
  );
}

El hook se ocupa del DPR, ResizeObserver, disabled y del value controlado. El <canvas> tiene que tener tamaño CSS (width/height) para que el backing store se calcule bien.

15. Completo — onboarding de contrato

Controlado + validación + theme + preview + JPEG.

import { useState } from 'react';
import { PadSignature, type SignatureResult } from '@ssi-lib/pad-signature';

export function FirmaContrato() {
  const [firma, setFirma] = useState<SignatureResult | null>(null);
  const [intentado, setIntentado] = useState(false);
  const vacia = !firma || firma.empty;

  const onContinuar = () => {
    setIntentado(true);
    if (vacia) return;
    enviar(firma.dataUrl);
  };

  return (
    <section>
      <PadSignature
        label="Firma del titular"
        placeholder="Firme dentro del recuadro"
        required
        value={firma?.dataUrl ?? ''}
        onChange={setFirma}
        error="La firma es obligatoria"
        showError={intentado && vacia}
        height={200}
        mimeType="image/jpeg"
        quality={0.82}
        theme={{
          pen: '#111111',
          background: '#ffffff',
          border: '#e7e1ef',
          radius: '12px',
          clearColor: '#5c349e',
        }}
      />

      {firma && !firma.empty && (
        <p>
          {firma.mimeType} · {firma.base64.length} chars
        </p>
      )}

      <button type="button" onClick={onContinuar}>
        Continuar
      </button>
    </section>
  );
}

function enviar(_dataUrl: string) {}

Resultado (SignatureResult)

interface SignatureResult {
  dataUrl: string;  // data:image/png;base64,...
  type: string;     // data:image/png;base64  (omega item.type)
  base64: string;   // payload                (omega item.value)
  mimeType: string; // image/png
  empty: boolean;
}

Helpers: joinSignature(type, value), splitDataUrl(dataUrl), EMPTY_SIGNATURE.

Props de PadSignature

| Prop | Tipo | Default | Descripción | | --- | --- | --- | --- | | label | string | — | Título. Sin copy default. | | placeholder | string | — | Texto de ayuda. Sin copy default. | | value | string | — | Data URL. Si se pasa, el pad es controlado. | | defaultValue | string | — | Data URL inicial (no controlado). | | onChange | (result) => void | — | Al trazar y al limpiar. | | onBegin / onEnd | () => void / (result) => void | — | Inicio / fin de un trazo. | | disabled | boolean | false | Bloquea el canvas y el botón limpiar. | | required | boolean | false | Muestra * junto al label. | | error / showError | string / boolean | — | Mensaje y visibilidad. Pinta borde de error. | | showClear | boolean | true | Botón limpiar cuando hay firma. | | clearLabel | string | 'Limpiar' | Texto del botón. Pasalo ya traducido. | | height | number \| string | 180 | Alto del recuadro. Gana sobre theme.height. | | mimeType | 'image/png' \| 'image/jpeg' \| 'image/svg+xml' | 'image/png' | Formato de toDataURL. JPEG pide fondo opaco. | | quality | number | — | Calidad JPEG 0–1. | | minWidth / maxWidth | number | 1.5 / 3.5 | Grosor del trazo. | | theme | PadSignatureTheme | — | CSS vars + color de pluma/fondo. | | className / classNames | string / slots | — | Root y piezas (root, label, hint, surface, canvas, clear, error, …). | | name | string | — | <input type="hidden"> con el data URL, para forms nativos. |

Theme — variables CSS

Tres capas, de menor a mayor especificidad: CSS del host → theme → classNames / className.

| Var | Default | Qué pinta | | --- | --- | --- | | --ssi-pad-bg | #ffffff | Fondo del canvas | | --ssi-pad-pen | #111111 | Color del trazo (theme.pen también va al motor) | | --ssi-pad-border | #e7e1ef | Borde del recuadro | | --ssi-pad-border-width | 1px | Grosor del borde | | --ssi-pad-radius | 12px | Radio del recuadro | | --ssi-pad-height | 180px | Alto del recuadro | | --ssi-pad-gap | 8px | Espacio entre label, pad y error | | --ssi-pad-padding | 0px | Padding del root | | --ssi-pad-label-color / -size / -weight | inherit / 0.875rem / 600 | Label | | --ssi-pad-hint-color / -size | #726c7c / 0.8125rem | Placeholder | | --ssi-pad-error-color | #c0392b | Error y * de required | | --ssi-pad-clear-color / -size | #5c349e / 0.8125rem | Botón limpiar | | --ssi-pad-disabled-opacity | 0.55 | Estado disabled |

CSS (clases)

Prefijo ssi-pad-. Estados en el root: .is-disabled, .is-invalid, .is-empty, .has-value.

| Clase | Elemento | | --- | --- | | .ssi-pad | Root | | .ssi-pad-head | Label + botón limpiar | | .ssi-pad-label / .ssi-pad-hint | Título y ayuda | | .ssi-pad-clear | Botón limpiar | | .ssi-pad-surface | Recuadro (borde, radio, alto) | | .ssi-pad-canvas | <canvas> | | .ssi-pad-error | Mensaje de error |

Desarrollo

npm install
npm run build
npm run typecheck

Preview local: library-hub/ui-preview-library → ruta /pad-signature.