エクスクルーシブ
Share Button <mv-share-button>
Web Share API でデータを共有できる環境(navigator.canShare で確認)ではワンタップでネイティブの共有シートを開き、それ以外では Copy link、Email、指定したネットワークを並べた小さなキーボード対応メニューを表示する共有ボタン。各項目は通常のインテントリンクで作られています。
仕組み
Web Share API でデータを共有できる環境(navigator.canShare で確認)ではワンタップでネイティブの共有シートを開き、それ以外では Copy link、Email、指定したネットワークを並べた小さなキーボード対応メニューを表示する共有ボタン。各項目は通常のインテントリンクで作られています。他の実装が見落とす点:利用者が共有先を選ぶまでサードパーティへのリクエストやトラッキングは一切ありません(スクリプト、ピクセル、iframe、ブランドアイコンはなく、リンクは noopener noreferrer で開きます)。共有される URL は絶対パスの canonical URL で、utm_*、fbclid、gclid などのクリック ID は取り除かれます(keep-params で無効化できます)。ネイティブのシートを閉じても何も起こらず、シートがブロックされた場合はメニューにフォールバックします。キャンセル可能な mv-share イベントと mv-shared イベントにより、アプリ自身で共有数を数えられます。
| カテゴリー | マイクロインタラクション |
|---|---|
| タイプ | Web Component(<mv-share-button>) |
| ステータス | 安定版 |
| 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
インストール
node scripts/add.mjs share-button --out ./src/marvelousMarvelous UI の MCP サーバーを使う AI エージェント:install_components({ slugs: ["share-button"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):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。
使い方
クイックスタート、動作する最小限のマークアップ:
<mv-share-button></mv-share-button>基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<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"
share-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;flex-wrap:wrap;align-items:center;gap:.6rem .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 1 9rem;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>
<div style="display:flex;align-items:center;gap:.9rem;margin-inline-start:auto">
<mv-share-button data-variant="icon" url="https://lumen.studio/shop/lacquer-tray?ref=home&gclid=Cj0KCQ" share-title="Kanazawa lacquer tray" networks="facebook reddit"></mv-share-button>
<mv-share-button data-variant="ghost" menu url="https://lumen.studio/invite/7QX2" share-title="Join Lumen Studio" networks="whatsapp telegram">Invite a friend</mv-share-button>
</div>
</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 | タイプ | Description |
|---|---|---|
url | string | URL to share, absolute or relative (resolved against the page). Default: the page’s <link rel="canonical">, else the current URL. |
share-title | string | Title to share (native sheet, email subject, Reddit and Hacker News title). Default: document.title. Named share-title so it never sets the global title attribute (no browser tooltip over the button). |
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 | タイプ | 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.