Open Library: How I Built SharePanel
For a long time, my share panel was a JavaScript file I dragged from one project to the next. Local, hardcoded, zero config. It did the job : 8 platforms, one layout, no options. But every new project meant starting over, patching code that was never meant to be reused. Classic.
One day I had enough. Not of the feature itself, of the process. I decided to do it right: rebuild from scratch and turn it into something open, configurable, and usable by anyone in any environment. That project is SharePanel, and what I learned building it goes well beyond JavaScript.
The Starting Point: An Honest But Limited File
The old share.js was simple: a self-executing IIFE, a handful of hardcoded SVG icons, eight platforms (WhatsApp, Telegram, Twitter, LinkedIn, Email, SMS, Copy), and a single bottom-sheet layout. Fixed theme, French-only strings, no options, configure it by editing the source directly.
const SharePanel = (() => {
const ICONS = {
whatsapp: { label: 'WhatsApp', color: '#25d366', svg: '...' },
telegram: { label: 'Telegram', color: '#229ed9', svg: '...' },
// 6 more platforms, hardcoded, not configurable
};
const ALL_PLATFORMS = ['native','whatsapp','telegram','twitter','linkedin','mail','sms','copy'];
function init(config = {}) {
const platforms = config.platforms || ALL_PLATFORMS;
_inject(platforms);
_cfg = {
name : config.name || document.title,
url : config.url || location.href,
accent : config.accent || '#00e676',
// no locale, no layout, no shareText
};
}
return { init, open, close, copy };
})();
This wasn't bad code. It was honest code, built for a specific purpose. The problem was it was only built for that.
As soon as a project needed something slightly different, a different accent color, custom share text, or just three platforms instead of eight; you had to edit the file by hand. Across multiple projects, that means five slightly different versions of the same file, no consistency, no history. Unsustainable.
The Real Challenge Was Design, Not Code
When I started the refactor, I thought the hard part would be adding platforms or handling mobile deep links. The real work was elsewhere: making something configurable without making it complex. That's a very fine line, and where most libraries fail in one direction or the other.
Every option is a decision you're making for someone else
Exposing an option means telling the user: you need to think about this. Each parameter added is extra cognitive load. The rule I set from the start: every option must have a sensible default that covers 90% of use cases. Users should never have to configure anything for it to work correctly.
In practice, the minimal setup is three lines, trigger, name, accent, and everything else layers on naturally without breaking anything. Want to filter platforms? Pass an array. Different layout? A string. French UI? One key. Each option is orthogonal to the others.
// ❌ Before: edit the source file directly
const ALL_PLATFORMS = ['whatsapp', 'telegram', 'twitter']; // hardcoded
const ACCENT = '#00e676'; // hardcoded
// ✅ After: everything at init(), source file never changes
SharePanel.init({
trigger : '#btn-share',
name : 'My App',
accent : '#2f7a52',
platforms : ['whatsapp', 'telegram', 'copy'],
layout : 'popup',
locale : 'fr',
shareText : 'Check out {name} → {url}',
});
Think in Formats, Not in Use Cases
The original file only worked as a script tag, it exposed a global SharePanel variable on window, and that was it. Using it in a React or Vue project was painful: no import, no module, just a script tag bolted to the HTML.
Switching to UMD (Universal Module Definition) changed everything. UMD auto-detects the environment and adapts: global variable for script tags, CommonJS export for require(), ESM for import. One file, all environments.
// Script tag, automatic global
<script src="share.js"></script>
<script>SharePanel.init({ trigger: '#btn-share', name: 'My App' });</script>
// ESM native import
import SharePanel from 'lionra-sharepanel';
SharePanel.init({ trigger: '#btn-share', name: 'My App' });
// CommonJS classic require
const SharePanel = require('lionra-sharepanel');
SharePanel.init({ trigger: '#btn-share', name: 'My App' });
// React inside useEffect
import SharePanel from 'lionra-sharepanel';
useEffect(() => {
SharePanel.init({ trigger: '#btn-share', name: 'My App' });
}, []);
Result: SharePanel now works in script tags, React, Vue, Angular, Next.js, Nuxt, Vite, Webpack, CommonJS, ESM, and AMD, zero source changes needed.
But adopting UMD wasn't enough. I had to rethink the module's internal structure to eliminate all side effects. No more globals written directly, no implicit dependency on execution context. The module had to be pure and predictable.
i18n Must Be Designed In From Day One
The old file had its text strings scattered throughout the source , 'Copy', 'Close', 'Link copied to clipboard', all hardcoded in French, buried in dynamically generated HTML. Adding a language after the fact would have meant hunting down every string manually. Painful and error-prone.
Starting fresh, I isolated every string into a centralized locale object from line one. The result is a three-tier system: a default 'en' locale, a built-in 'fr' locale, and the ability to pass a custom object to override only the strings you need.
// Built-in locale: one key
SharePanel.init({ locale: 'fr' });
// Partial override: only redefine what you want
SharePanel.init({
locale: {
copyBtn : 'Copy link',
copiedBtn : '✓ Done!',
toastCopied : '✓ Link copied!',
// The 20+ other strings stay in English by default
}
});
// Full custom locale: every platform, every string
SharePanel.init({
locale: {
whatsapp : { label: 'WhatsApp', sublabel: 'Send via WhatsApp' },
copy : { label: 'Copy', sublabel: 'Copy the link' },
copyBtn : 'Copy',
ariaClose: 'Close the panel',
}
});
The principle of least surprise, applied to localization.
The Single-File Constraint as an Architecture Guide
I had one hard constraint from the start: SharePanel had to be copy-paste-able. Someone who doesn't want npm should be able to download one file, drop it in their project, and use it immediately. No bundler, no dependencies, no build step.
This constraint turned out to be one of the best decisions of the project, not because it simplifies deployment, but because it forces discipline. When everything has to fit in one file with no external dependencies, you can't afford to sprawl. Every feature has to justify itself. Every line of code has to earn its place.
The best architecture decisions I made on this project were imposed by the simplicity constraint, not by theoretical thinking about design patterns.
Mobile Deep Links: More Subtle Than It Looks
The old file just opened share URLs in a new tab. It worked, but on mobile it forced the browser open even when the app was installed. Not ideal for WhatsApp or Telegram.
The solution: try the app's native URI scheme first (whatsapp://, tg://, fb-messenger://), then automatically fall back to the web URL if the app isn't installed after 1.5 seconds. The fallback is silent the user never sees an error.
function _to(platform) {
const appSchemes = {
whatsapp : `whatsapp://send?text=${t}`,
telegram : `tg://msg?text=${t}`,
twitter : `twitter://post?message=${t}`,
};
const webUrls = {
whatsapp : `https://wa.me/?text=${t}`,
telegram : `https://t.me/share/url?url=${u}&text=${t}`,
twitter : `https://twitter.com/intent/tweet?url=${u}&text=${t}`,
};
// 1. Try the native app
window.location.href = appSchemes[platform];
// 2. If the app isn't installed, web fallback after 1.5s
setTimeout(() => {
window.open(webUrls[platform], '_blank', 'noopener');
}, 1500);
}
This works because mobile browsers silently ignore URI schemes they don't recognize while letting JavaScript keep executing. The fallback timer detects the app wasn't opened and redirects to the web. Simple to understand, invisible to the end user exactly the goal.
What SharePanel Became
SharePanel v2 is 19 platforms, 4 layouts (iOS sheet, centered popup, vertical drawer, Android grid), mobile deep links with automatic web fallback, dark/light/auto theme, full i18n, TypeScript types, and compatibility with every modern JavaScript environment all in a single file, ~15 KB gzip, zero dependencies.
Platforms: WhatsApp, Telegram, X/Twitter, Facebook, Messenger, Instagram, LinkedIn, Reddit, Pinterest, Bluesky, Mastodon, Discord, Slack, Teams, Snapchat, Email, SMS, Native Share API, Copy link
Layouts:
| Value | Description |
|---|---|
sheet |
iOS-style bottom drawer horizontally scrollable icons |
popup |
Centered modal 3-column icon grid |
list |
Bottom drawer vertical list with sub-labels and chevrons |
grid |
Android-style bottom drawer 4-column scrollable icon grid |
On sheet, grid, and list, a bouncing arrow + fade gradient signals more content to scroll. Both disappear automatically once the end is reached.
Quick Start
CDN / Script tag:
<button id="btn-share">Share</button>
<script src="https://cdn.jsdelivr.net/npm/lionra-sharepanel/share.js"></script>
<script>
SharePanel.init({
trigger : '#btn-share',
name : 'My App',
accent : '#00e676',
});
</script>
npm:
npm install lionra-sharepanel
import SharePanel from 'lionra-sharepanel';
SharePanel.init({
trigger : '#btn-share',
name : 'My App',
accent : '#00e676',
});
Programmatic control:
SharePanel.open();
SharePanel.close();
SharePanel.copy(); // copies URL to clipboard
All Options at a Glance
| Option | Default | Description |
|---|---|---|
trigger |
'#btn-share' |
CSS selector of the button(s) that open the panel |
name |
document.title |
App/page name shown in the panel header |
url |
location.href |
URL to share |
shareText |
null |
Custom share text use {name} and {url} as placeholders |
accent |
'#00e676' |
Accent color (hex, rgb…) |
accentText |
'#000' |
Text color on top of the accent |
theme |
'auto' |
'dark' · 'light' · 'auto' (follows html[data-theme]) |
favicon |
null |
SVG string or image URL. null = auto-generated initials |
locale |
'en' |
'en', 'fr', or a custom LocaleStrings object |
platforms |
null |
Array to filter/reorder platforms. null = all in default order |
layout |
'sheet' |
'sheet' · 'popup' · 'list' · 'grid'
|
What I Take Away From This
Turning a utility file into an open library is as much a design exercise as a coding one. You have to think about people who don't know the original context, anticipate environments you don't use yourself, and resist the temptation to add features just because they're possible.
The best libraries are the ones you can pick up in 30 seconds and never think about again. That was the goal and the constraint of keeping everything in one file was the best forcing function I could have given myself.
Framework Demo
ESM — Vite, Rollup, Webpack 5+
React / Next.js
Vue 3 / Nuxt
Angular
SharePanel is available on npm (lionra-sharepanel), via jsDelivr and unpkg CDN, and as a direct download on GitHub. Open source, MIT license, built to be used and adapted.
A Lìonra project
Top comments (0)