Docs

Clipboard

Using the Clipboard API to copy text, HTML, and images to the user’s clipboard, read clipboard content, and handle paste events from the server.

The Clipboard API lets server-side Java code copy text, HTML, and images to the user’s clipboard through the browser Clipboard API. Actions are bound to a clickable component and run during the DOM event that fires the underlying click, so the browser sees a fresh user gesture and allows the write. The same click binding can also read the clipboard, and separate listeners react to browser paste events — including pasted files — without any click binding.

Note
The Clipboard API requires a secure context (HTTPS), except on localhost during development. Browsers also require the write call to happen inside a short-lived user gesture (click, key press, …) — writes triggered outside of a gesture, for example from a background thread or a timer, will fail with a NotAllowedError.

Copying Text

Clipboard.onClick(component) returns a ClipboardBinding — a fluent surface that attaches a clipboard action to the component’s click event. The typical "Copy to clipboard" button is one line:

Source code
Java
Button copy = new Button("Copy");
Clipboard.onClick(copy).writeText("Hello, world!");

Every subsequent click of the button copies the literal string to the clipboard. The call returns immediately and does not subscribe to the result — see Observing the Outcome if you need to know whether the write succeeded.

Copying a Field Value

To copy the current value of an input field, pass the component to writeText(). The value is read on the client at the moment the click fires, so user edits are reflected without a server round-trip:

Source code
Java
TextField token = new TextField("API token");
Button copy = new Button("Copy");
Clipboard.onClick(copy).writeText(token);

Any component implementing HasValue<?, String> works — TextField, TextArea, PasswordField, EmailField, and so on.

Other Click Sources

Clipboard.onClick() accepts any component implementing ClickNotifier, not just Button. A ContextMenu item is a common alternative — the user right-clicks a target and picks "Copy" from the menu:

Source code
Java
ContextMenu menu = new ContextMenu(target);
MenuItem copyItem = menu.addItem("Copy value");
Clipboard.onClick(copyItem).writeText(value,
        written -> Notification.show("Token copied"),
        error -> Notification.show("Copy failed: " + error.message()));

The menu item’s click counts as the user gesture, so the write is allowed. The same pattern works for icons, list items, or any custom component that fires a click event.

Copying HTML

Use writeHtml() to put a text/html payload on the clipboard. This is what rich-text targets (word processors, mail composers) paste as formatted content:

Source code
Java
Button copy = new Button("Copy formatted");
Clipboard.onClick(copy).writeHtml("<b>Hello</b>, <i>world</i>!");

Most targets fall back to a plain-text representation when only text/html is present, but the fallback is browser-dependent. To control both representations explicitly, use the multi-format form below.

Copying Images

Use writeImage() to copy an image to the clipboard:

Source code
Java
Image preview = new Image("images/chart.png", "Chart preview");
Clipboard.onClick(copy).writeImage(preview);

The image slot in the clipboard only accepts a component whose root element is an <img>. The source can be a same-origin image, a data: URL, or any cross-origin image served with matching CORS headers and crossorigin="anonymous" on the <img>.

When the image is rendered server-side — for example a dynamic chart that the user wants to paste into an email or slide deck — pass a DownloadHandler instead:

Source code
Java
Button copyChart = new Button("Copy chart");
Clipboard.onClick(copyChart).writeImage(DownloadHandler.fromInputStream(event -> {
    // Produce the PNG bytes for the chart. How you render the image is
    // application-specific -- here a service turns the report data into a PNG.
    byte[] png = chartRenderer.renderPng(report.currentData());
    return new DownloadResponse(new ByteArrayInputStream(png),
            "chart.png", "image/png", png.length);
}));

To include an image together with text or HTML, use ClipboardContent.image() in the multi-format form.

Multi-Format Content

ClipboardContent packs several MIME types into a single clipboard item, so the paste target can pick the representation it understands. Set the text/plain, text/html, and image/png slots independently; at least one must be set.

Source code
Java
Image preview = new Image("images/chart.png", "Chart preview");
Button copy = new Button("Copy");
Clipboard.onClick(copy).write(ClipboardContent.create()
        .text("Hello, world!")
        .html("<b>Hello</b>, <i>world</i>!")
        .image(preview));

The text() setter also accepts a component, with the same client-side read semantics as writeText(Component):

Source code
Java
TextField message = new TextField();
Button copy = new Button("Copy");
Clipboard.onClick(copy).write(ClipboardContent.create()
        .text(message)
        .html("<blockquote>" + message.getValue() + "</blockquote>"));

The HTML slot only accepts a literal string — there is no component overload. If the HTML depends on a field value, build it on the server before binding, or use the plain-text component overload alongside a static HTML literal.

Reading the Clipboard

The same ClipboardBinding that writes can also read. The read* methods read the clipboard via navigator.clipboard.read() when the bound click fires and deliver the result to a server-side callback. readText() delivers the text/plain content:

Source code
Java
TextField address = new TextField("Address");
Button paste = new Button("Paste");
Clipboard.onClick(paste).readText(
        text -> address.setValue(text != null ? text : ""),
        error -> Notification.show("Couldn't read: " + error.message()));

Both consumers are required. The payload consumer receives null when the clipboard is empty or has no representation of the requested type. readHtml() works the same way for the text/html representation.

To receive both textual representations at once, use read(). It delivers a ClipboardPayload record whose text() and html() accessors are each null when the corresponding MIME type is not present on the clipboard item:

Source code
Java
Clipboard.onClick(paste).read(
        payload -> {
            if (payload == null) {
                Notification.show("Clipboard is empty");
            } else if (payload.html() != null) {
                renderHtml(payload.html());
            } else {
                renderText(payload.text());
            }
        },
        error -> Notification.show("Couldn't read: " + error.message()));
Note
Reading is more restricted than writing: in addition to the user gesture, the browser requires the user to grant the clipboard-read permission, typically through a prompt on the first read. If the permission is denied, the read is rejected with a NotAllowedError, which is delivered to the onError consumer — see Handling Errors.

Observing the Outcome

All write* methods have an overload that takes onCopied and onError consumers. The write becomes observed: after the browser resolves or rejects the underlying promise, the framework invokes one of the consumers on the UI thread.

Source code
Java
Clipboard.onClick(copy).writeText(token,
        copied -> Notification.show("Copied " + copied),
        err -> Notification.show("Couldn't copy: " + err.message()));

onCopied receives the string that was copied — the text/plain value if present, otherwise the text/html value. This is useful when the input was a component value, since the exact string is only known on the client.

Both consumers are required in the observed form. To opt out of one side, pass an empty lambda:

Source code
Java
Clipboard.onClick(copy).writeText(token,
        copied -> { /* ignore */ },
        err -> log.warn("clipboard write failed: {}", err.message()));

Handling Errors

The onError consumer receives a PromiseAction.Error record with the browser’s rejection details:

name()

The DOM exception class name, typically NotAllowedError when the user has denied clipboard access or the write happened outside a user gesture. Switch on this when you need typed handling.

message()

The free-form description from the browser. Useful for logging and diagnostics; not meant for end users.

Source code
Java
Clipboard.onClick(copy).writeText(token,
        copied -> Notification.show("Copied"),
        err -> {
            String userMessage = switch (err.name()) {
                case "NotAllowedError" ->
                    "Clipboard access is blocked. Please allow it in your browser settings.";
                default ->
                    "Couldn't copy to clipboard.";
            };
            Notification.show(userMessage);
            log.warn("clipboard write failed: {} -- {}",
                    err.name(), err.message());
        });

The Clipboard API does not expose a permission state the same way other browser APIs do, so there is no separate availability signal — handle errors at the point of the write.

User Gesture Requirement

The Clipboard API requires every write to happen inside a fresh user gesture. A few consequences:

  • Writes cannot be initiated from a background thread, a scheduled task, or a server push. The gesture must originate in the browser.

  • Each click is one gesture. Chaining multiple write* calls on the same binding attaches multiple actions to the same click, but each one needs to succeed against the browser’s gesture budget.

  • If the application performs a long server round-trip before the write call, the browser may consider the gesture stale and reject the write.

Reads bound to a click are subject to the same requirement — the click satisfies the gesture, but the clipboard-read permission is still checked separately.

Handling Paste Events

Clipboard.onPaste(component, listener) registers a server-side listener for the browser’s native paste event — the one the browser fires when the user presses Ctrl+V (Cmd+V on macOS) or picks Paste from a context menu. Unlike the write and read APIs, it needs no click binding — the listener is attached directly to the component’s element and is invoked on the UI thread once per paste gesture targeting the component, or any of its descendants, since paste bubbles. The listener receives a PasteEvent carrying the text/plain and text/html representations of the pasted content:

Source code
Java
Div pasteTarget = new Div();
pasteTarget.getElement().setAttribute("tabindex", "0");
add(pasteTarget);

Clipboard.onPaste(pasteTarget, event -> {
    if (event.hasHtml()) {
        renderHtml(event.getHtml());
    } else if (event.hasText()) {
        renderText(event.getText());
    }
});

The call returns a Registration; call remove() on it to detach the listener. The component does not need to be attached at registration time — the DOM listener is applied when the element is attached to a UI.

PasteEvent exposes the paste through these methods:

getText(), getHtml()

The text/plain and text/html content, or null when the paste did not include that representation. The browser returns an empty string both for an absent MIME type and for a pasted empty string; PasteEvent collapses both into null, so hasText() and hasHtml() are sufficient checks.

getTargetElement()

The closest Flow-tracked Element ancestor of the paste’s DOM target, resolved against the live DOM in the browser. For a paste inside a Vaadin web component this is typically the component’s host element. Use Element.getComponent() to look up the enclosing component. Returns null in the rare case that no Flow-tracked element encloses the target.

getSource()

The component the listener was registered on.

A few browser caveats apply:

  • The browser only fires paste when the target element is focused at the moment the user invokes paste. Non-editable elements such as a plain Div must be made focusable, typically via tabindex="0".

  • On editable targets (<input>, <textarea>, contenteditable elements), the browser still performs its native paste insertion. The listener observes the paste; it does not replace it.

  • On Safari, some plain-text pastes do not include a text/html representation even when Chromium would synthesize one, so getHtml() may be null on Safari for a paste that yields HTML on Chrome.

Files and binary clipboard items — pasted screenshots, files copied from a file manager — are not delivered by PasteEvent. Register for those with Clipboard.onFilePaste() instead; the same paste gesture fires both listeners when both are registered.

Page-Wide Listeners and Paste Options

To observe pastes anywhere on the page, pass the UI as the component — its root element is <body>, so every bubbling paste event reaches it. A page-wide listener usually should not react to pastes the user intends for a form field. The PasteOptions overload controls this:

Source code
Java
Clipboard.onPaste(UI.getCurrent(), PasteOptions.defaults(), event -> {
    importFromClipboard(event);
});
PasteOptions.defaults()

Skips pastes whose target is an input, textarea, or contenteditable element. The check sees through open shadow DOMs, so a focused field inside a Vaadin web component is also skipped.

PasteOptions.includingInputFields()

Forwards every paste regardless of focus. This is also the behavior of the two-argument onPaste() overload, which takes no options.

Handling Pasted Files

Clipboard.onFilePaste(component, handler) registers a listener for pastes that deliver files — a screenshot from the OS clipboard, files copied from a file manager, anything that arrives on event.clipboardData.files. Each pasted file is uploaded to the server individually and handed to the given UploadHandler, exactly as for a regular upload; pastes without files are ignored. The simplest form uses a plain in-memory handler:

Source code
Java
Clipboard.onFilePaste(this, UploadHandler.inMemory(
        (metadata, bytes) -> store(metadata.fileName(), bytes)));

UI changes made in the handler are flushed to the browser automatically once the paste’s uploads have finished — no server push is required.

For paste-aware handling, PasteFileHandler builds upload handlers that correlate the parallel uploads of one paste gesture. PasteFileHandler.perFile() delivers each file as a PasteFile record on the UI thread:

Source code
Java
Clipboard.onFilePaste(this, PasteFileHandler.perFile(file -> {
    if (file.newPaste()) {
        attachments.removeAll();
    }
    attachments.add(createPreview(file.fileName(), file.bytes()));
}));

PasteFile carries the file’s bytes plus metadata and paste correlation:

fileName(), contentType(), size(), bytes()

The file name and MIME type as reported by the browser (contentType() may be null), the size in bytes, and the content itself.

pasteId()

A sequence number shared by all files of the same paste gesture; subsequent pastes in the same tab carry strictly larger values.

newPaste()

true on the first file of each paste to reach the listener, false for the rest.

totalFiles()

The total number of files the originating paste contained.

When the application needs to know when a whole paste has finished, use the three-step batch form. onStart fires once per paste before its first file, onFile fires per file, and onComplete fires once the paste’s declared file count has been delivered; any step may be omitted:

Source code
Java
Clipboard.onFilePaste(this, PasteFileHandler.batch()
        .onStart(start -> progress.setVisible(true))
        .onFile(file -> addAttachment(file.fileName(), file.bytes()))
        .onComplete(complete -> progress.setVisible(false))
        .build());

onStart receives a PasteStart record with pasteId() and totalFiles(); onComplete receives a PasteComplete with pasteId() and receivedFiles(). Each paste runs as its own batch and completes on its own timeline, even when the uploads of two pastes interleave in transit. An upload that fails in transit never reaches the server and does not count, so a paste with a lost upload never fires onComplete.

PasteFileHandler.perFile() and the onFile step read each upload fully into memory as a byte[] before invoking the callback, so a very large paste is held entirely in heap. Cap it with the size limits of a custom UploadHandler, or read the stream yourself, when that matters. Also treat fileName() as untrusted browser input — never use it directly as a filesystem path without sanitizing it first.

Unlike onPaste(), the file variant has no option to skip pastes into form fields: browsers never paste files into an <input> or <textarea>, so there is no native paste behavior to compete with. The API is independent of onPaste() — a paste that carries both files and text fires both listeners when both are registered.

Updated