Implementing Clipboard Functionality
In this tutorial, you will learn how to implement a "Copy to Clipboard" feature with automatic state management and visual feedback. You will build a reusable component that allows users to copy unique identifiers (IDs) from a data table, similar to the implementation found in the Items dashboard.
Prerequisites
To follow this tutorial, you need the following components and hooks already available in your project:
useCopyToClipboard: The custom hook located infrontend/src/hooks/useCopyToClipboard.ts.- Lucide Icons: Specifically
CheckandCopyfor visual feedback. - Shadcn UI Button: The base button component used for the interaction.
Step 1: Initialize the Clipboard Hook
First, you need to import and initialize the useCopyToClipboard hook within your functional component. This hook returns a tuple containing the current state and a function to perform the copy operation.
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
function CopyId({ id }: { id: string }) {
const [copiedText, copy] = useCopyToClipboard()
// ... component logic
}
The useCopyToClipboard hook manages two internal types:
CopiedValue: Astring | nullrepresenting the text currently in the clipboard state.CopyFn: An asynchronous function(text: string) => Promise<boolean>that handles the browser's Clipboard API.
Step 2: Determine the Copied State
To provide visual feedback, you need to know if the specific ID associated with this component instance is the one currently stored in the hook's state.
const isCopied = copiedText === id
Because the hook automatically resets its state to null after 2 seconds (as defined in frontend/src/hooks/useCopyToClipboard.ts), isCopied will automatically revert to false, allowing the UI to switch back from a "Success" state to the "Default" state.
Step 3: Implement the Interactive Button
Now, create the UI using a button that triggers the copy function. You will use the isCopied boolean to conditionally render different icons.
import { Check, Copy } from "lucide-react"
import { Button } from "@/components/ui/button"
// Inside your CopyId component return statement:
<Button
variant="ghost"
size="icon"
className="size-6"
onClick={() => copy(id)}
>
{isCopied ? (
<Check className="size-3 text-green-500" />
) : (
<Copy className="size-3" />
)}
<span className="sr-only">Copy ID</span>
</Button>
When the user clicks the button, the copy(id) function is called. This function performs the following actions internally:
- Checks if
navigator.clipboardis supported by the browser. - Attempts to write the text to the clipboard using
navigator.clipboard.writeText(text). - Updates the
copiedTextstate. - Sets a timeout to clear the state after 2000ms.
Step 4: Complete Implementation
The following example shows the complete CopyId component as implemented in frontend/src/components/Items/columns.tsx. It includes styling to show the copy button only when the user hovers over the ID container.
import { Check, Copy } from "lucide-react"
import { Button } from "@/components/ui/button"
import { useCopyToClipboard } from "@/hooks/useCopyToClipboard"
function CopyId({ id }: { id: string }) {
const [copiedText, copy] = useCopyToClipboard()
const isCopied = copiedText === id
return (
<div className="flex items-center gap-1.5 group">
<span className="font-mono text-xs text-muted-foreground">{id}</span>
<Button
variant="ghost"
size="icon"
className="size-6 opacity-0 group-hover:opacity-100 transition-opacity"
onClick={() => copy(id)}
>
{isCopied ? (
<Check className="size-3 text-green-500" />
) : (
<Copy className="size-3" />
)}
<span className="sr-only">Copy ID</span>
</Button>
</div>
)
}
How it Works
The underlying hook useCopyToClipboard ensures that the operation is safe and provides consistent behavior:
- Safety Checks: It verifies
navigator?.clipboardexists to prevent crashes in older browsers or non-secure contexts. - Error Handling: If the browser blocks the clipboard action, it catches the error, logs a warning, and resets the state to
null. - Auto-Reset: The
setTimeout(() => setCopiedText(null), 2000)call inside the hook is what allows your UI to "flicker" the checkmark icon briefly before returning to the copy icon, providing a polished user experience without requiring manual state management in your components.