Previous Article
Stripe Payment Integration in PHP: Payment Intents, SCA and Webhooks
19 May 2018
Official share buttons from Facebook, X and LinkedIn all work the same way: you load a third-party JavaScript SDK, it injects an iframe, and that iframe tracks every visitor who sees it — whether or not they click. You can replace the whole arrangement with ordinary anchor tags.
Custom share buttons are faster, do not leak your visitors to anyone, need no cookie consent, and give you complete control over the styling. Here is how to build them properly.
Before any button, get this right. When someone shares your URL, the receiving platform fetches the page and reads its meta tags to build the preview card. The button does not control the image or headline — these tags do:
<meta property="og:title" content="Custom Social Share Buttons Without Third-Party Scripts" /> <meta property="og:description" content="Fast, private share buttons using plain links and the Web Share API." /> <meta property="og:image" content="https://example.com/og/share-buttons.png" /> <meta property="og:url" content="https://example.com/blog/share-buttons" /> <meta property="og:type" content="article" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="Custom Social Share Buttons Without Third-Party Scripts" /> <meta name="twitter:image" content="https://example.com/og/share-buttons.png" />
Use an absolute URL for og:image — relative paths silently fail. Size it 1200×630, and keep it under 5 MB or Facebook will skip it. If you change the image later, the old one stays cached; use Facebook's Sharing Debugger or LinkedIn's Post Inspector to force a refresh.
Every platform accepts a plain GET request. These are the endpoints as they stand in 2026:
<!-- X (formerly Twitter) -->
https://twitter.com/intent/tweet?url={URL}&text={TITLE}
<!-- Facebook -->
https://www.facebook.com/sharer/sharer.php?u={URL}
<!-- LinkedIn -->
https://www.linkedin.com/sharing/share-offsite/?url={URL}
<!-- WhatsApp -->
https://api.whatsapp.com/send?text={TITLE}%20{URL}
<!-- Telegram -->
https://t.me/share/url?url={URL}&text={TITLE}
<!-- Reddit -->
https://www.reddit.com/submit?url={URL}&title={TITLE}
<!-- Email -->
mailto:?subject={TITLE}&body={URL}
Two things worth noting. LinkedIn's old shareArticle?mini=true endpoint is deprecated — share-offsite is the current one, and it ignores any title or summary you pass, taking everything from your Open Graph tags. And Google+ no longer exists; it shut down in April 2019, so remove any plus.google.com/share links still lurking in your templates.
Every value must be URL-encoded. This is where hand-written share links usually break — an unencoded & or ? in your title silently truncates the shared URL.
These are ordinary links. They work with JavaScript disabled, they can be middle-clicked into a new tab, and screen readers announce them correctly:
<div class="share" data-share>
<span class="share__label" id="share-label">Share this article</span>
<ul class="share__list" aria-labelledby="share-label">
<li>
<a class="share__link"
href="https://twitter.com/intent/tweet?url=https%3A%2F%2Fexample.com%2Fpost&text=Custom%20Social%20Share%20Buttons"
target="_blank"
rel="noopener noreferrer">
<svg aria-hidden="true" focusable="false" width="20" height="20"><use href="#icon-x" /></svg>
<span class="sr-only">Share on X</span>
</a>
</li>
<!-- repeat per platform -->
</ul>
<button type="button" class="share__copy" data-copy>Copy link</button>
</div>
The accessibility details matter here. Icon-only links need a text alternative — the visually hidden <span class="sr-only"> provides it, while aria-hidden="true" on the SVG stops the icon being announced separately. rel="noopener" prevents the opened page from accessing your window object, and noreferrer stops leaking the referring URL.
Avoid the old pattern of href="javascript:void(0)" with an inline onclick. It is not a real link, so it breaks keyboard navigation and middle-click, and inline handlers require an unsafe Content Security Policy.
const shareTargets = {
x: (u, t) => `https://twitter.com/intent/tweet?url=${u}&text=${t}`,
facebook: (u) => `https://www.facebook.com/sharer/sharer.php?u=${u}`,
linkedin: (u) => `https://www.linkedin.com/sharing/share-offsite/?url=${u}`,
whatsapp: (u, t) => `https://api.whatsapp.com/send?text=${t}%20${u}`,
reddit: (u, t) => `https://www.reddit.com/submit?url=${u}&title=${t}`,
email: (u, t) => `mailto:?subject=${t}&body=${u}`,
};
function buildShareLinks(root) {
// Prefer the canonical URL — otherwise you share tracking parameters.
const canonical = document.querySelector('link[rel="canonical"]')?.href
?? window.location.href;
const url = encodeURIComponent(canonical);
const title = encodeURIComponent(document.title);
root.querySelectorAll('[data-platform]').forEach((link) => {
const build = shareTargets[link.dataset.platform];
if (build) link.href = build(url, title);
});
}
Reading the canonical URL rather than location.href matters more than it sounds. If a visitor arrived from a campaign link carrying ?utm_source=newsletter, sharing location.href propagates those parameters to everyone who sees the post, corrupting your analytics.
Modern browsers can open the operating system's native share sheet, giving users every app they actually have installed rather than the six you guessed:
const shareBtn = document.querySelector('[data-native-share]');
if (navigator.share) {
shareBtn.hidden = false;
shareBtn.addEventListener('click', async () => {
try {
await navigator.share({
title: document.title,
text: document.querySelector('meta[name="description"]')?.content ?? '',
url: document.querySelector('link[rel="canonical"]')?.href ?? location.href,
});
} catch (err) {
// AbortError simply means the user dismissed the sheet — not a failure.
if (err.name !== 'AbortError') console.error(err);
}
});
}
Two constraints: navigator.share() requires a secure context (HTTPS) and must be called from a genuine user gesture. Support is universal on mobile and now good on desktop Safari and Edge, with Chrome on Windows supported too; Firefox desktop remains the notable gap.
The right pattern is progressive enhancement — show the native button when the API exists, and keep the individual platform links as the fallback. Do not hide the links entirely on mobile; some users prefer a specific target.
Often the most-used control on the row, and trivial to add:
document.querySelector('[data-copy]').addEventListener('click', async (e) => {
const btn = e.currentTarget;
const url = document.querySelector('link[rel="canonical"]')?.href ?? location.href;
try {
await navigator.clipboard.writeText(url);
btn.textContent = 'Copied';
setTimeout(() => { btn.textContent = 'Copy link'; }, 2000);
} catch {
btn.textContent = 'Press Ctrl+C to copy';
}
});
The Clipboard API also needs HTTPS and a user gesture. The catch covers browsers where permission is refused.
The traditional approach intercepts the click and opens a small window.open() popup. It is still reasonable on desktop, but skip it on mobile where popups behave badly, and never make it the only path:
root.addEventListener('click', (e) => {
const link = e.target.closest('.share__link');
if (!link || window.innerWidth < 768) return; // let mobile use the normal link
e.preventDefault();
window.open(link.href, 'share', 'width=600,height=500,noopener');
});
Because the markup is a real anchor, everything still works if this script never runs.
Plain links plus the Web Share API give you sharing that loads instantly, tracks nobody, needs no consent banner, and cannot be broken by an ad blocker. Spend your effort on the Open Graph tags instead — they, not the buttons, determine whether a shared link looks worth clicking.
Advertisement
Written by
Amit VermaFounder / Senior Software Engineer
Amit leads engineering at Being Idea. With 15+ years building scalable software products across global markets, he drives architecture decisions and engineering culture across every engagement.
More articles by AmitYou might also like
We ship software that scales. Let's work together.