Skip to content

Clipboard

@symbiote-native/clipboard wraps expo-clipboard so every SymbioteNative adapter can read and write the clipboard. Like sensors and local auth, it’s built on expo-modules-core; unlike local-auth’s pure free functions or sensors’ fully-subscription-based API, clipboard mixes both — most of the surface is stateless one-shot async calls, plus exactly one listener-based subscription, addClipboardListener.

OS platform Support
iOS ✅ live
Android ✅ live
Framework adapter Support
React ✅ live
Vue ✅ live
Angular ✅ live
Svelte ✅ live
Solid ✅ live
Terminal window
npm install @symbiote-native/clipboard

Scaffolding or extending a SymbioteNative app? npx @symbiote-native/cli new --clipboard (or add --clipboard in an existing app) installs and wires this for you — see @symbiote-native/cli.

expo-clipboard and expo-modules-core come along as regular dependencies, pinned to exact versions — never install either yourself, and never add the expo meta-package to your project (it bundles its own Metro/Babel pipeline, which conflicts with this project’s own).

getStringAsync/setStringAsync and the rest of the async functions are already framework-agnostic — import them straight from the package root, on any adapter, with no hook/composable/service in the way:

import { getStringAsync, setStringAsync } from '@symbiote-native/clipboard/react';
export default function CopyPaste() {
const handleCopy = () => {
void setStringAsync('Hello from SymbioteNative');
};
const handlePaste = () => {
void getStringAsync().then(text => console.log('clipboard text', text));
};
return (
<view>
<pressable onPress={handleCopy}>
<text>Copy</text>
</pressable>
<pressable onPress={handlePaste}>
<text>Paste</text>
</pressable>
</view>
);
}

addClipboardListener is the one piece of live state clipboard has — each adapter wraps it in its own mount/unmount lifecycle so you don’t manage the subscription by hand:

import { useClipboard } from '@symbiote-native/clipboard/react';
export default function ClipboardWatcher() {
const clipboardEvent = useClipboard(); // IClipboardEvent | null
return <text>{clipboardEvent && `content types: ${clipboardEvent.contentTypes.join(', ')}`}</text>;
}
Signature Description
getStringAsync(options?: IGetStringOptions): Promise<string> Reads the clipboard’s text content; resolves an empty string if the clipboard is empty or (iOS 16+) paste permission was denied
setStringAsync(text: string, options?: ISetStringOptions): Promise<boolean> Writes a string to the clipboard; resolves true once saved
hasStringAsync(): Promise<boolean> Whether the clipboard has text content, plain or rich (e.g. HTML)
getUrlAsync(): Promise<string | null> Reads the clipboard’s URL content, or null if there is none. @platform ios
setUrlAsync(url: string): Promise<void> Writes a URL to the clipboard, marking its content type as a URL. @platform ios
hasUrlAsync(): Promise<boolean> Whether the clipboard has URL content. @platform ios
getImageAsync(options: IGetImageOptions): Promise<IClipboardImage | null> Reads the clipboard’s image content in the requested format, or null if there is none
setImageAsync(base64Image: string): Promise<void> Writes a base64-encoded image (no MIME prefix) to the clipboard
hasImageAsync(): Promise<boolean> Whether the clipboard has image content
addClipboardListener(listener: (event: IClipboardEvent) => void): EventSubscription Subscribes to clipboard-content changes; call .remove() on the returned subscription to unsubscribe. The primitive useClipboard/ClipboardService.connect() wrap
removeClipboardListener(subscription: EventSubscription): void Deprecated — call subscription.remove() instead
Field Type Default Description
preferredFormat (get) StringFormat StringFormat.PLAIN_TEXT The target format to convert the clipboard string to, if possible
inputFormat (set) StringFormat StringFormat.PLAIN_TEXT The format of the string being written, so other applications can interpret the copied content correctly
Field Type Default Description
format 'png' | 'jpeg' — The format to convert the clipboard image to
jpegQuality number 1 Quality between 0 and 1; only applies when format is 'jpeg'
Field Type Description
data string Base64-encoded image data, already prefixed with data:image/png;base64, or data:image/jpeg;base64, depending on the requested format
size { width: number; height: number } Dimensions of the pasted image
Field Type Description
contentTypes ContentType[] The content types currently available on the clipboard
Enum Members Description
ContentType PLAIN_TEXT, HTML, IMAGE, URL (@platform ios) What kind of data the clipboard currently holds
StringFormat PLAIN_TEXT, HTML The string encoding to read/write clipboard text as
React (/react) Vue (/vue) Angular (/angular) Svelte (/svelte) Solid (/solid) Signature Returns
useClipboard useClipboard ClipboardService.connect() useClipboard createClipboard () — no config, always subscribes React/Vue: live IClipboardEvent | null directly; Angular: Signal<IClipboardEvent | null>; Svelte: { readonly current: IClipboardEvent | null }; Solid: Accessor<IClipboardEvent | null>

Every variant returns null until the clipboard changes at least once after mount — there’s no initial read of whatever’s already on the clipboard, only new changes going forward.

  • On Android the change listener is paused while the app is backgrounded. The native module pauses on activity-background and resumes on activity-foreground, with no catch-up event — a copy made in another app is never delivered, and (combined with the no-initial-read behavior above) useClipboard is still null when the user comes back. Read the clipboard explicitly on resume if you need what happened while you were away.
  • hasStringAsync/hasImageAsync do not trigger Android 12+’s “pasted from clipboard” toast; the getters do. The has* calls inspect only the clip description, while getStringAsync/ getImageAsync read the clip itself. Probe with has*Async when all you need is whether something is there to paste.
  • Android never reports a url content type. The Android module’s own content-type enum has only plain-text, html and image, so IClipboardEvent.contentTypes can carry 'url' on iOS alone — the same split as the iOS-only URL functions.
  • plain-text is reported for HTML-only content on both platforms. iOS marks plain-text available when the pasteboard has strings or HTML; Android’s text check matches both the plain-text and HTML MIME types. Seeing plain-text in an event does not mean the content was copied as plain text — check for html first if the distinction matters.
  • getStringAsync returns an empty string on iOS. On iOS 16+ a denied paste returns '', and it cannot be told apart from an empty clipboard; no error is thrown.
  • How do I avoid the paste prompt? Use ClipboardPasteButton, which wraps UIPasteControl and pastes without asking permission.
  • How do I copy an image? setImageAsync takes a base64 string.

Sources: Expo docs: Clipboard, expo/expo#40189, expo/expo#15335.

@symbiote-native/clipboard ships zero React/Vue/Angular/Svelte/Solid logic in expo-clipboard itself — that package’s own types file hard-imports from the expo meta-package (which this project never installs), so its functions, enums, and option/result types are hand-ported, verbatim, into this package’s own core/, changing only the native-module resolution to go through expo-modules-core’s requireNativeModule instead:

packages/clipboard/src/
├── core/ # framework-agnostic: every exported function, ContentType, StringFormat, the
│ # option/image/event types, and addClipboardListener — native-module.ts resolves
│ # the native module via expo-modules-core's requireNativeModule
├── react/ # @symbiote-native/clipboard/react — re-exports core + hooks/use-clipboard
├── vue/ # @symbiote-native/clipboard/vue — re-exports core + composables/use-clipboard
├── svelte/ # @symbiote-native/clipboard/svelte — re-exports core + runes/use-clipboard
└── angular/ # @symbiote-native/clipboard/angular — re-exports core + services/clipboard.service

The stateless functions are plain re-exports with no lifecycle code, same as local-auth; addClipboardListener itself lives once in core/, and each adapter’s useClipboard/ClipboardService is a thin lifecycle wrapper — subscribe on mount, unsubscribe on unmount — over that same subscription, the same logic/lifecycle split as every other SymbioteNative component (see how it works). The native code itself is never vendored or copied — expo-modules-autolinking resolves it straight out of node_modules (see the native setup guide).

ClipboardPasteButton (the UIPasteControl view, iOS 16+) is ported on every adapter. It is an Expo native view: the core registers it through requireNativeViewManager when it renders, and it paints nothing on Android. Check isPasteButtonAvailable before rendering it, give it a width and a height in style, and style it through backgroundColor, foregroundColor, cornerStyle and displayMode since Apple restricts the control. onPress receives { type: 'text', text } or { type: 'image', data, size }: Vue takes @press, Angular [onPress], the others onPress.