Description:
In modern web applications, providing users with tools to interact visually with content—such as capturing screenshots of specific components—can greatly enhance user experience. A practical use case is enabling users to capture screenshots of individual <li>
(list item) elements directly from a React component and automatically copy them to the clipboard.
This tutorial walks through a simple and efficient implementation using React
, html2canvas
, and the Clipboard API.
Why You Might Need This
Imagine a scenario where your app displays a list of notes, tasks, or posts. Allowing users to quickly take a visual snapshot of any specific item—without capturing the entire screen—can streamline workflows, aid in sharing, and enhance visual feedback.
Step-by-Step Implementation
1. Set Up Your Project
Make sure you're working in a React environment. First, install html2canvas
:
bash
npm install html2canvas
2. Create a Reusable Component
You’ll use useRef
to store references to each <li>
element and a button in each list item to trigger the screenshot.
Here’s a full example:
jsx
import React, { useRef } from 'react';
import html2canvas from 'html2canvas';
const items = [
'First item',
'Second item',
'Third item',
'Fourth item'
];
const ScreenshotList = () => {
const itemRefs = useRef([]);
const handleScreenshot = async (index) => {
const target = itemRefs.current[index];
if (!target) return;
try {
const canvas = await html2canvas(target);
canvas.toBlob(async (blob) => {
if (blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
alert('Screenshot copied to clipboard!');
} catch (err) {
console.error('Clipboard write failed:', err);
}
}
});
} catch (err) {
console.error('Screenshot capture failed:', err);
}
};
return (
<ul>
{items.map((text, index) => (
<li
key={index}
ref={(el) => (itemRefs.current[index] = el)}
style={{
marginBottom: '10px',
padding: '10px',
border: '1px solid #ccc',
position: 'relative'
}}
>
{text}
<button
onClick={() => handleScreenshot(index)}
style={{ marginLeft: '10px' }}
>
Copy Screenshot
</button>
</li>
))}
</ul>
);
};
export default ScreenshotList;
How It Works
html2canvas
takes the <li>
DOM node and renders it to a canvas.- The canvas is converted to a
Blob
(PNG image). - The Clipboard API (
navigator.clipboard.write
) copies the image to the clipboard. - Users can paste it directly into an email, document, or chat.
Browser Support
- Clipboard API (image support): Mostly supported in modern Chromium-based browsers like Chrome and Edge.
- Security: This feature only works over
https://
or on localhost
in development.
Use Cases
- Task management apps (copy individual tasks)
- Notes apps
- Bug reporting tools
- Social sharing of individual list items
Nice! Let’s enhance the component with:
- Modern button styles (with CSS-in-JS styling or classes).
- Two additional features for each
<li>
:
- Copy screenshot to clipboard (image).
- Download content of
<li>
as .txt
file.
Updated React Component:
jsx
import React, { useRef } from 'react';
import html2canvas from 'html2canvas';
const items = [
'First item',
'Second item',
'Third item',
'Fourth item'
];
const ScreenshotList = () => {
const itemRefs = useRef([]);
const handleScreenshot = async (index) => {
const target = itemRefs.current[index];
if (!target) return;
try {
const canvas = await html2canvas(target);
canvas.toBlob(async (blob) => {
if (blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
alert('Screenshot copied to clipboard!');
} catch (err) {
console.error('Clipboard write failed:', err);
}
}
});
} catch (err) {
console.error('Screenshot capture failed:', err);
}
};
const handleDownloadText = (index) => {
const li = itemRefs.current[index];
if (!li) return;
const text = li.innerText.replace(/Copy Screenshot|Download Text/g, '').trim();
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `item-${index + 1}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const buttonStyle = {
padding: '6px 10px',
marginLeft: '8px',
border: 'none',
borderRadius: '4px',
backgroundColor: '#007bff',
color: '#fff',
cursor: 'pointer',
fontSize: '0.9rem'
};
const secondaryButtonStyle = {
...buttonStyle,
backgroundColor: '#28a745'
};
return (
<ul style={{ listStyleType: 'none', padding: 0 }}>
{items.map((text, index) => (
<li
key={index}
ref={(el) => (itemRefs.current[index] = el)}
style={{
marginBottom: '12px',
padding: '12px',
border: '1px solid #ccc',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<span>{text}</span>
<div>
<button
onClick={() => handleScreenshot(index)}
style={buttonStyle}
>
Copy Screenshot
</button>
<button
onClick={() => handleDownloadText(index)}
style={secondaryButtonStyle}
>
Download Text
</button>
</div>
</li>
))}
</ul>
);
};
export default ScreenshotList;
What’s New:
- Buttons styled to look modern (bootstrap-inspired look).
Download Text
button creates and downloads a .txt
file with the content of the <li>
.- Clean layout using Flexbox.
Here's the complete, self-contained React component with:
- Font Awesome icons
- Modern styled buttons
- Screenshot to clipboard
- Download
.txt
feature - Clean layout
Final Code:
jsx
// ScreenshotList.js
import React, { useRef } from 'react';
import html2canvas from 'html2canvas';
const items = [
'First item',
'Second item',
'Third item',
'Fourth item'
];
const ScreenshotList = () => {
const itemRefs = useRef([]);
const handleScreenshot = async (index) => {
const target = itemRefs.current[index];
if (!target) return;
try {
const canvas = await html2canvas(target);
canvas.toBlob(async (blob) => {
if (blob) {
try {
await navigator.clipboard.write([
new ClipboardItem({ 'image/png': blob })
]);
alert('Screenshot copied to clipboard!');
} catch (err) {
console.error('Clipboard write failed:', err);
}
}
});
} catch (err) {
console.error('Screenshot capture failed:', err);
}
};
const handleDownloadText = (index) => {
const li = itemRefs.current[index];
if (!li) return;
const text = li.innerText.replace(/Copy Screenshot|Download Text/g, '').trim();
const blob = new Blob([text], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `item-${index + 1}.txt`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const buttonStyle = {
padding: '6px 10px',
marginLeft: '8px',
border: 'none',
borderRadius: '4px',
backgroundColor: '#007bff',
color: '#fff',
cursor: 'pointer',
fontSize: '0.9rem',
transition: 'background-color 0.2s'
};
const secondaryButtonStyle = {
...buttonStyle,
backgroundColor: '#28a745'
};
return (
<ul style={{ listStyleType: 'none', padding: 0 }}>
{items.map((text, index) => (
<li
key={index}
ref={(el) => (itemRefs.current[index] = el)}
style={{
marginBottom: '12px',
padding: '12px',
border: '1px solid #ccc',
borderRadius: '6px',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between'
}}
>
<span>{text}</span>
<div>
<button
onClick={() => handleScreenshot(index)}
style={buttonStyle}
>
<i className="fas fa-copy" style={{ marginRight: '6px' }}></i>
Copy Screenshot
</button>
<button
onClick={() => handleDownloadText(index)}
style={secondaryButtonStyle}
>
<i className="fas fa-download" style={{ marginRight: '6px' }}></i>
Download Text
</button>
</div>
</li>
))}
</ul>
);
};
export default ScreenshotList;
Add Font Awesome to public/index.html
:
html
<!-- public/index.html -->
<head>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css"
integrity="sha512-Fo3rlrZj/k7ujTTXRN+2H5QZ1oR3N6FhN0zJdLR0mVnxCyaDfCCQd7K8XTCaULjZ7R8uF8nE5Y0knc9VZfU2NA=="
crossorigin="anonymous"
referrerpolicy="no-referrer"
/>
</head>
✅ What You Get:
- Responsive layout
- Screenshot-to-clipboard per
<li>
- Download button to save
.txt
version - Font Awesome icons (
copy
, download
) - Clear, modern UI