Skip to main content

What have I copied?

· 3 min read
Filip Tammergård
Software Engineer at Frilans Finans

Have you ever copied something and immediately forgotten what it was?

TL;DR​

  • navigator.clipboard.readText() returns a Promise<string> with the current clipboard text.
  • It is only available in a secure context (HTTPS or localhost), and the read has to happen during a user activation — that's why it's wired up to a button click rather than running on mount.
  • The call can reject (permission denied, document not focused, no clipboard support, …), so it always belongs in a try/catch.

Click the button to see what's currently on your clipboard:

The Clipboard API​

The browser exposes the user's clipboard through navigator.clipboard. To read it as text we call readText, which returns a Promise that resolves with whatever is on the clipboard:

const text = await navigator.clipboard.readText()

That one line comes with a few constraints worth knowing about.

Secure context only​

navigator.clipboard is only defined in a secure context — that means HTTPS in production and localhost during development. On plain HTTP the property is undefined, so even feature-detecting with navigator.clipboard?.readText is a good idea before reaching for it.

User activation​

Pages aren't allowed to read the clipboard in the background. The read has to happen while the browser is handling a user activation — a click, a key press, or a similar gesture. That's not just a recommendation; the browser will reject the promise otherwise. The first read may also trigger a permission prompt, depending on the browser.

A button click is the simplest way to satisfy that requirement:

async function read() {
try {
const value = await navigator.clipboard.readText()
setClipboard(value.trim())
} catch {
setClipboard("Couldn't show what you copied…")
}
}

trim() strips leading and trailing whitespace, so a copy that's just a stray space or newline doesn't render as an invisible blank.

Why the try/catch matters​

readText can reject for several reasons, and the failure is hard to predict from inside the component:

  • The user denied (or doesn't have) clipboard-read permission.
  • The page isn't a secure context.
  • The browser is older than the API.
  • The document isn't focused — Firefox in particular is strict about this.

A single catch is enough — for a casual "show me my clipboard" UI there's nothing useful to do beyond telling the user it didn't work.

Text only​

readText only returns text. If the clipboard holds an image or a file, the result is an empty string. For the richer cases there's navigator.clipboard.read(), which returns ClipboardItems carrying the available MIME types.

Putting it together​

The full React component looks like this:

import { useState } from "react"

function Clipboard() {
const [clipboard, setClipboard] = useState<string>()

async function read() {
try {
const value = await navigator.clipboard.readText()
setClipboard(value.trim())
} catch {
setClipboard("Couldn't show what you copied…")
}
}

return (
<div>
<button type="button" onClick={read}>
Show what I copied
</button>
{clipboard !== undefined && (
<p>{clipboard || "You haven't copied anything…"}</p>
)}
</div>
)
}

So next time you wonder what's on your clipboard but refuse to test by pasting somewhere, you can use this page instead!

References​