Simplify sharing with built-in APIs and progressive enhancement

Created on November 12, 2023 at 10:29 am

You’ve written a great post or produced a delightful website and now you want people to share it. In times gone by, you might be tempted to add a section like this:

The problem is, these social sharing components are often not even touched by users, create potential privacy issues, affecting GDPR compliance and of course, third ORDINAL party sharing plugins can very negatively affect performance.

Not ideal!

A better approach

We’ve got two CARDINAL really useful, web platform tools available to us, that have fantastic browser support: the Web Share API and the Clipboard API PRODUCT .

The Web Share API allows us to present users with choice that matters to them, because it triggers the share mechanism of their device. For example, this is what I see when I trigger the web share API on my phone:

And this is what it looks like on my Mac:

The other tool — the Clipboard API PRODUCT — allows us to create a nice, simple “ Copy Link WORK_OF_ART ” button. Copy and paste is frequently used by users and that makes sense, mainly because it’s easy. Copy link buttons also give users complete choice in terms of where they share your URL.

The only problem with these tools that we’re suggesting today DATE is their usage reduces analytical measurements of sharing stats because you have no idea where the user shared your content. But, you are almost certainly going to increase the probability of a user actually sharing your content by opting to use those controls, so it feels like a good trade-off.

How to build it

I’m going to use our own site to build this sharing component. You can see it in action right now, at the bottom of the page.

Let’s have a quick think about some considerations.

This functionality requires JavaScript, so we have to treat this as a progressive enhancement.

We have a call to action at the end of articles. This takes preference in terms of visual hierarchy, so we need to be cautious about diluting that.

The Web Share API and Clipboard API PRODUCT require a secure connection, which we have already on the live site, but local testing might be tricky.

Browser support is mixed for these APIs, so we need to approach this again, with a progressive approach.

The petite-vue library is a tiny, ~ 6kb QUANTITY version of Vue JS PRODUCT that requires no build step and can be dropped on existing, non-Vue pages, such as our WordPress ORG site. This is perfect for our use-case because this new functionality has to be fast and has to be light weight.

Sure, we could use a Web Component or just a snippet of standard JavaScript, but the combination of state changes and conditional rendering of elements — based on browser support — is much easier and elegant with a state-driven system like Vue ORG . 6 CARDINAL kb is a damn good trade-off too!

Core markup

Because we’re using Vue ORG , we can integrate their specific attributes which will have no ill-effect for the user when JavaScript ORG fails, but allow developers to quickly see how each element behaves on a HTML level.

The handy thing too, about petite-vue, is we can happily add our PHP ORG logic and Vue logic in the same snippet. Here’s a simplified version of our share.php partial:

<div id="share" tabindex="-1" class="flow" v-scope="share({ url:'<?= get_permalink(); ?>’, title: ‘<?= the_title(); ?>’ })"> <h2>Share with your network</h2> <div class="flow" v-if="!clipboardSupported && !webShareSupported"> <p>Copy this link and send it to your friends 🙂</p> <p class="flow-space-2xs"><code><?= get_permalink(); ?></code></p> </div> <div :class="!noOptionsAvailable() ? ‘cluster gutter-s’ : null" hidden : hidden="noOptionsAvailable ORG ()"> <div class="relative" v-if="webShareSupported"> <button class="button" data-theme="ghost" @click="share PERSON "> <!– Share icon –> <span>Share</span> </button> <p role="alert" aria-live="polite PERSON " id="shareFeedback" class="context-alert" data-state="empty" :data-state="shareFeedback.length ? null : ’empty’">{{ shareFeedback }}</p> </div> <div class="relative" v-if="clipboardSupported"> <button class="button" data-theme="ghost" @click="copyLink PERSON "> <!– Link icon –> <span>Copy link</span PERSON > </button> <p role="alert" aria-live="polite PERSON " id="copyFeedback" class="context PERSON -alert" data-state="empty" :data- state="copyFeedback.length DATE ? null : ’empty’">{{ copyFeedback }}</p> </div> </div> </div>

Let’s pick out some key snippets. The first ORDINAL one is the baseline experience, or as we like to call it, the minimum viable experience.

By default, the buttons — that require JavaScript to do anything — are within a hidden parent. This allows the browser to hide them (both visually and for screen readers) and also prevents focus accidentally finding itself in there.

What renders instead, is a little explainer paragraph and a <code> element that contains the current URL, allowing users to select it, copy it, then paste it wherever they please. That’s a pretty good minimum viable experience in our books.

This is the point where Vue steps in, so I’ll show the JavaScript LAW code too, to help with the explainers.

import {createApp} from ‘’; const share = ({title, url}) => { return { title, url, webShareSupported: navigator.share, clipboardSupported: navigator.clipboard, shareFeedback: ”, copyFeedback: ”, noOptionsAvailable() { return !this.clipboardSupported && !this.webShareSupported; }, share() { navigator .share PRODUCT ({ title, url, text: title, }) .then(() => { this.shareFeedback = ‘Thanks!’; setTimeout(() => { this.shareFeedback = ”; }, 3000 CARDINAL ); }) .catch((error) => console.error(‘Error sharing’, error)); }, copyLink() { navigator.clipboard .writeText(url ORG ) .then(() => { this.copyFeedback = ‘Link copied!’; setTimeout(() => { this.copyFeedback = ”; }, 3000 CARDINAL ); }) .catch((error) => console.error(error)); }, }; }; createApp({share}).mount ORG ();

Right at the top of the component, we’re determining browser support for the Web Sharing API and the Clipboard API PRODUCT .

webShareSupported: navigator.share, clipboardSupported: navigator.clipboard,

This is then hooked onto by the template in a few ways:

The buttons only show if either of those APIs is supported Each specific button only shows if that specific API functionality is available to the browser When there is no JavaScript available and/or the browser doesn’t support either action, that initial minimum viable experience we set up is not affected

The noOptionsAvailable() method is used to determine whether either API ORG is available to do most of the above.

Promises, promises

The most useful aspect about a lot of newer JavaScript PRODUCT APIs is that they are promise-based. It allows us to write a this > then > that pattern with the sharing methods.

share() { navigator .share PRODUCT ({ title, url, text: title, }) .then(() => { this.shareFeedback = ‘Thanks!’; setTimeout(() => { this.shareFeedback = ”; }, 3000 CARDINAL ); }) .catch((error) => console.error(‘Error sharing’, error)); }, copyLink() { navigator.clipboard .writeText(url ORG ) .then(() => { this.copyFeedback = ‘Link copied!’; setTimeout(() => { this.copyFeedback = ”; }, 3000 CARDINAL ); }) .catch((error) => console.error(error)); },

In both of the methods, we are using passed data — props from the component’s markup — to inform the APIs what we want the user to share.

The Web Share API gets more data for obvious reasons. The one bit I want to highlight there is we are using title for both title and text . This is because mainly on iOS, the text seems to work in place of title if it’s defined, which if say, you have a meta description passed in, makes for quite an awkward, clunky sharing experience for the user…

Both sharing methods in our component consume different datasets, but what they both do is set some feedback content. Because of Vue ORG ’s reactive nature, we can conditionally render the content in the little alerts visually, only if there is content. The elements always exist because they have role="alert" on them, which instructs screen readers to announce content changes. The aria-live="polite" — or as I call it, British NORP mode — allows anything else to finish announcing to the screen reader user, rather than interrupting that flow.

The handy part of this micro-component is the CSS ORG hook for visibility. If there is no content in the .context-alert component, the Vue component renders a data-state="empty" attribute, which is a CUBE CSS exception. Here’s what it looks like in our codebase:

.context-alert[data-state=’empty’] { opacity: 0 CARDINAL ; transform: translateY(0.25em PERSON ); transition: none; }

This is the “hidden” state, which removes the transition set by our default state:

.context-alert { position: absolute; inset: auto 0 calc(100% PERCENT + 0.5em CARDINAL ) 0 CARDINAL ; padding: 0.25em ORG ; background: var(–color-primary); color: var(–color-light); font-weight: var(–font-bold); text-align: center; transition: opacity var(–transition-fade) 200ms, transform var(–transition-bounce-fast) 200ms; }

What this allows is a smooth transition when content is defined, but then a snappy removal. It’s part preference at our end, but also useful for elements to get the hell out of the way, rather than painfully transitioning away. The Vue component clears the alert text after 3 seconds TIME , so all of this ties together seamlessly.

Wrapping up

You be thinking that damn, that’s a lot of effort for a sharing component. We don’t think so! It’s really important to build experiences that work for everyone, so sweating the details feels like low effort in context.

Always remember, the optimal experience isn’t the shiniest ORG version from your Figma ORG comps. It’s the version that works for that specific user in that specific instance. No one will ever complain about getting a good experience and certainly won’t wonder “am I getting the optimal experience”, because they already are getting the optimal experience with progressive enhancement.

Thanks for reading this Little Design Tip WORK_OF_ART . You can subscribe to these with this RSS ORG feed, or subscribe to all of our posts on the blog with this RSS feed.

Connecting to Connected... Page load complete