In Prod

← Back to Posts

Sharing Text

August 22, 2022

The Web Share API allows users to share text and files using the native share dialogs in browsers. While support is decent, at the time of writing, it is still missing from Firefox and Android Web View.

In this article we will look at how we can share text using the Web Share API, while falling back to copying the text to the users clipboard to add some functionality for instances where it isn't supported.

Methods

To start, we need to understand a few different parts of the API:

Navigator.canShare

Before calling the share functions of the API it is recommended to validate the data by calling navigator.canShare. This function takes the same arguments as the navigator.share method and will return true if it passes.

There are multiple fields on the data object that allow sharing of files, as well as providing a title and a URL, however, at the time of writing I found that Mobile Safari would not play nice if URL and title were also provided.

share.ts

_2
const text = "String to share!";
_2
navigator.canShare({text});

Navigator.share

Once we have validated our data can be shared, we can now call navigator.share to call the native share dialog by passing in the same data. The main difference is this function returns a promise, so it should be awaited.

share.ts

_2
const text = "String to share!";
_2
await navigator.share({text});

Error Handling

The share promise will reject if there is an error or user cancels the share dialog. We don't really care about the user aborting, but other errors should still be thrown. To achieve this, we can try/catch to check to see if the error is of type AbortError and ignore the error.

share.ts

_9
try {
_9
const text = "String to share!";
_9
await navigator.share({text});
_9
} catch (e) {
_9
let error = e as Error;
_9
if (!error.toString().includes('AbortError')) {
_9
console.log(error.message);
_9
}
_9
}

Compatibility

As these functions are not implemented in all browsers and need to be used on secure domains (SSL), we need to check that they exist on the navigator object before trying to access them. Checking canShare exists should be enough, as it implies that share also exists.

share.ts

_13
try {
_13
const text = "String to share!";
_13
if (navigator.canShare && navigator.canShare({text})) {
_13
await navigator.share({text});
_13
} else {
_13
console.log("Share not supported");
_13
}
_13
} catch (e) {
_13
let error = e as Error;
_13
if (!error.toString().includes('AbortError')) {
_13
console.log(error.message);
_13
}
_13
}

We now have a working share function, but this will only work for users who have supported browsers.

Though these users are not able to use the Web Share API, we can still support them by providing them another method to fallback on. Copying the text to the users clipboard is a common method that we can use to make sure we can support the most users.

Clipboard API

We can use the Clipboard API to copy our text to the users clipboard on all modern browsers (note that this needs to be called using a user gesture on some browsers). This at least gives the user the ability to paste the content directly into the desired app to share with.

To use the function, we check to see if it exists on the navigator object, and then pass in the string data and await the promise.

share.ts

_4
const text = "String to share!";
_4
if (navigator.clipboard) {
_4
await navigator.clipboard.writeText(text);
_4
}

Legacy Fallback

In the off chance that a user is using a browser that doesn't support one of the above two functions, we can create a legacy function that uses the deprecated execCommand feature. Though not recommended , it is still supported on all modern browsers.

copy.ts

_9
function copyFallback(text: string) {
_9
const element = document.createElement("textarea");
_9
element.value = text;
_9
element.readOnly = true;
_9
document.body.appendChild(element);
_9
element.select();
_9
document.execCommand("copy");
_9
element.remove();
_9
}

Again, this is deprecated and not recommended.

Reusable React Component

With the above knowledge we can now put together a reusable React component that takes a string and either launches the native share dialog, or copies it to the users' clipboard, depending on supported features.

For the button and notification toasts I am using Mantine and for the icon I am using react-icons.

ShareButton.tsx

_57
import { Button } from "@mantine/core";
_57
import { showNotification } from "@mantine/notifications";
_57
import { MdShare } from "react-icons/md";
_57
_57
interface IShareButtonProps {
_57
text: string;
_57
}
_57
_57
export default function ShareButton(props: IShareButtonProps) {
_57
async function share(text: string) {
_57
try {
_57
if (navigator.canShare && navigator.canShare({text})) {
_57
await navigator.share({text});
_57
} else {
_57
if (navigator.clipboard) {
_57
await navigator.clipboard.writeText(text);
_57
} else {
_57
copyFallback(text);
_57
}
_57
showNotification({
_57
title: "Share",
_57
message: "Copied to clipboard"
_57
});
_57
}
_57
} catch (e) {
_57
let error = e as Error;
_57
if (!error.toString().includes('AbortError')) {
_57
showNotification({
_57
title: "Share Failed",
_57
message: "An error occurred during the share process, your browser may not support sharing.",
_57
color: "red"
_57
});
_57
}
_57
}
_57
}
_57
_57
function copyFallback(text: string) {
_57
const element = document.createElement("textarea");
_57
element.value = text;
_57
element.readOnly = true;
_57
document.body.appendChild(element);
_57
element.select();
_57
document.execCommand("copy");
_57
element.remove();
_57
}
_57
_57
return (
_57
<div>
_57
<Button
_57
leftIcon={<MdShare/>}
_57
onClick={() => share(props.text)}
_57
>
_57
{`Share`}
_57
</Button>
_57
</div>
_57
);
_57
}

To use it we can pass the string we want to share to the text prop. This can be seen in action in my Hangman game for sharing results.

Or click the button below to try it out.


Andrew McMahon
These are a few of my insignificant productions
by Andrew McMahon.