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