DEV Community

Cover image for Svelte Share Buttons: using Web Share API with Fallback
Rodney Lab
Rodney Lab

Posted on • Originally published at rodneylab.com

Svelte Share Buttons: using Web Share API with Fallback

🤗 What is the Web Share API?

In this post we see how you can add Svelte share buttons to your Svelte-based app. We will follow a roll-your-own approach here using inbuilt browser APIs, rather than adding extra packages or dependencies. We will work in SvelteKit though our Svelte components can just as easily be used in Slinkity or Astro Svelte apps.

We add buttons, to a blog starter, which let site visitors share blog posts with their friends and followers on WhatsApp, Telegram and Twitter as well as Facebook. That functionality works in any browser, relying on just JavaScript. As well as that, we make use of the relatively new Web Share API. On the visitor’s mobile; this will bring up a menu, making it easy to share your site on any suitable apps they have installed. We are going for a progressive enhancement approach. This means mobile (and Safari on desktop) will show the Web Share API button, while browsers yet to support it will show our share buttons instead.

🧱 What we’re Building

I mentioned we are working in SvelteKit using the MDsveX blog starter. If Astro is your preferred tool for building Svelte sites, start with the Astro Svelte Markdown starter instead. Of course, if you have an existing Svelte app, you can create a feature branch and add this functionality there. Anyway to start let’s clone a repo:

git clone https://github.com/rodneylab/sveltekit-blog-mdx.git sveltekit-share-buttons
cd sveltekit-share-buttons
pnpm install
cp .env.EXAMPLE .env
pnpm run dev
Enter fullscreen mode Exit fullscreen mode

With that done, we will start by adding the fallback share button functionality. We will run through adding Twitter in detail, then sketch over adding other networks. If you want to add a popular network not mentioned, drop a comment below and I will see what I can do. Equally feel free to submit a pull request to the repo if you code up support for a well-known network, not already implemented.

🧩 Initial Share Button Component

To get the ball rolling, let’s create a src/lib/components/ShareButtons folder and inside it add an index.svelte file. This file will ultimately contain the logic for the Web Share progressive enhancement as well as the fallbacks. Progressive enhancement expresses a similar sentiment to graceful degradation. The idea is we want to support a new feature which does not currently enjoy wide support. We have a baseline which supports all (or most) devices, then the progressive enhancement offers the new feature, but only where the user device supports it.

To get going, paste this code into the new file:

<script>
  import Twitter from '$lib/components/ShareButtons/Twitter.svelte';
  import website from '$lib/config/website';

  const { siteUrl } = website;

  export let slug;
  export let title;

  const url = `${siteUrl}/${slug}`;
</script>

<aside aria-label="Share buttons" class="container">
  <div class="wrapper">
    Share: <div class="buttons">
        <Twitter {url} {title} />
    </div>
  </div>
</aside>
Enter fullscreen mode Exit fullscreen mode

Here is some optional styling which you can paste at the end of the file:

<style lang="scss">
  .container {
    display: flex;
    flex-direction: row;
    margin-top: $spacing-12;
    width: $max-width-full;
  }
  .wrapper {
    display: flex;
    flex-direction: row;
    margin-left: auto;
    font-weight: $font-weight-bold;
    font-size: $font-size-2;
  }
  .buttons {
    margin-left: $spacing-4;
  }

  button {
    background: transparent;
    border-style: none;
    transition: all 0.2s ease-in-out;
  }

  @media (prefers-reduced-motion: reduce) {
    button {
      transition: all 2s ease-in-out;
    }
  }

  button:focus,
  button:hover {
    transform: scale(1.1);
  }
</style>
Enter fullscreen mode Exit fullscreen mode

This will not work yet — we need to create the Twitter share button component. Nonetheless, let’s add the ShareButton component to our blog post template now:

<script>
  import BannerImage from '$lib/components/BannerImage.svelte';
  import SEO from '$lib/components/SEO/index.svelte';
  import ShareButtons from '$lib/components/ShareButtons/index.svelte';
  import readingTime from 'reading-time';
Enter fullscreen mode Exit fullscreen mode
<BannerImage {imageData} />
<h1>{title}</h1>
<ShareButtons {slug} {title} />
Enter fullscreen mode Exit fullscreen mode

With that basic wiring up done, we will next create the Twitter share button component. The other components work in a similar way, though the Twitter API supports the most customisation, so it provides a good starting point.

🐦 Twitter Share Button Component

This button will be part of the fallback; as such, we will not show it on devices which support the Web Share API. Currently on MacOS, Safari does support the API, so test this part in Firefox and Chrome. The Twitter code, together with the other share button components are based off the nygardk/react-share GitHub repo by Klaus Nygård.

<script>
  import TwitterIcon from '$lib/components/Icons/Twitter.svelte';

  export let hashtags = []; // array of hashtags exclude '#' e.g. ['svelte', 'askRodney']
  export let quote = undefined;
  export let related = []; // array of Twitter users (including '@')
  export let title; // text in Tweet
  export let url;
  export let via = ''; // include '@' e.g. '@askRodney'

  const TWITTER_BLUE = '#00aced';

  const baseUrl = 'https://twitter.com/share';
  const parametersObject = {
    url,
    ...(hashtags.length > 0 ? { hashtags: hashtags.join(',') } : {}),
    quote,
    text: title,
    ...(related.length > 0 ? { related: related.join(',') } : {}),
    ...(via.length > 0 ? { via: via.slice(1) } : {}),
  };

  const params = Object.entries(parametersObject)
    .filter(([, value]) => value ?? false)
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
    .join('&');

  const urlWithParameters = params === '' ? baseUrl : `${baseUrl}?${params}`;

  function handleClick() {
    const config = {
      height: 550,
      width: 400,
      location: 'no',
      toolbar: 'no',
      status: 'no',
      directories: 'no',
      menubar: 'no',
      scrollbars: 'yes',
      resizable: 'no',
      centerscreen: 'yes',
      chrome: 'yes',
    };
    return window.open(
      urlWithParameters,
      '',
      Object.keys(config)
        .map((key) => `${key}=${config[key]}`)
        .join(','),
    );
  }
</script>

<button on:click={handleClick}
  ><span class="screen-reader-text">Share on Twitter</span><TwitterIcon
    colour={TWITTER_BLUE}
    width={48}
  /></button
>

<style lang="scss">
  button {
    background: transparent;
    border-style: none;
    transition: all 0.2s ease-in-out;
  }
  @media (prefers-reduced-motion: reduce) {
    button {
      transition: all 2s ease-in-out;
    }
  }

  button:focus,
  button:hover {
    transform: scale(1.1);
  }
  @media screen and (max-width: $desktop-breakpoint) {
    button {
      padding-left: $spacing-2;
      padding-right: $spacing-2;
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

In line 2 we import an iconify Twitter icon already used in the project. It is fairly easy to add icons from dozens of libraries using iconify. In fact this post is a follow-up to an earlier post which tells you exactly how to gain access to over 100,000 SVG icons using a single package. So jump back there when you want to see how to add icons to your own project.

The Twitter API has most bells and whistles: you can include hashtags, quotes and related accounts as well as a via Twitter account. I have added some comments in the code to explain how to use these. Here, we’ll just focus on adding the text (title prop) and URL.

Twitter Customisations

Lines 1421 (above) build up the query parameters for the URL which we need to send to Twitter to share the post. Because this is a URL, some characters are not allowed and we have to encode them. In line 25, we make use of encodeURIComponent to URL encode the keys and parameters for us.

The handleClick code in lines 3051 brings up a new window on Twitter’s site which lets the visitor log in (if they are not already logged in) and share. The sizes are sensible defaults which work on different sized devices, so you will probably not need to adjust these.

The other components will be similar, though with differing baseUrl (see line 13). For improved accessibility, include some screen reader text on all buttons (as in line 55). This will not be visible, though screen readers will announce it, helping screen reader users know what the button does.

🔘 Other Network Buttons

We will skim over the other networks now, in fact, we will only point out details which are different. Then in the following section we will add them into the principal component and also wire up the Web Share API.

Facebook

<script>
  import FacebookIcon from '$lib/components/Icons/Facebook.svelte';
  export let hashtag = '';
  export let quote = '';
  export let url;

  const FACEBOOK_BLUE = '#3b5998';

  const baseUrl = 'https://www.facebook.com/sharer/sharer.php';
  const parametersObject = {
    u: url,
    ...(quote !== '' ? { quote } : {}),
    ...(hashtag !== '' ? { hashtag } : {}),
  };

  const params = Object.entries(parametersObject)
    .filter(([, value]) => value ?? false)
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
    .join('&');

  const urlWithParameters = params === '' ? baseUrl : `${baseUrl}?${params}`;

  function handleClick() {
    const config = {
      height: 550,
      width: 400,
      location: 'no',
      toolbar: 'no',
      status: 'no',
      directories: 'no',
      menubar: 'no',
      scrollbars: 'yes',
      resizable: 'no',
      centerscreen: 'yes',
      chrome: 'yes',
    };
    return window.open(
      urlWithParameters,
      '',
      Object.keys(config)
        .map((key) => `${key}=${config[key]}`)
        .join(','),
    );
  }
</script>

<button on:click={handleClick}
  ><span class="screen-reader-text">Share on Facebook</span><FacebookIcon
    colour={FACEBOOK_BLUE}
    width={48}
  /></button
>

<style lang="scss">
  button {
    background: transparent;
    border-style: none;
    transition: all 0.2s ease-in-out;
  }

  @media (prefers-reduced-motion: reduce) {
    button {
      transition: all 2s ease-in-out;
    }
  }

  button:focus,
  button:hover {
    transform: scale(1.1);
  }

  @media screen and (max-width: $desktop-breakpoint) {
    button {
      padding-left: $spacing-2;
      padding-right: $spacing-2;
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

Telegram

<script>
  import TelegramIcon from '$lib/components/Icons/Telegram.svelte';

  export let url;
  export let title;

  const TELEGRAM_BLUE = '#49a9e9';

  const baseUrl = 'https://telegram.me/share/url';
  const parametersObject = {
    url,
    text: title,
  };

  const params = Object.entries(parametersObject)
    .filter(([, value]) => value ?? false)
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
    .join('&');

  const urlWithParameters = params === '' ? baseUrl : `${baseUrl}?${params}`;

  function handleClick() {
    const config = {
      height: 550,
      width: 400,
      location: 'no',
      toolbar: 'no',
      status: 'no',
      directories: 'no',
      menubar: 'no',
      scrollbars: 'yes',
      resizable: 'no',
      centerscreen: 'yes',
      chrome: 'yes',
    };
    return window.open(
      urlWithParameters,
      '',
      Object.keys(config)
        .map((key) => `${key}=${config[key]}`)
        .join(','),
    );
  }
</script>

<button on:click={handleClick}
  ><span class="screen-reader-text">Share on Telegram</span><TelegramIcon
    colour={TELEGRAM_BLUE}
    width={48}
  /></button
>

<style lang="scss">
  @import '../../styles/variables';

  button {
    background: transparent;
    border-style: none;
    transition: all 0.2s ease-in-out;
  }

  @media (prefers-reduced-motion: reduce) {
    button {
      transition: all 2s ease-in-out;
    }
  }

  button:focus,
  button:hover {
    transform: scale(1.1);
  }

  @media screen and (max-width: $desktop-breakpoint) {
    button {
      padding-left: $spacing-2;
      padding-right: $spacing-2;
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

WhatsApp

<script>
  import { browser } from '$app/env';
  import WhatsappIcon from '$lib/components/Icons/Whatsapp.svelte';
  import { isMobileOrTablet } from '$lib/utilities/device';

  const WHATSAPP_GREEN = '#25D366';

  export let url;
  export let title;

  const baseUrl =
    browser && isMobileOrTablet()
      ? 'https://api.whatsapp.com/send'
      : 'https://web.whatsapp.com/send';
  const parametersObject = {
    text: title ? title + ' ' + url : url,
  };

  const params = Object.entries(parametersObject)
    .filter(([, value]) => value ?? false)
    .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)
    .join('&');

  const urlWithParameters = params === '' ? baseUrl : `${baseUrl}?${params}`;

  function handleClick() {
    const config = {
      height: 550,
      width: 400,
      location: 'no',
      toolbar: 'no',
      status: 'no',
      directories: 'no',
      menubar: 'no',
      scrollbars: 'yes',
      resizable: 'no',
      centerscreen: 'yes',
      chrome: 'yes',
    };
    return window.open(
      urlWithParameters,
      '',
      Object.keys(config)
        .map((key) => `${key}=${config[key]}`)
        .join(','),
    );
  }
</script>

<button on:click={handleClick}
  ><span class="screen-reader-text">Share on Whatsapp</span><WhatsappIcon
    colour={WHATSAPP_GREEN}
    width={48}
  /></button
>

<style lang="scss">
  button {
    background: transparent;
    border-style: none;
    transition: all 0.2s ease-in-out;
  }
  @media (prefers-reduced-motion: reduce) {
    button {
      transition: all 2s ease-in-out;
    }
  }

  button:focus,
  button:hover {
    transform: scale(1.1);
  }

  @media screen and (max-width: $desktop-breakpoint) {
    button {
      padding-left: $spacing-2;
      padding-right: $spacing-2;
    }
  }
</style>
Enter fullscreen mode Exit fullscreen mode

There is not already a WhatsApp icon in the starter. Add this code, or create your own using your preferred icon set:

<script>
  import Icon, { addCollection } from '@iconify/svelte/dist/OfflineIcon.svelte';

  export let label = 'Whatsapp icon';
  export let colour = 'inherit';
  export let ariaHidden = false;
  export let width = 48;

  addCollection({
    prefix: 'simple-icons',
    icons: {
      whatsapp: {
        body: '<path fill="currentColor" d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967c-.273-.099-.471-.148-.67.15c-.197.297-.767.966-.94 1.164c-.173.199-.347.223-.644.075c-.297-.15-1.255-.463-2.39-1.475c-.883-.788-1.48-1.761-1.653-2.059c-.173-.297-.018-.458.13-.606c.134-.133.298-.347.446-.52c.149-.174.198-.298.298-.497c.099-.198.05-.371-.025-.52c-.075-.149-.669-1.612-.916-2.207c-.242-.579-.487-.5-.669-.51a12.8 12.8 0 0 0-.57-.01c-.198 0-.52.074-.792.372c-.272.297-1.04 1.016-1.04 2.479c0 1.462 1.065 2.875 1.213 3.074c.149.198 2.096 3.2 5.077 4.487c.709.306 1.262.489 1.694.625c.712.227 1.36.195 1.871.118c.571-.085 1.758-.719 2.006-1.413c.248-.694.248-1.289.173-1.413c-.074-.124-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 0 1-5.031-1.378l-.361-.214l-3.741.982l.998-3.648l-.235-.374a9.86 9.86 0 0 1-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884c2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 0 1 2.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0 0 12.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 0 0 5.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 0 0-3.48-8.413Z"/>',
      },
    },
    width: 24,
    height: 24,
  });

  // https://api.iconify.design/simple-icons.json?icons=whatsapp
</script>

<Icon icon="simple-icons:whatsapp" {ariaHidden} aria-label={label} color={colour} {width} />
Enter fullscreen mode Exit fullscreen mode

Also for WhatsApp, there are different API URLs depending on whether the visitor is using a mobile device or not. We make use of a utility function to check the type of device the user has. Create this file if you added the WhatsApp share button code above to your app:

/**
 * Returns true if the device is thought to be a mobile or tablet
 * @returns {boolean}
 */
export function isMobileOrTablet() {
  return /(android|iphone|ipad|mobile)/i.test(navigator.userAgent);
}
Enter fullscreen mode Exit fullscreen mode

🔨 Pulling it all Together

We are going back to src/lib/components/ShareButtons/index.svelte now to tie the new buttons in. Feel free to omit any networks you will not need on your app here.

<script>
  import Facebook from '$lib/components/ShareButtons/Facebook.svelte';
  import Telegram from '$lib/components/ShareButtons/Telegram.svelte';
  import Twitter from '$lib/components/ShareButtons/Twitter.svelte';
  import Whatsapp from '$lib/components/ShareButtons/Whatsapp.svelte';
  import website from '$lib/config/website';

  const { siteUrl } = website;

  export let slug;
  export let title;

  const url = `${siteUrl}/${slug}`;
</script>

<aside aria-label="Share buttons" class="container">
  <div class="wrapper">
    Share: <div class="buttons">
        <Twitter {url} {title} /><Facebook {url} /><Whatsapp {url} {title} />
        <Telegram {url} {title} />
    </div>
  </div>
</aside>
Enter fullscreen mode Exit fullscreen mode

I think it will be clear what we are doing here, but let me know if I have forgotten to explain something!

🕸 Web Share API

The final missing piece is the Web Share button. We will add a webShareAPISupported boolean variable with a reactive declaration. This needs access to the browser navigator object so will not work when the code is running server-side. SvelteKit provides the browser variable defined in $app/env which we make use of here. If you are working in Astro, check for ssr using this snippet:

// ASTRO ONLY - IGNORE FOR SVELTEKIT
const ssr = import.meta.env.SSR;
Enter fullscreen mode Exit fullscreen mode

Remember when browser in the SvelteKit code is true, ssr will be false (and vice verse).

Anyway, let’s update the file:

<script>
  import { browser } from '$app/env';
  import ShareIcon from '$lib/components/Icons/Share.svelte';
  import Facebook from '$lib/components/ShareButtons/Facebook.svelte';
  import Telegram from '$lib/components/ShareButtons/Telegram.svelte';
  import Twitter from '$lib/components/ShareButtons/Twitter.svelte';
  import Whatsapp from '$lib/components/ShareButtons/Whatsapp.svelte';
  import website from '$lib/config/website';

  const { siteTitle, siteUrl } = website;

  export let slug;
  export let title;

  $: webShareAPISupported = browser && typeof navigator.share !== 'undefined';

  $: handleWebShare;
  const handleWebShare = async () => {
    try {
      navigator.share({
        title,
        text: `Shared from ${siteTitle}`,
        url,
      });
    } catch (error) {
      webShareAPISupported = false;
    }
  };
  const url = `${siteUrl}/${slug}`;
</script>

<aside class="container">
  <div class="wrapper">
    Share: <div class="buttons">
      {#if webShareAPISupported}
        <button on:click={handleWebShare}
          ><span class="screen-reader-text">Share</span><ShareIcon width={48} /></button
        >
      {:else}
        <Twitter {url} {title} /><Facebook {url} /><Whatsapp {url} {title} />
        <Telegram {url} {title} />{/if}
    </div>
  </div>
</aside>
Enter fullscreen mode Exit fullscreen mode

In line 17 we declare the handleWebShare function as reactive, using the dollar syntax. This will let the interface update if, in line 26, the function sets webShareAPISuported to false.

What have we got here?

In lines 3541, we have a guard so if webShareAPISupported is false, the user will see the share buttons we previously defined. On the other hand, if it is true, the user sees the share button which we will add in a moment.

When the use clicks or taps the share button, the handleWebShare function is invoked. This is asynchronous so it is best practice to include try/catch blocks. If for some reason the share fails, there is no issue! Remember we have our fallback, so just show the share buttons for the networks we added initially. Lines 2024 have the core Web Share code.

Sharing via the API

The Web Share API lets users share links, text and files. We focus on sharing text and links here. To share a file, just add an extra files field to the share object. We share both a link and some related text. You can opt to share either one of these on its own and drop the other (some browsers do not yet support text though). You see the API is fairly simple and there is not much more left to explain… the browsers do all the heavy lifting! The final thing worth a mention is the check for support in line 15. To check the latest browser support see MDN Web Share API docs.

Share Icon

Before we can test, we need to put the missing share icon into the project (feel free to swap this out, using your preferred collection):

<script>
  import Icon, { addCollection } from '@iconify/svelte/dist/OfflineIcon.svelte';

  export let label = 'Share icon';
  export let colour = 'inherit';
  export let ariaHidden = false;
  export let width = 24;

  addCollection({
    prefix: 'simple-line-icons',
    icons: {
      share: {
        body: '<path fill="currentColor" d="M864 704c-52.688 0-99.295 25.585-128.431 64.88l-421.36-214.72c3.664-13.455 5.792-27.535 5.792-42.16c0-18.303-3.216-35.807-8.88-52.175l423.76-205.616C763.97 294.016 810.897 320 864.001 320c88.367 0 160-71.649 160-160c0-88.368-71.633-160-160-160S704 71.633 704 160c0 12.431 1.567 24.464 4.24 36.08L278.4 404.657c-29.281-32.273-71.393-52.656-118.4-52.656C71.631 352 0 423.633 0 512c0 88.351 71.631 160 160 160c50.895 0 96.127-23.824 125.423-60.865l423.104 215.632C705.664 838.736 704 851.152 704 864c0 88.368 71.632 160 160 160s160-71.632 160-160s-71.632-160-160-160zm.002-639.999c53.008 0 96 42.992 96 96s-42.992 96-96 96s-96-42.992-96-96s42.992-96 96-96zm-704 544c-53.024 0-96-42.992-96-96s42.976-96 96-96c53.008 0 96 42.992 96 96s-42.992 96-96 96zm704 352c-53.008 0-96-42.992-96-96s42.992-96 96-96s96 42.992 96 96s-42.992 96-96 96z"/>',
      },
    },
    width: 1024,
    height: 1024,
  });

  // https://api.iconify.design/simple-line-icons.json?icons=share
</script>

<Icon icon="simple-line-icons:share" {ariaHidden} aria-label={label} color={colour} {width} />
Enter fullscreen mode Exit fullscreen mode

💯 Svelte Share Buttons: Test

You should now be able to see the share buttons if you open any of the blog posts in a browser. To test on desktop, at the time of writing the WebShare API was not supported on Firefox and Chrome, so these are good for testing the fallback buttons. Safari on desktop does support the Web Share API so is fantastic for testing that button. Also try deploying your app to a staging environment to test a few mobile browsers.

🙌🏽 Svelte Share Buttons: Wrapup

In this post we looked at:

  • how to add share buttons to a Svelte app with progressive enhancement,
  • using the Web Share API to share from a Svelte app,
  • adding fallback share icons with iconify.

I do hope there is at least one thing in this article which you can use in your work or a side project. Also let me know if you feel more explanation of the config is needed.

This post came from a request in a comment on another post, so do drop a comment below if there is something else you would like to see a video or post on.

You can see the full SvelteKit code for this project on the Rodney Lab Git Hub repo. If you run into any issues, you can drop a comment below as well as reach out for a chat on Element. Also Twitter @mention if you have suggestions for improvements or questions.

🙏🏽 Feedback

If you have found this video useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on Twitter, giving me a mention so I can see what you did. Finally be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram. Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Search Engine Optimisation among other topics. Also subscribe to the newsletter to keep up-to-date with our latest projects.

Top comments (2)

Collapse
 
vhs profile image
vhs

Love your material, Rodney. Please keep it coming. 🙏🏼

Collapse
 
askrodney profile image
Rodney Lab

Thanks for the feedback, will do!