paste-rich
v1.0.0
Published
Smart clipboard paste handler. Normalizes paste events into predictable types — image, file, HTML, or text. Zero dependencies.
Maintainers
Readme
📋 paste-rich
Smart clipboard paste handler. Normalizes paste events into predictable types — image, file, HTML, or text — in 3 lines. Zero dependencies.
Why
Handling paste events in the browser is deceptively complex. Screenshots paste as image/png blobs. Word documents include both HTML and plain text. File explorers inject DataTransferItem lists. Every app — Notion clones, WYSIWYG editors, comment boxes — ends up writing its own clipboardData parsing from scratch.
There's no standardized solution. Developers re-discover the same quirks every time: items vs files, getData('text/html') returning empty strings, images hidden inside DataTransferItem with kind === 'file'.
paste-rich normalizes all of it. ~0.8kB gzipped. Zero dependencies. One callback, one predictable shape: { type, data, files }.
Install
npm install paste-rich
# or
yarn add paste-rich
# or
pnpm add paste-richQuick Start
Listen for all paste types
import PasteRich from 'paste-rich';
const pr = new PasteRich({
target: '#editor',
onPaste: (result) => {
console.log(result.type); // 'image' | 'file' | 'html' | 'text'
console.log(result.data); // File or string
console.log(result.files); // File[] (empty for text/html)
},
});Filter to specific types
import PasteRich from 'paste-rich';
const pr = new PasteRich({
target: '#drop-zone',
types: ['image'],
onPaste: ({ data }) => {
const url = URL.createObjectURL(data as File);
document.getElementById('preview')!.src = url;
},
});Features
- Zero dependencies — pure TypeScript, no external packages
- Normalized output — every paste becomes
{ type, data, files }, no guesswork - Smart detection — images, files, rich HTML, and plain text, prioritized in that order
- Type filtering — pass
types: ['image', 'text']to ignore everything else - Multiple files — all pasted files available via
result.files - Flexible targeting — attach to any element via selector or reference, or the whole document
preventDefaultcontrol — on by default, opt out withpreventDefault: false- SSR safe — guards all DOM access behind
typeof window - ~0.8kB minified + gzipped
API
new PasteRich(options)
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| target | HTMLElement \| string | document | Element or CSS selector to listen on |
| onPaste | (result: PasteResult) => void | — | Required. Called with the normalized paste result |
| types | PasteType[] | all | Filter to specific types; unmatched pastes are ignored |
| preventDefault | boolean | true | Whether to call preventDefault() on handled paste events |
PasteResult
| Property | Type | Description |
|----------|------|-------------|
| type | 'image' \| 'file' \| 'html' \| 'text' | Detected paste type (highest priority match) |
| data | string \| File | Primary data — File for image/file, string for html/text |
| files | File[] | All files from the paste (empty array for text/html-only) |
Instance methods
| Method | Returns | Description |
|--------|---------|-------------|
| .destroy() | void | Remove the paste listener and clean up |
| [Symbol.dispose]() | void | Alias for destroy() — enables using syntax |
Detection priority
| Priority | Type | Condition |
|----------|------|-----------|
| 1 | image | clipboardData.items with type.startsWith('image/') |
| 2 | file | clipboardData.files with non-image MIME types |
| 3 | html | clipboardData.getData('text/html') is non-empty |
| 4 | text | clipboardData.getData('text/plain') fallback |
Examples
React hook
import { useEffect, useRef } from 'react';
import PasteRich, { PasteResult } from 'paste-rich';
function usePasteRich(
ref: React.RefObject<HTMLElement>,
onPaste: (result: PasteResult) => void
) {
const prRef = useRef<PasteRich | null>(null);
useEffect(() => {
if (!ref.current) return;
prRef.current = new PasteRich({
target: ref.current,
onPaste,
});
return () => prRef.current?.destroy();
}, [ref, onPaste]);
}Vue composable
import { onMounted, onUnmounted, ref } from 'vue';
import PasteRich, { PasteResult } from 'paste-rich';
export function usePasteRich(selector: string) {
let pr: PasteRich;
const lastPaste = ref<PasteResult | null>(null);
onMounted(() => {
pr = new PasteRich({
target: selector,
onPaste: (result) => { lastPaste.value = result; },
});
});
onUnmounted(() => pr?.destroy());
return { lastPaste };
}Image upload on paste
import PasteRich from 'paste-rich';
const pr = new PasteRich({
types: ['image'],
onPaste: async ({ data }) => {
const form = new FormData();
form.append('image', data as File);
await fetch('/api/upload', { method: 'POST', body: form });
},
});CDN (no build step)
<script type="module">
import PasteRich from 'https://esm.sh/paste-rich';
new PasteRich({
target: '#editor',
onPaste: ({ type, data }) => {
console.log('Pasted:', type, data);
},
});
</script>