useClipboard
A hook for copying and reading text from the clipboard
A React hook that provides clipboard read/write functionality using the Clipboard API. Includes copy state management for showing feedback to users like "Copied!" buttons.
Source Code
View the full hook implementation in the Hook Source Code section below.
Permission Notes
Writing to the clipboard works in most modern browsers without explicit permission (on HTTPS). Reading from the clipboard requires user permission and may be blocked by some browsers for security reasons.
Features
- Copy to Clipboard - Write text to the system clipboard
- Read from Clipboard - Read text from clipboard (with permission)
- Copy State -
hasCopiedboolean with auto-reset after configurable timeout - Last Value - Track the last copied value with
copiedValue - SSR Safe - Gracefully handles server-side rendering
- Error Handling - Comprehensive error state for failed operations
Learn More
Basic Usage
A simple copy button that shows "Copied!" feedback after clicking:
Try it out!
Click the Copy button - it will show "Copied!" for 2 seconds, then reset.
Clipboard API is not supported in this browser
"use client";
import { useClipboard } from "@repo/hooks/utility/use-clipboard";
import { Check, Copy, AlertCircle } from "lucide-react";
/* BASIC USAGE - Copy Button */
export const Example1 = () => {
const { copy, hasCopied, isSupported, error } = useClipboard();
const textToCopy = "Hello, World! 👋";
if (!isSupported) {
return (
<div className="rounded-md border border-yellow-500/50 bg-yellow-500/10 p-4">
<p className="text-sm text-yellow-600 dark:text-yellow-400">
Clipboard API is not supported in this browser
</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<code className="bg-muted rounded-md px-3 py-2 text-sm">
{textToCopy}
</code>
<button
onClick={() => copy(textToCopy)}
className={`text-primary-foreground inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium transition-colors ${
hasCopied
? "bg-amber-500 hover:bg-amber-600"
: "bg-primary hover:bg-primary/90"
}`}
>
{hasCopied ? (
<>
<Check className="h-4 w-4" />
Copied!
</>
) : (
<>
<Copy className="h-4 w-4" />
Copy
</>
)}
</button>
</div>
{error && (
<div className="flex items-center gap-2 text-sm text-red-500">
<AlertCircle className="h-4 w-4" />
{error.message}
</div>
)}
</div>
);
};
Copy Input Value
Copy dynamic content from an input field with a custom 3-second timeout:
The "Copied" state resets after 3 seconds (custom timeout)
"use client";
import { useState } from "react";
import { useClipboard } from "@repo/hooks/utility/use-clipboard";
import { Check, Copy } from "lucide-react";
/* COPY INPUT VALUE - Custom Timeout */
export const Example2 = () => {
const [inputValue, setInputValue] = useState("npm install @repo/hooks");
const { copy, hasCopied, copiedValue } = useClipboard({ timeout: 3000 });
return (
<div className="flex flex-col gap-4">
<div className="flex gap-2">
<input
type="text"
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
className="border-input bg-background flex-1 rounded-md border px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Enter text to copy..."
/>
<button
onClick={() => copy(inputValue)}
disabled={!inputValue.trim()}
className={`inline-flex items-center gap-2 rounded-md px-4 py-2 text-sm font-medium text-white transition-colors disabled:cursor-not-allowed disabled:opacity-50 ${
hasCopied
? "bg-green-600"
: "bg-blue-600 hover:bg-blue-700"
}`}
>
{hasCopied ? (
<Check className="h-4 w-4" />
) : (
<Copy className="h-4 w-4" />
)}
</button>
</div>
{copiedValue && (
<p className="text-muted-foreground text-sm">
Last copied:{" "}
<code className="bg-muted rounded px-1.5 py-0.5">
{copiedValue}
</code>
</p>
)}
<p className="text-muted-foreground text-xs">
The "Copied" state resets after 3 seconds (custom
timeout)
</p>
</div>
);
};
Code Block Copy
A common pattern for documentation - copy buttons for code snippets. Uses copiedValue to track which specific snippet was copied:
npm install @repo/ui @repo/hooksimport { Button } from "@repo/ui/button";<Button variant="primary">Click me</Button>"use client";
import { useClipboard } from "@repo/hooks/utility/use-clipboard";
import { Check, Copy } from "lucide-react";
/* CODE BLOCK - Click to Copy */
export const Example3 = () => {
const { copy, hasCopied, copiedValue } = useClipboard();
const codeSnippets = [
{
label: "Install",
code: "npm install @repo/ui @repo/hooks",
},
{
label: "Import",
code: 'import { Button } from "@repo/ui/button";',
},
{
label: "Usage",
code: '<Button variant="primary">Click me</Button>',
},
];
return (
<div className="flex flex-col gap-3">
{codeSnippets.map((snippet) => (
<div
key={snippet.label}
className="group relative overflow-hidden rounded-lg border"
>
<div className="bg-muted/50 flex items-center justify-between border-b px-3 py-1.5">
<span className="text-muted-foreground text-xs font-medium">
{snippet.label}
</span>
<button
onClick={() => copy(snippet.code)}
className="text-muted-foreground hover:text-foreground inline-flex items-center gap-1.5 text-xs transition-colors"
>
{hasCopied && copiedValue === snippet.code ? (
<>
<Check className="h-3.5 w-3.5 text-green-500" />
<span className="text-green-500">
Copied!
</span>
</>
) : (
<>
<Copy className="h-3.5 w-3.5" />
<span>Copy</span>
</>
)}
</button>
</div>
<pre className="bg-muted/30 overflow-x-auto p-3">
<code className="text-sm">{snippet.code}</code>
</pre>
</div>
))}
</div>
);
};
Read from Clipboard
Read text from the user's clipboard. This requires explicit permission and may be blocked by some browsers:
Browser Permissions
Reading from clipboard is more restricted than writing. Users will see a permission prompt, and some browsers (especially on mobile) may not support this feature at all.
Clipboard API is not supported in this browser
"use client";
import { useState } from "react";
import { useClipboard } from "@repo/hooks/utility/use-clipboard";
import { ClipboardPaste, AlertCircle } from "lucide-react";
/* READ FROM CLIPBOARD */
export const Example4 = () => {
const { read, isSupported, error } = useClipboard();
const [pastedText, setPastedText] = useState<string | null>(null);
const [isReading, setIsReading] = useState(false);
const handlePaste = async () => {
setIsReading(true);
const text = await read();
setPastedText(text);
setIsReading(false);
};
if (!isSupported) {
return (
<div className="rounded-md border border-yellow-500/50 bg-yellow-500/10 p-4">
<p className="text-sm text-yellow-600 dark:text-yellow-400">
Clipboard API is not supported in this browser
</p>
</div>
);
}
return (
<div className="flex flex-col gap-4">
<div className="flex items-start gap-3">
<button
onClick={handlePaste}
disabled={isReading}
className="inline-flex items-center gap-2 rounded-md bg-purple-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-purple-700 disabled:opacity-50"
>
<ClipboardPaste className="h-4 w-4" />
{isReading ? "Reading..." : "Paste from Clipboard"}
</button>
</div>
{pastedText !== null && (
<div className="bg-muted/30 rounded-md border p-3">
<p className="text-muted-foreground mb-1 text-xs font-medium">
Clipboard content:
</p>
<p className="break-all text-sm">
{pastedText || (
<span className="text-muted-foreground italic">
(empty)
</span>
)}
</p>
</div>
)}
{error && (
<div className="flex items-start gap-2 rounded-md border border-red-500/50 bg-red-500/10 p-3">
<AlertCircle className="mt-0.5 h-4 w-4 shrink-0 text-red-500" />
<div className="text-sm">
<p className="font-medium text-red-600 dark:text-red-400">
Permission Denied
</p>
<p className="text-muted-foreground mt-1 text-xs">
Reading from clipboard requires explicit permission.
Some browsers may block this action.
</p>
</div>
</div>
)}
<p className="text-muted-foreground text-xs">
💡 Try copying some text, then click the button to paste it
here.
</p>
</div>
);
};
API Reference
Hook Signature
function useClipboard(options?: UseClipboardOptions): UseClipboardReturn;Options
| Property | Type | Default | Description |
|---|---|---|---|
timeout | number | 2000 | Duration in milliseconds before hasCopied resets to false |
Return Value
| Property | Type | Description |
|---|---|---|
copy | (text: string) => Promise<boolean> | Copy text to clipboard. Returns true on success |
read | () => Promise<string | null> | Read text from clipboard. Requires permission |
isCopying | boolean | true while a copy operation is in progress |
hasCopied | boolean | true after successful copy, resets after timeout |
copiedValue | string | null | The last successfully copied text |
error | Error | null | Error object if the last operation failed |
isSupported | boolean | false if the Clipboard API is not available |
reset | () => void | Manually reset hasCopied and copiedValue states |
Common Patterns
Copy with Toast Notification
const { copy } = useClipboard();
const handleCopy = async () => {
const success = await copy("text to copy");
if (success) {
toast.success("Copied to clipboard!");
} else {
toast.error("Failed to copy");
}
};Copy Current URL
const { copy, hasCopied } = useClipboard();
<button onClick={() => copy(window.location.href)}>
{hasCopied ? "Link Copied!" : "Share Link"}
</button>;Persistent Copy State
If you need to keep the copied state indefinitely (until manual reset):
const { copy, hasCopied, reset } = useClipboard({ timeout: 0 });
// hasCopied stays true until you call reset()
<button onClick={reset}>Clear</button>;Hook Source Code
import { useState, useCallback } from "react";
/**
* Options for the useClipboard hook
*/
export interface UseClipboardOptions {
/** Duration in milliseconds before `hasCopied` resets to false (default: 2000) */
timeout?: number;
}
/**
* Return type for the useClipboard hook
*/
export interface UseClipboardReturn {
/** Copy text to clipboard */
copy: (text: string) => Promise<boolean>;
/** Read text from clipboard (requires permission) */
read: () => Promise<string | null>;
/** Whether copying is in progress */
isCopying: boolean;
/** Whether text was recently copied (resets after timeout) */
hasCopied: boolean;
/** The last copied text value */
copiedValue: string | null;
/** Error if copy/read operation failed */
error: Error | null;
/** Whether the Clipboard API is supported */
isSupported: boolean;
/** Reset hasCopied state manually */
reset: () => void;
}
/**
* A React hook that provides clipboard read/write functionality using the
* Clipboard API. Includes copy state management for showing feedback to users.
*
* @param options - Configuration options for the hook
* @returns UseClipboardReturn object with copy/read functions and state
*
* @example
* ```tsx
* const { copy, hasCopied, isSupported } = useClipboard();
*
* return (
* <button onClick={() => copy("Hello, World!")}>
* {hasCopied ? "Copied!" : "Copy"}
* </button>
* );
* ```
*/
export function useClipboard(
options: UseClipboardOptions = {},
): UseClipboardReturn {
const { timeout = 2000 } = options;
const [isCopying, setIsCopying] = useState(false);
const [hasCopied, setHasCopied] = useState(false);
const [copiedValue, setCopiedValue] = useState<string | null>(null);
const [error, setError] = useState<Error | null>(null);
// Check if Clipboard API is supported
const isSupported =
typeof navigator !== "undefined" &&
"clipboard" in navigator &&
typeof navigator.clipboard.writeText === "function";
// Reset hasCopied state
const reset = useCallback(() => {
setHasCopied(false);
setCopiedValue(null);
setError(null);
}, []);
// Copy text to clipboard
const copy = useCallback(
async (text: string): Promise<boolean> => {
if (!isSupported) {
setError(new Error("Clipboard API is not supported"));
return false;
}
setIsCopying(true);
setError(null);
try {
await navigator.clipboard.writeText(text);
setCopiedValue(text);
setHasCopied(true);
// Reset hasCopied after timeout
if (timeout > 0) {
setTimeout(() => {
setHasCopied(false);
}, timeout);
}
return true;
} catch (err) {
const error =
err instanceof Error
? err
: new Error("Failed to copy to clipboard");
setError(error);
setHasCopied(false);
setCopiedValue(null);
return false;
} finally {
setIsCopying(false);
}
},
[isSupported, timeout],
);
// Read text from clipboard
const read = useCallback(async (): Promise<string | null> => {
if (!isSupported) {
setError(new Error("Clipboard API is not supported"));
return null;
}
// Check if readText is available (not all browsers support it)
if (typeof navigator.clipboard.readText !== "function") {
setError(new Error("Reading from clipboard is not supported"));
return null;
}
setError(null);
try {
const text = await navigator.clipboard.readText();
return text;
} catch (err) {
const error =
err instanceof Error
? err
: new Error("Failed to read from clipboard");
setError(error);
return null;
}
}, [isSupported]);
return {
copy,
read,
isCopying,
hasCopied,
copiedValue,
error,
isSupported,
reset,
};
}
export default useClipboard;