Clipboard API in Practice: Browser Clipboard Read/Write and Security Policies
技术架构(Updated Jun 3, 2026)
Clipboard API: The Modern Clipboard Interaction Standard
Browser clipboard operations have evolved from document.execCommand to the Async Clipboard API:
| Aspect | execCommand('copy') | Async Clipboard API |
|---|---|---|
| Standard | Deprecated | W3C recommended |
| Async | No (sync blocking) | Yes (Promise-based) |
| Permissions | Implicit (user gesture) | Explicit Permissions API |
| Rich text | Not supported | Supported (ClipboardItem) |
| Custom formats | Not supported | Supported (web custom formats) |
| Image read/write | Not supported | Supported |
| Reliability | Depends on selection | Direct clipboard access |
Core API Deep Dive
Clipboard API Interfaces
interface Clipboard extends EventTarget {
read(): Promise<ClipboardItem[]>;
readText(): Promise<string>;
write(data: ClipboardItem[]): Promise<void>;
writeText(text: string): Promise<void>;
}
interface ClipboardItem {
readonly types: string[];
getType(type: string): Promise<Blob>;
}
Text Read/Write
async function copyText(text: string) {
try {
await navigator.clipboard.writeText(text);
showToast('Copied to clipboard');
} catch (err) {
console.error('Copy failed:', err);
fallbackCopyText(text);
}
}
async function pasteText(): Promise<string> {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (err) {
console.error('Read failed:', err);
return '';
}
}
Rich Text and Multi-Format Writing
ClipboardItem Multi-Format
async function copyRichText(html: string, plainText: string) {
const clipboardItem = new ClipboardItem({
'text/html': new Blob([html], { type: 'text/html' }),
'text/plain': new Blob([plainText], { type: 'text/plain' }),
});
await navigator.clipboard.write([clipboardItem]);
}
copyRichText(
'<strong>Important</strong>: Submit by <em>Friday</em>',
'Important: Submit by Friday'
);
Image Copy
async function copyImage(pngBlob: Blob) {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': pngBlob })
]);
}
async function copyCanvas(canvas: HTMLCanvasElement) {
const blob = await new Promise<Blob>((resolve) => {
canvas.toBlob((b) => resolve(b!), 'image/png');
});
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
}
Image Paste
document.addEventListener('paste', async (e) => {
const items = e.clipboardData?.items;
if (!items) return;
for (const item of items) {
if (item.type.startsWith('image/')) {
const blob = item.getAsFile()!;
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
document.body.appendChild(img);
}
}
});
Custom MIME Types (Web Custom Formats)
async function copyCustomData(data: object) {
const json = JSON.stringify(data);
await navigator.clipboard.write([
new ClipboardItem({
'web application/json': new Blob([json], { type: 'application/json' }),
'text/plain': new Blob([json], { type: 'text/plain' }),
})
]);
}
async function pasteCustomData(): Promise<object | null> {
const items = await navigator.clipboard.read();
for (const item of items) {
if (item.types.includes('web application/json')) {
const blob = await item.getType('web application/json');
const text = await blob.text();
return JSON.parse(text);
}
}
return null;
}
The
webprefix is a security requirement for custom formats, preventing pages from reading native clipboard formats.
Permissions and Security Policies
Permissions API
async function checkClipboardPermission(): Promise<boolean> {
try {
const result = await navigator.permissions.query({
name: 'clipboard-read' as PermissionName
});
return result.state === 'granted';
} catch {
return false;
}
}
Security Restrictions
| Operation | Requirement | Notes |
|---|---|---|
writeText |
User activation | Needs click/keydown gesture |
write |
User activation | Same as above |
readText |
User activation + permission | User authorization prompt |
read |
User activation + permission | User authorization prompt |
| Focused window | Document must be focused | Unfocused window calls rejected |
User Activation Check
button.addEventListener('click', async () => {
// ✅ Inside click event — user activation present
await navigator.clipboard.writeText('copied!');
});
setTimeout(async () => {
await navigator.clipboard.writeText('fails'); // ❌ DOMException
}, 1000);
Fallback: execCommand
function fallbackCopyText(text: string): boolean {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try {
const success = document.execCommand('copy');
document.body.removeChild(textarea);
return success;
} catch {
document.body.removeChild(textarea);
return false;
}
}
async function safeCopy(text: string) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {}
}
return fallbackCopyText(text);
}
Deep Paste Event Handling
Intercept and Transform Paste Content
editor.addEventListener('paste', (e) => {
e.preventDefault();
const clipboardData = e.clipboardData!;
const html = clipboardData.getData('text/html');
if (html) {
const sanitized = sanitizeHTML(html);
insertHTML(sanitized);
return;
}
const text = clipboardData.getData('text/plain');
if (text) {
const linked = text.replace(
/https?:\/\/\S+/g,
(url) => `<a href="${url}">${url}</a>`
);
insertHTML(linked);
}
});
Clipboard Data Type Detection
editor.addEventListener('paste', (e) => {
const data = e.clipboardData!;
console.log('Types:', [...data.types]);
const priorityOrder = [
'text/html',
'application/json',
'text/plain',
'image/png',
];
for (const type of priorityOrder) {
if (data.types.includes(type)) {
const content = data.getData(type);
console.log(`Paste ${type}:`, content.slice(0, 100));
break;
}
}
});
Practice: Copy Button Component
class CopyButton {
private timeoutId: number | null = null;
constructor(private button: HTMLButtonElement, private getText: () => string) {
button.addEventListener('click', () => this.handleCopy());
}
private async handleCopy() {
const text = this.getText();
const success = await safeCopy(text);
if (success) {
this.showFeedback('Copied!');
} else {
this.showFeedback('Failed');
}
}
private showFeedback(message: string) {
const original = this.button.textContent;
this.button.textContent = message;
this.button.disabled = true;
if (this.timeoutId) clearTimeout(this.timeoutId);
this.timeoutId = window.setTimeout(() => {
this.button.textContent = original;
this.button.disabled = false;
}, 2000);
}
}
Browser Compatibility
| API | Chrome | Firefox | Safari | Edge |
|---|---|---|---|---|
writeText |
66+ | 63+ | 13.1+ | 79+ |
readText |
66+ | 63+ | 13.1+ | 79+ |
write (ClipboardItem) |
76+ | 87+ | 13.1+ | 79+ |
read |
76+ | 87+ | 13.1+ | 79+ |
| Web Custom Formats | 104+ | No | No | 104+ |
| Image clipboard | 76+ | 87+ | 13.1+ | 79+ |
Best Practices
- Always provide fallback: Use
execCommandwhen Clipboard API is unavailable - Call within user activation: Copy must be triggered in click/keydown handlers
- Always show feedback: Visual feedback for both success and failure
- Read with caution:
read()involves privacy—request only when necessary - Sanitize paste content: HTML paste must be filtered for XSS risks
Summary
The Async Clipboard API is the standard approach for modern browser clipboard operations, supporting text, rich text, image, and custom format read/write. More reliable, secure, and capable than execCommand, but requires attention to user activation and permission models.
Use Clipboard Viewer to debug clipboard content, Text Replace for batch paste processing, and Case Convert to normalize clipboard text.
Try these browser-local tools — no sign-up required →
#Clipboard API#剪贴板#异步API#浏览器安全#用户交互