JavaScript Clipboard API: Secure Copy and Paste Operations
Master the Clipboard API for secure programmatic access to clipboard operations. Learn to implement copy/paste functionality, handle different data types, and follow best practices.
The Clipboard API provides a secure way to read from and write to the system clipboard. It replaces the older document.execCommand() approach with a modern, promise-based API that handles permissions and supports various data types.
Understanding the Clipboard API
The Clipboard API offers asynchronous methods to interact with the clipboard, ensuring better security and user control over clipboard access.
Basic Clipboard Operations
// Modern Clipboard API
async function copyText() {
try {
await navigator.clipboard.writeText('Hello, Clipboard!');
console.log('Text copied to clipboard');
} catch (err) {
console.error('Failed to copy text: ', err);
}
}
// Read text from clipboard
async function pasteText() {
try {
const text = await navigator.clipboard.readText();
console.log('Pasted text: ', text);
return text;
} catch (err) {
console.error('Failed to read clipboard: ', err);
}
}
// Copy selected text
async function copySelection() {
const selection = window.getSelection().toString();
if (selection) {
try {
await navigator.clipboard.writeText(selection);
console.log('Selection copied');
} catch (err) {
console.error('Failed to copy selection: ', err);
}
}
}
// Copy element content
async function copyElementContent(elementId) {
const element = document.getElementById(elementId);
if (element) {
try {
await navigator.clipboard.writeText(element.textContent);
console.log('Element content copied');
} catch (err) {
console.error('Failed to copy element content: ', err);
}
}
}
Working with ClipboardItem
// Copy multiple types of data
async function copyRichContent(text, htmlContent) {
try {
const clipboardItem = new ClipboardItem({
'text/plain': new Blob([text], { type: 'text/plain' }),
'text/html': new Blob([htmlContent], { type: 'text/html' }),
});
await navigator.clipboard.write([clipboardItem]);
console.log('Rich content copied');
} catch (err) {
console.error('Failed to copy rich content: ', err);
}
}
// Read clipboard items
async function readClipboardItems() {
try {
const items = await navigator.clipboard.read();
for (const item of items) {
console.log('Available types:', item.types);
for (const type of item.types) {
const blob = await item.getType(type);
if (type === 'text/plain') {
const text = await blob.text();
console.log('Text content:', text);
} else if (type === 'text/html') {
const html = await blob.text();
console.log('HTML content:', html);
} else if (type.startsWith('image/')) {
const image = URL.createObjectURL(blob);
console.log('Image URL:', image);
}
}
}
} catch (err) {
console.error('Failed to read clipboard items: ', err);
}
}
// Copy image to clipboard
async function copyImage(imageUrl) {
try {
const response = await fetch(imageUrl);
const blob = await response.blob();
const clipboardItem = new ClipboardItem({
[blob.type]: blob,
});
await navigator.clipboard.write([clipboardItem]);
console.log('Image copied to clipboard');
} catch (err) {
console.error('Failed to copy image: ', err);
}
}
// Copy canvas content
async function copyCanvas(canvas) {
try {
const blob = await new Promise((resolve) =>
canvas.toBlob(resolve, 'image/png')
);
const clipboardItem = new ClipboardItem({
'image/png': blob,
});
await navigator.clipboard.write([clipboardItem]);
console.log('Canvas copied to clipboard');
} catch (err) {
console.error('Failed to copy canvas: ', err);
}
}
Practical Applications
Advanced Copy Button
class CopyButton {
constructor(buttonElement, targetElement) {
this.button = buttonElement;
this.target = targetElement;
this.originalText = this.button.textContent;
this.init();
}
init() {
this.button.addEventListener('click', () => this.copy());
}
async copy() {
const content = this.getContent();
try {
await navigator.clipboard.writeText(content);
this.showSuccess();
} catch (err) {
this.showError();
// Fallback to older method
this.fallbackCopy(content);
}
}
getContent() {
if (typeof this.target === 'string') {
return this.target;
} else if (
this.target instanceof HTMLInputElement ||
this.target instanceof HTMLTextAreaElement
) {
return this.target.value;
} else {
return this.target.textContent;
}
}
showSuccess() {
this.button.textContent = '✓ Copied!';
this.button.classList.add('copy-success');
setTimeout(() => {
this.button.textContent = this.originalText;
this.button.classList.remove('copy-success');
}, 2000);
}
showError() {
this.button.textContent = '✗ Failed';
this.button.classList.add('copy-error');
setTimeout(() => {
this.button.textContent = this.originalText;
this.button.classList.remove('copy-error');
}, 2000);
}
fallbackCopy(text) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
this.showSuccess();
} catch (err) {
console.error('Fallback copy failed:', err);
}
document.body.removeChild(textarea);
}
}
// Code snippet copy functionality
class CodeCopy {
constructor(container) {
this.container = container;
this.init();
}
init() {
const codeBlocks = this.container.querySelectorAll('pre code');
codeBlocks.forEach((block) => {
const wrapper = document.createElement('div');
wrapper.className = 'code-block-wrapper';
block.parentNode.insertBefore(wrapper, block);
wrapper.appendChild(block);
const copyButton = document.createElement('button');
copyButton.className = 'code-copy-button';
copyButton.textContent = 'Copy';
wrapper.appendChild(copyButton);
new CopyButton(copyButton, block);
});
}
}
// Table data copy
class TableCopy {
constructor(table) {
this.table = table;
this.init();
}
init() {
this.addCopyButtons();
this.setupKeyboardShortcuts();
}
addCopyButtons() {
// Add copy row buttons
const rows = this.table.querySelectorAll('tbody tr');
rows.forEach((row) => {
const copyCell = document.createElement('td');
const copyButton = document.createElement('button');
copyButton.textContent = '📋';
copyButton.title = 'Copy row';
copyButton.addEventListener('click', () => this.copyRow(row));
copyCell.appendChild(copyButton);
row.appendChild(copyCell);
});
// Add copy column buttons
const headers = this.table.querySelectorAll('thead th');
headers.forEach((header, index) => {
const copyButton = document.createElement('button');
copyButton.textContent = '📋';
copyButton.title = 'Copy column';
copyButton.addEventListener('click', () => this.copyColumn(index));
header.appendChild(copyButton);
});
}
async copyRow(row) {
const cells = Array.from(row.querySelectorAll('td'));
const text = cells.map((cell) => cell.textContent.trim()).join('\t');
try {
await navigator.clipboard.writeText(text);
this.showCopyFeedback(row);
} catch (err) {
console.error('Failed to copy row:', err);
}
}
async copyColumn(columnIndex) {
const cells = this.table.querySelectorAll(
`tbody tr td:nth-child(${columnIndex + 1})`
);
const values = Array.from(cells).map((cell) => cell.textContent.trim());
const text = values.join('\n');
try {
await navigator.clipboard.writeText(text);
this.showColumnFeedback(columnIndex);
} catch (err) {
console.error('Failed to copy column:', err);
}
}
async copyTable() {
const rows = Array.from(this.table.querySelectorAll('tr'));
const data = rows.map((row) => {
const cells = Array.from(row.querySelectorAll('td, th'));
return cells.map((cell) => cell.textContent.trim()).join('\t');
});
const text = data.join('\n');
try {
await navigator.clipboard.writeText(text);
this.showTableFeedback();
} catch (err) {
console.error('Failed to copy table:', err);
}
}
setupKeyboardShortcuts() {
this.table.addEventListener('keydown', async (e) => {
if (e.ctrlKey || e.metaKey) {
if (e.key === 'c') {
e.preventDefault();
await this.copySelection();
}
}
});
}
async copySelection() {
const selection = window.getSelection();
const selectedCells = this.getSelectedCells(selection);
if (selectedCells.length > 0) {
const text = this.formatCellsAsText(selectedCells);
await navigator.clipboard.writeText(text);
}
}
getSelectedCells(selection) {
const cells = [];
for (let i = 0; i < selection.rangeCount; i++) {
const range = selection.getRangeAt(i);
const cellsInRange = this.table.querySelectorAll('td, th');
cellsInRange.forEach((cell) => {
if (range.intersectsNode(cell)) {
cells.push(cell);
}
});
}
return cells;
}
formatCellsAsText(cells) {
// Group cells by row
const rows = new Map();
cells.forEach((cell) => {
const row = cell.parentElement;
if (!rows.has(row)) {
rows.set(row, []);
}
rows.get(row).push(cell);
});
// Format as tab-separated values
const text = Array.from(rows.values())
.map((rowCells) =>
rowCells.map((cell) => cell.textContent.trim()).join('\t')
)
.join('\n');
return text;
}
showCopyFeedback(element) {
element.classList.add('copied');
setTimeout(() => element.classList.remove('copied'), 1000);
}
showColumnFeedback(columnIndex) {
const cells = this.table.querySelectorAll(
`td:nth-child(${columnIndex + 1}), th:nth-child(${columnIndex + 1})`
);
cells.forEach((cell) => this.showCopyFeedback(cell));
}
showTableFeedback() {
this.table.classList.add('copied');
setTimeout(() => this.table.classList.remove('copied'), 1000);
}
}
Rich Content Copy
// Rich content editor with copy/paste
class RichContentEditor {
constructor(editorElement) {
this.editor = editorElement;
this.init();
}
init() {
this.editor.addEventListener('paste', (e) => this.handlePaste(e));
this.editor.addEventListener('copy', (e) => this.handleCopy(e));
this.editor.addEventListener('cut', (e) => this.handleCut(e));
}
async handlePaste(e) {
e.preventDefault();
// Try to read clipboard items
try {
const items = await navigator.clipboard.read();
for (const item of items) {
// Handle images
if (
item.types.includes('image/png') ||
item.types.includes('image/jpeg')
) {
const blob = await item.getType(
item.types.find((type) => type.startsWith('image/'))
);
this.insertImage(blob);
}
// Handle HTML
else if (item.types.includes('text/html')) {
const blob = await item.getType('text/html');
const html = await blob.text();
this.insertHTML(this.sanitizeHTML(html));
}
// Handle plain text
else if (item.types.includes('text/plain')) {
const blob = await item.getType('text/plain');
const text = await blob.text();
this.insertText(text);
}
}
} catch (err) {
// Fallback to clipboard data
const clipboardData = e.clipboardData;
if (clipboardData.types.includes('text/html')) {
const html = clipboardData.getData('text/html');
this.insertHTML(this.sanitizeHTML(html));
} else {
const text = clipboardData.getData('text/plain');
this.insertText(text);
}
}
}
async handleCopy(e) {
const selection = window.getSelection();
if (!selection.toString()) return;
e.preventDefault();
const range = selection.getRangeAt(0);
const container = document.createElement('div');
container.appendChild(range.cloneContents());
const html = container.innerHTML;
const text = container.textContent;
try {
const clipboardItem = new ClipboardItem({
'text/plain': new Blob([text], { type: 'text/plain' }),
'text/html': new Blob([html], { type: 'text/html' }),
});
await navigator.clipboard.write([clipboardItem]);
} catch (err) {
// Fallback
e.clipboardData.setData('text/plain', text);
e.clipboardData.setData('text/html', html);
}
}
async handleCut(e) {
await this.handleCopy(e);
const selection = window.getSelection();
if (selection.toString()) {
selection.deleteFromDocument();
}
}
insertImage(blob) {
const url = URL.createObjectURL(blob);
const img = document.createElement('img');
img.src = url;
img.style.maxWidth = '100%';
this.insertAtCursor(img);
// Clean up object URL after image loads
img.onload = () => URL.revokeObjectURL(url);
}
insertHTML(html) {
const template = document.createElement('template');
template.innerHTML = html;
const fragment = template.content;
this.insertAtCursor(fragment);
}
insertText(text) {
const textNode = document.createTextNode(text);
this.insertAtCursor(textNode);
}
insertAtCursor(content) {
const selection = window.getSelection();
if (selection.rangeCount > 0) {
const range = selection.getRangeAt(0);
range.deleteContents();
if (content instanceof DocumentFragment) {
range.insertNode(content);
} else {
range.insertNode(content);
}
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
sanitizeHTML(html) {
// Basic HTML sanitization - use DOMPurify in production
const template = document.createElement('template');
template.innerHTML = html;
// Remove script tags and event handlers
const scripts = template.content.querySelectorAll('script');
scripts.forEach((script) => script.remove());
const elements = template.content.querySelectorAll('*');
elements.forEach((el) => {
// Remove event handlers
Array.from(el.attributes).forEach((attr) => {
if (attr.name.startsWith('on')) {
el.removeAttribute(attr.name);
}
});
});
return template.innerHTML;
}
}
// Formatted data copy
class FormattedDataCopy {
static async copyAsJSON(data) {
const json = JSON.stringify(data, null, 2);
try {
await navigator.clipboard.writeText(json);
return true;
} catch (err) {
console.error('Failed to copy JSON:', err);
return false;
}
}
static async copyAsCSV(data, headers) {
const csv = this.convertToCSV(data, headers);
try {
await navigator.clipboard.writeText(csv);
return true;
} catch (err) {
console.error('Failed to copy CSV:', err);
return false;
}
}
static async copyAsMarkdown(data, headers) {
const markdown = this.convertToMarkdown(data, headers);
try {
await navigator.clipboard.writeText(markdown);
return true;
} catch (err) {
console.error('Failed to copy Markdown:', err);
return false;
}
}
static convertToCSV(data, headers) {
const rows = [];
if (headers) {
rows.push(headers.join(','));
}
data.forEach((row) => {
const values = Object.values(row).map((value) => {
// Escape values containing commas or quotes
if (
typeof value === 'string' &&
(value.includes(',') || value.includes('"'))
) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
});
rows.push(values.join(','));
});
return rows.join('\n');
}
static convertToMarkdown(data, headers) {
const rows = [];
if (headers) {
rows.push('| ' + headers.join(' | ') + ' |');
rows.push('| ' + headers.map(() => '---').join(' | ') + ' |');
}
data.forEach((row) => {
const values = Object.values(row).map((value) => String(value));
rows.push('| ' + values.join(' | ') + ' |');
});
return rows.join('\n');
}
}
Clipboard Permissions
// Permission handling
class ClipboardPermissions {
static async checkReadPermission() {
try {
const result = await navigator.permissions.query({
name: 'clipboard-read',
});
return result.state;
} catch (err) {
console.error('Failed to check clipboard read permission:', err);
return 'denied';
}
}
static async checkWritePermission() {
try {
const result = await navigator.permissions.query({
name: 'clipboard-write',
});
return result.state;
} catch (err) {
// Write permission is usually granted by default
return 'granted';
}
}
static async requestReadPermission() {
try {
// Reading will prompt for permission if needed
await navigator.clipboard.readText();
return true;
} catch (err) {
console.error('Failed to get clipboard read permission:', err);
return false;
}
}
static monitorPermissions(callback) {
navigator.permissions.query({ name: 'clipboard-read' }).then((result) => {
callback('read', result.state);
result.addEventListener('change', () => {
callback('read', result.state);
});
});
navigator.permissions
.query({ name: 'clipboard-write' })
.then((result) => {
callback('write', result.state);
result.addEventListener('change', () => {
callback('write', result.state);
});
})
.catch(() => {
// Write permission query not supported
callback('write', 'granted');
});
}
}
// Clipboard monitor
class ClipboardMonitor {
constructor() {
this.listeners = new Set();
this.lastContent = null;
this.monitoring = false;
}
start() {
if (this.monitoring) return;
this.monitoring = true;
this.checkClipboard();
}
stop() {
this.monitoring = false;
}
async checkClipboard() {
if (!this.monitoring) return;
try {
const permission = await ClipboardPermissions.checkReadPermission();
if (permission === 'granted') {
const content = await navigator.clipboard.readText();
if (content !== this.lastContent) {
this.lastContent = content;
this.notifyListeners(content);
}
}
} catch (err) {
// Silent fail - user may have denied permission
}
// Check again after delay
setTimeout(() => this.checkClipboard(), 1000);
}
addListener(callback) {
this.listeners.add(callback);
}
removeListener(callback) {
this.listeners.delete(callback);
}
notifyListeners(content) {
this.listeners.forEach((callback) => {
try {
callback(content);
} catch (err) {
console.error('Clipboard listener error:', err);
}
});
}
}
Share and Copy Integration
// Share with copy fallback
class ShareWithCopy {
constructor(data) {
this.data = data;
}
async share() {
// Try Web Share API first
if (navigator.share) {
try {
await navigator.share(this.data);
return { method: 'share', success: true };
} catch (err) {
if (err.name === 'AbortError') {
return { method: 'share', success: false, reason: 'cancelled' };
}
}
}
// Fallback to clipboard
return await this.copyToClipboard();
}
async copyToClipboard() {
const text = this.formatForClipboard();
try {
await navigator.clipboard.writeText(text);
return { method: 'clipboard', success: true };
} catch (err) {
return { method: 'clipboard', success: false, error: err };
}
}
formatForClipboard() {
const parts = [];
if (this.data.title) {
parts.push(this.data.title);
}
if (this.data.text) {
parts.push(this.data.text);
}
if (this.data.url) {
parts.push(this.data.url);
}
return parts.join('\n\n');
}
static createShareButton(buttonElement, data) {
const sharer = new ShareWithCopy(data);
buttonElement.addEventListener('click', async () => {
const result = await sharer.share();
if (result.success) {
if (result.method === 'clipboard') {
// Show copy feedback
buttonElement.textContent = 'Copied!';
setTimeout(() => {
buttonElement.textContent = 'Share';
}, 2000);
}
} else {
console.error('Share failed:', result);
}
});
}
}
// URL shortener with copy
class URLShortenerWithCopy {
constructor(apiEndpoint) {
this.apiEndpoint = apiEndpoint;
}
async shortenAndCopy(longUrl) {
try {
// Call your URL shortening API
const response = await fetch(this.apiEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: longUrl }),
});
const data = await response.json();
const shortUrl = data.shortUrl;
// Copy to clipboard
await navigator.clipboard.writeText(shortUrl);
return { success: true, shortUrl };
} catch (err) {
console.error('Failed to shorten and copy URL:', err);
// Fallback: just copy the long URL
try {
await navigator.clipboard.writeText(longUrl);
return { success: true, shortUrl: longUrl, fallback: true };
} catch (copyErr) {
return { success: false, error: copyErr };
}
}
}
}
Best Practices
- Always handle errors gracefully
async function safeCopy(text) {
try {
await navigator.clipboard.writeText(text);
return { success: true };
} catch (err) {
// Fallback for older browsers
if (document.execCommand) {
const textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try {
document.execCommand('copy');
document.body.removeChild(textarea);
return { success: true, fallback: true };
} catch (fallbackErr) {
document.body.removeChild(textarea);
return { success: false, error: fallbackErr };
}
}
return { success: false, error: err };
}
}
- Provide user feedback
class CopyFeedback {
static show(element, success = true) {
const feedback = document.createElement('div');
feedback.className = `copy-feedback ${success ? 'success' : 'error'}`;
feedback.textContent = success ? 'Copied!' : 'Failed to copy';
const rect = element.getBoundingClientRect();
feedback.style.position = 'fixed';
feedback.style.top = `${rect.top - 40}px`;
feedback.style.left = `${rect.left + rect.width / 2}px`;
feedback.style.transform = 'translateX(-50%)';
document.body.appendChild(feedback);
setTimeout(() => {
feedback.classList.add('fade-out');
setTimeout(() => feedback.remove(), 300);
}, 2000);
}
}
- Check permissions before attempting to read
async function safeRead() {
try {
const permission = await navigator.permissions.query({
name: 'clipboard-read',
});
if (permission.state === 'denied') {
throw new Error('Clipboard access denied');
}
return await navigator.clipboard.readText();
} catch (err) {
console.error('Cannot read clipboard:', err);
return null;
}
}
- Use appropriate MIME types
const mimeTypes = {
text: 'text/plain',
html: 'text/html',
png: 'image/png',
jpeg: 'image/jpeg',
svg: 'image/svg+xml',
json: 'application/json',
};
async function copyWithType(content, type) {
const blob = new Blob([content], { type: mimeTypes[type] || type });
const item = new ClipboardItem({ [blob.type]: blob });
await navigator.clipboard.write([item]);
}
Conclusion
The Clipboard API provides a secure, modern way to interact with the system clipboard. With support for various data types, async operations, and proper permission handling, it enables rich copy/paste functionality in web applications. By following best practices and providing proper fallbacks, you can create robust clipboard interactions that enhance user experience while respecting security and privacy concerns.