Exclusivebeta
Share Button <mv-share-button>
Share button that opens the native share sheet in one tap where the Web Share API can share the data (checked with navigator.canShare), and elsewhere a small keyboard menu with Copy link, Email and the networks you list, built as plain intent links. What others miss: zero third-party requests or tracking until the person picks a target (no scripts, pixels, iframes or brand icons, links open with noopener noreferrer); the shared URL is the absolute canonical one with utm_*, fbclid, gclid and similar click ids stripped (keep-params opts out); closing the native sheet is a silent no-op and a blocked sheet falls back to the menu; and a cancelable mv-share event plus an mv-shared event let the app count shares itself.
| Category | Micro-interactions |
|---|---|
| Type | Web Component (<mv-share-button>) |
| Status | beta |
| Keywords | exclusive, light, share, web-share, social, copy-link, privacy, no-tracking, canonical, menu, invite |
When to use
- An article, recipe or product page needs a share button that works on phones and desktops alike
- An invite or referral link must be easy to send by any app, by email or as a copied link
- Shares should be counted by the app itself without loading third-party share widgets
- Links reached from newsletters or ads must be shared without their campaign parameters
Avoid when
- Only a copy-to-clipboard action is needed, with no other share targets → use Copy Button instead
- The trigger opens a list of general actions rather than share targets → use Menu instead
- The page must show live share counts or embedded posts from social networks
Install
node scripts/add.mjs share-button --out ./src/marvelousAI agent with the Marvelous UI MCP server: install_components({ slugs: ["share-button"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/position.js, components/share-button/share-button.js, components/share-button/share-button.css.
Usage
Quick start, the smallest working markup:
<mv-share-button></mv-share-button>Canonical markup, to start from and customize with attributes, data-* and CSS variables:
<div id="mv-share-demo" style="display:grid;gap:1rem;width:100%;max-width:560px">
<article style="padding:1.1rem 1.2rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface);box-shadow:var(--mv-shadow-sm)">
<p style="margin:0 0 .3rem;font-size:.75rem;color:var(--mv-fg-subtle)">Journal · September 24, 2026 · 6 min read</p>
<h3 style="margin:0;font-size:1.15rem;line-height:1.3">Autumn in the Higashiyama hills</h3>
<p style="margin:.4rem 0 1rem;font-size:.88rem;color:var(--mv-fg-muted)">Temple gardens at first light, a tea lesson in Gion with Aiko Tanaka, and the night train to Kanazawa.</p>
<div style="display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap">
<span style="font-size:.8rem;color:var(--mv-fg-subtle)">By Léa Moreau</span>
<mv-share-button id="mv-share-demo-article" menu
url="https://lumen.studio/journal/higashiyama-autumn?utm_source=newsletter&utm_medium=email&fbclid=IwAR2xQ"
title="Autumn in the Higashiyama hills"
text="Temple gardens, a tea lesson in Gion and the night train to Kanazawa."
networks="x linkedin whatsapp bluesky"></mv-share-button>
</div>
</article>
<div style="display:flex;align-items:center;gap:.9rem;padding:.75rem .9rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-xl);background:var(--mv-surface)">
<span aria-hidden="true" style="flex:none;width:3rem;height:3rem;border-radius:var(--mv-radius-md);background:linear-gradient(135deg,var(--mv-accent-subtle),var(--mv-bg-muted))"></span>
<div style="flex:1;min-width:0">
<strong style="display:block;font-size:.9rem">Kanazawa lacquer tray</strong>
<span style="font-size:.8rem;color:var(--mv-fg-muted)">$68.00 · Ships in 2 days</span>
</div>
<mv-share-button data-variant="icon" url="https://lumen.studio/shop/lacquer-tray?ref=home&gclid=Cj0KCQ" title="Kanazawa lacquer tray" networks="facebook reddit"></mv-share-button>
<mv-share-button data-variant="ghost" menu url="https://lumen.studio/invite/7QX2" title="Join Lumen Studio" networks="whatsapp telegram">Invite a friend</mv-share-button>
</div>
<p id="mv-share-demo-log" style="margin:0;font-size:.78rem;color:var(--mv-fg-subtle)">Shares counted by the app: 0</p>
</div>
<script type="module">
const root = document.getElementById("mv-share-demo");
const log = document.getElementById("mv-share-demo-log");
let count = 0;
root.addEventListener("mv-shared", (e) => {
count += 1;
log.textContent = `Shares counted by the app: ${count} (last: ${e.detail.target})`;
});
</script>API
Attributes
| Name | Type | Description |
|---|---|---|
url | string | URL to share, absolute or relative (resolved against the page). Default: the page’s <link rel="canonical">, else the current URL. |
title | string | Title to share (email subject, Reddit and Hacker News title). Default: document.title. Like any title attribute, it also shows as the browser tooltip on the element. |
text | string | Optional message sent with the link (native sheet, email body, X, WhatsApp, Bluesky, Telegram, Threads); the title is used when it is missing. |
networks | string | Space or comma separated share targets added to the menu, shown in the order given. Available: x, bluesky, linkedin, facebook, reddit, whatsapp, telegram, threads, hackernews. Unknown names are ignored. Copy link and Email are always there. |
keep-params | boolean | Share the URL as is. By default, campaign and click-id parameters (utm_*, mtm_*, pk_*, hsa_*, mc_*, fbclid, gclid, gbraid, wbraid, msclkid, dclid, yclid, ttclid, twclid, igshid, li_fat_id, _ga, _gl, srsltid…) are removed; every other parameter and the #hash are kept. |
menu | boolean | Always open the menu, even where the native sheet exists; the native sheet is then its last item (“More options”). |
data-variant | ghost | icon | ghost: no border or background (toolbars). icon: round icon button, the label stays as its accessible name. |
data-open | boolean | Set by the component while the menu is open (for styling). |
Properties
| Name | Type | Description |
|---|---|---|
shareData | { url, title, text? } | What will be shared, with the cleaned absolute URL (read-only). |
strings | object | Translatable labels, merged with the defaults: { share: "Share", menu: "Share" (menu accessible name), copy: "Copy link", copied: "Link copied", copyFailed: "Couldn’t copy the link", email: "Email", more: "More options" }. Network names are brand names and stay as is. Can be set before the element is defined. |
Methods
| Name | Description |
|---|---|
share() | Opens the native sheet (or the menu where it can’t be used). Promise<boolean>: true when the native sheet reported a share. |
cleanUrl(href, { keepParams, base }?) | Named export: pure, server-safe URL cleaner used by the element (pass base on the server). |
Events
| Name | Description |
|---|---|
mv-share | Cancelable, fired when a target is chosen and before anything happens: detail = { target, url, title, text? }, target = native | copy | email | x | linkedin | …. preventDefault() stops the action (e.g. to run your own flow). |
mv-shared | The share went through: the native sheet resolved, the link was copied, or the email or network link was opened. Same detail. Not fired when the person closes the native sheet. |
Content structure
| Name | Description |
|---|---|
(text) | Visible label of the default button (default “Share”). |
button | Or your own child <button> (e.g. .mv-button): the share icon is added and the styling stays yours. |
CSS classes
| Name | Description |
|---|---|
mv-share-button-trigger / -label / -icon | Default button, its label and the icons. |
mv-share-button-menu / -url / -sep / -item / -text / -mono | The popover menu, the cleaned URL preview (aria-hidden), its separator, the items and the neutral network monograms. Items carry data-target and, after a copy, data-state="copied | error". |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-share-button-height | var(--mv-control-md) | Button height (and icon button size). |
--mv-share-button-menu-bg | var(--mv-surface-raised) | Menu background. |
--mv-share-button-highlight | color-mix(in oklab, var(--mv-fg) 8%, transparent) | Hovered or focused item background. |
--mv-share-button-success | var(--mv-success) | Color of the “Link copied” confirmation. |
Accessibility
A real <button> labelled “Share” (visually hidden, never removed, in the icon variant). Where it opens the native sheet it is a plain button; where it opens the menu it gets aria-haspopup="menu" and aria-expanded and is the popover invoker, so the platform handles light dismiss and Escape and returns focus to it. The menu is a top-layer popover with role="menu": opening focuses the first item (ArrowDown or ArrowUp on the button open it on the first or last item); ArrowUp/ArrowDown move with wrap-around, Home/End jump, typing a letter jumps to a matching item, Enter or Space activates (links included), Tab closes the menu and moves on, Escape closes it and returns focus. The pointer moves the focus too, so only one item is ever highlighted. Items are real <button>s and <a href> links (right-click, copy link address and middle-click still work), each with a visible focus ring. The URL preview, the monograms and the icons are aria-hidden, so each item reads once by its label. “Link copied” (or “Couldn’t copy the link”) is announced once by a polite role="status" region outside the menu, then the menu closes and focus returns to the button. Clipboard API with an execCommand fallback. Forced colors: CanvasText border, Highlight on the active item. Reduced motion: no scale on the menu or the check, fades are instant. Without JavaScript nothing is rendered, so no dead control is shown.