navigator.shareで共有ボタンを作る:未対応ブラウザはコピーに逃がす
共有ボタンが半分の端末で無反応だった僕の失敗から、navigator.shareの使い方、HTTPSとタップ必須の制約、未対応時のコピー退避、ファイル共有まで実装で解説します。
「共有ボタン付けといて」と頼んで、できあがったボタンを自分のPCで押したら、何も起きませんでした。
エラーも出ない。クリックした感触すらない。バグだと思って小一時間ログを眺めて、ようやく気づきました。僕のデスクトップChromeでは、そもそもその機能が存在していなかったんです。スマホで開いたら、ちゃんとOSの共有シートが出てきました。
これがWeb Share APIの一番厄介なところです。動く端末では魔法みたいに便利で、動かない端末では「無」になる。 しかも無反応なので、壊れていることに気づきにくい。今日は、このボタンを「半分の端末で死んでいる」状態から、全部の端末で何かしら反応する状態まで持っていきます。
この記事の要点
navigator.share()はOS標準の共有シートを開くAPI。スマホでは強いが、デスクトップChromeなど未対応環境が普通にある。- 呼び出しには制約が2つ。HTTPS(またはlocalhost)必須と、クリックやタップなどユーザー操作の直後でないと動かないこと。
- 未対応・失敗時は、同じボタンからクリップボードへコピーに逃がすのが定石。ボタンを消してはいけない。
- ファイル共有は
navigator.canShare({ files })で事前確認してから渡す。いきなり渡すと例外で落ちる。 - ユーザーがシートを閉じただけの
AbortErrorは、エラー扱いしない。赤いトーストを出すと混乱する。
そもそもnavigator.shareって何をするのか
スマホで何かを「共有」したとき、LINE・メール・メモ・Slack・AirDropがずらっと並ぶシートが下から出てきますよね。あれを、Webページから直接呼び出すのがWeb Share APIです。中心になるのが navigator.share() という1つの関数。
何がうれしいかというと、共有先のアプリを自分で並べなくていい点です。
昔ながらのやり方だと、X・Facebook・LINE…とSNSのアイコンを自分で1個ずつ置いていました。でもこれだと、ユーザーが本当に使いたいアプリ(個人のメモ帳とか、社内のSlackとか)はだいたい漏れます。navigator.share() を使えば、その人の端末に入っているアプリがそのまま選択肢になる。アイコンを並べる仕事をOSに丸投げできるわけです。
渡せるデータはシンプルで、こんな形のオブジェクトを1つ渡すだけです。
navigator.share({
title: "記事のタイトル",
text: "ひとこと紹介文",
url: "https://example.com/article",
});
title・text・url の3つが基本。url だけ、text だけ、という渡し方もできます。受け取ったアプリがどう表示するかはアプリ次第なので、こちらは「素材を渡すだけ」と考えておくと気が楽です。詳しい引数はMDNのNavigator.share()が一次情報として正確です。
動く環境・動かない環境を最初に把握する
一番大事なのは、Web Share APIはどこでも動く機能ではないと腹をくくることです。僕がハマったように、デスクトップChromeでは未対応(執筆時点)。Safariやモバイル系では概ね使えます。だから「あるかどうか」を毎回チェックしてから呼ぶのが鉄則です。
判定はこれだけ。navigator.share が関数として存在するかを見ます。
if (typeof navigator.share === "function") {
// 共有シートを開ける環境
}
加えて、呼び出しには制約が2つあります。これを知らないと「ローカルでは動いたのに本番で動かない」「ボタンは出てるのに押すと例外」みたいな事故が起きます。MDNでもWeb Share APIの利用条件として明記されています。
| 制約 | 中身 | やりがちな事故 |
|---|---|---|
| secure context | HTTPS、または localhost でしか動かない | http:// の検証環境やIP直打ちで無反応になる |
| ユーザー操作必須 | クリック/タップなどの直後でないと拒否される | 画面表示と同時に自動で共有を開こうとして失敗 |
| データの妥当性 | 渡す url の形やファイル対応が条件を満たす必要 | 不正なURLやサポート外ファイルで例外 |
| Permissions Policy | iframe内は親が web-share を許可していないと不可 | 埋め込み環境で静かに動かない |
window.isSecureContext を見れば、今いるページがsecure contextかどうかを true / false で確認できます。開発中は localhost で検証して、本番URLでだけコピーUIに退避させる、という切り分けにも使えます。
コピペで動く:共有→ダメならコピーの完全版
ここが本体です。普通のHTMLでもAstroでも静的サイトでもPWAでも、そのまま動く最小構成を載せます。考え方は3段構え。
navigator.shareがあれば、それで共有シートを開く。- ユーザーがキャンセル(
AbortError)したら、何もなかったように戻す。 - 未対応・失敗なら、同じボタンからクリップボードにコピーする。
まずボタンと、状態を伝える小さなテキスト。aria-live を付けておくと、スクリーンリーダーが状態変化を読み上げてくれます。
<button data-share type="button">この記事を共有</button>
<p data-share-status aria-live="polite"></p>
次にスクリプト。コメントを読めば流れが追えるようにしてあります。
// 共有する素材。ページごとに title / text / url を差し替える
const payload = {
title: document.title,
text: "navigator.shareで共有ボタンを作る実装メモです。",
url: window.location.href,
};
const button = document.querySelector("[data-share]");
const statusEl = document.querySelector("[data-share-status]");
// 状態テキストを書き換えるだけの小さな関数
function setStatus(message) {
if (statusEl) statusEl.textContent = message;
}
// 共有先アプリに渡す1枚のテキスト(コピー退避で使う)
function buildText(data) {
return [data.title, data.text, data.url].filter(Boolean).join("\n\n");
}
// フォールバック:クリップボードへコピー。それも無理なら手動コピーへ
async function copyFallback(data) {
const text = buildText(data);
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(text);
setStatus("リンクをコピーしました。チャットやSNSに貼り付けてください。");
return;
}
window.prompt("このテキストをコピーしてください", text);
setStatus("表示されたテキストをコピーして共有してください。");
}
button?.addEventListener("click", async () => {
// url は絶対URLに正規化しておくと、共有先で迷子にならない
const data = { ...payload, url: new URL(payload.url, location.href).href };
// 1. 対応環境なら共有シートを開く
if (typeof navigator.share === "function") {
try {
await navigator.share(data);
setStatus("共有シートを開きました。");
return;
} catch (error) {
// 2. ユーザーが閉じただけ。失敗ではないので静かに戻す
if (error.name === "AbortError") {
setStatus("");
return;
}
// それ以外の例外はコピー退避へ流す
console.warn("navigator.share に失敗。コピーへ退避します。", error);
}
}
// 3. 未対応 or 失敗 → コピー
try {
await copyFallback(data);
} catch (error) {
console.error("コピーにも失敗しました。", error);
setStatus("コピーに失敗しました。アドレスバーからURLをコピーしてください。");
}
});
ポイントは1つだけ覚えてください。navigator.share が無い環境でも、同じボタンが「コピー」として生き残ること。ボタンを条件分岐で消してしまうと、ユーザーは「この機能は無い」と判断して二度と探しません。残しておけば、ブラウザ差を吸収しつつ、押した人を必ず次の行動に送り出せます。
React や Vue に移すときも、この click ハンドラの中身はそのまま使えます。statusEl.textContent を setState に置き換えるだけです。クリップボード側の権限やテストまで詰めたい人は、別記事のClipboard APIの実装に、コピー失敗時の挙動やPlaywrightでの検証をまとめてあります。
ファイルを共有するなら canShare を先に呼ぶ
URLやテキストだけでなく、画像やPDFを共有したい場面もあります。生成した図、領収書PDF、スクショ。これらは files 配列に File オブジェクトを入れて渡します。
ただし、ファイル共有はテキスト共有より対応が狭い。だからいきなり navigator.share({ files }) を呼ぶと、未対応環境で例外を吐いて落ちます。先に navigator.canShare() で「この素材、共有できる?」と聞いてから渡すのが安全です。
async function shareFile(file) {
const data = { files: [file], title: "資料", text: "PDFを共有します" };
// canShare で「このファイルを共有できるか」を事前確認する
if (navigator.canShare && navigator.canShare(data)) {
try {
await navigator.share(data);
return "shared";
} catch (error) {
if (error.name === "AbortError") return "cancelled";
console.warn("ファイル共有に失敗。URL共有へ切替。", error);
}
}
// 未対応 or 失敗 → ダウンロードURLの共有やコピーに逃がす
return "fallback";
}
canShare は true / false を返すだけの軽い関数で、これ自体はシートを開きません。判定専用です。未対応だったら、ファイルを無理に渡すより、アップロード済みのダウンロードURLを共有したほうが失敗率はぐっと下がります。
PWAでは「共有する側」と「共有される側」の両方がある
ここはWeb Share APIの一番面白いところです。
navigator.share() は、自分のサイトから外へ共有する側の話でした。一方でPWAには、他のアプリから**共有されてくる側(共有ターゲット)**になる仕組みもあります。スマホで写真を共有しようとしたとき、共有先の一覧に自作PWAが並ぶ、あれです。
仕組みは、PWAの manifest.json に share_target を書いておくだけ。OSが「このPWAは共有を受け取れる」と認識して、共有シートの選択肢に入れてくれます。
{
"share_target": {
"action": "/share-receiver",
"method": "GET",
"params": {
"title": "title",
"text": "text",
"url": "url"
}
}
}
こう書くと、他アプリから共有されたデータが /share-receiver?title=...&text=...&url=... のようにクエリ付きで飛んできます。あとは受け取りページでそれを読んで保存なり表示なりすればいい。PWAのインストールやservice worker側の設計はPWA化の記事に手順をまとめてあるので、共有ターゲットまで作り込みたい人はあわせて読んでください。
PWAで共有ボタンが効いてくる理由はもう1つあります。ホーム画面から起動するとブラウザのアドレスバーや共有メニューが消えるので、ページ内に置いた共有ボタンが、外部に持ち出す唯一の導線になるんです。タブで見るときより、ボタンの重要度が上がります。
僕がやらかした失敗3つ
正直に書きます。最初の共有ボタンは穴だらけでした。
ひとつ目は、冒頭のデスクトップで無反応事件。navigator.share の存在チェックをサボって、いきなり呼んでいました。スマホでテストしていたので気づかず、未対応のPCで「押しても何も起きないボタン」を本番に出してしまった。存在チェックとコピー退避を入れた今は、どの端末でも必ず何か反応します。
ふたつ目は、AbortError を赤いトーストで出したこと。ユーザーが共有シートをスッと閉じただけなのに、「共有に失敗しました」と毎回エラーが出る。閉じる=失敗じゃないんですよね。AbortError だけは握りつぶして何も出さない、に変えたら、変な警告が消えました。
みっつ目は、ページ表示と同時に共有を開こうとしたこと。「読み終わったらシェアしてほしい」と欲張って、onload で navigator.share() を呼んだら、ユーザー操作が無いと拒否されて NotAllowedError。Web Share APIはクリックやタップの直後でないと動きません。当たり前なんですが、自動で開きたくなる誘惑は地味に強い。今は必ずボタンの click から呼んでいます。
よくある質問
Q. navigator.share がデスクトップで動きません。バグですか?
A. バグではなく仕様です。執筆時点でデスクトップChromeなどは未対応で、navigator.share 自体が存在しません。typeof navigator.share === "function" で分岐して、未対応ならコピーに逃がしてください。
Q. ローカルでは動くのに本番で動きません。
A. secure context(HTTPSまたはlocalhost)の制約が原因のことが多いです。http:// のステージングやIP直打ちでは動きません。window.isSecureContext で確認できます。
Q. 誰がどのアプリに共有したか、計測できますか? A. できません。Web Share APIは共有先アプリをサイトに返しません。代わりに「ボタン押下」「共有シートを開けた」「キャンセル」「コピー退避」の4種類を分けて記録すると改善に使えます。イベント設計はアナリティクス実装に寄せると後で読みやすいです。
Q. AbortError と NotAllowedError は何が違いますか?
A. AbortError はユーザーがシートを閉じただけ(失敗扱いしない)。NotAllowedError はユーザー操作なしで呼んだなどの拒否です。後者はクリックの直後に呼べているか実装を見直します。
Q. 画像やPDFを共有したいです。
A. files 配列に File を入れて渡しますが、必ず navigator.canShare({ files }) で対応を確認してから呼んでください。未対応ならダウンロードURLの共有に切り替えるのが安全です。
実際に試した結果
いくつかの端末で配ってみて分かったのは、Web Share APIの価値は「シートが開くこと」より「開かない端末で読者を止めないこと」にあるという事実でした。MDNの利用条件どおり、対応・未対応・キャンセルがきれいに分かれるので、navigator.share の存在チェック → 成功 → AbortError は無視 → それ以外はコピー、の順で組むだけで、無反応ボタンは消えます。
Claude Codeに頼むときも、「共有ボタンを作って」ではなく「未対応ブラウザでもコピーに退避して、AbortError はエラー扱いしない共有ボタンを作って」と条件まで書くと、本番で使える形が一発で返ってきます。共有まわりの設計を自分のチームで一気に固めたい人は、研修・相談でPWA・計測・フォールバックをまとめて設計できます。素材を渡すのはOSの仕事、こちらの仕事は「どの端末でも次の一手を用意すること」。そう割り切ると、共有ボタンはぐっと作りやすくなります。
無料PDF: Claude Code はじめてのチートシート
まずは無料PDFで基本コマンドと最初の使い方をまとめて確認してください。登録後はそのままテンプレート集や導入相談にも進めます。
スパムは送りません。登録情報は厳重に管理します。
Claude Codeを仕事で使える形にしませんか?
まず無料PDFで基本を固め、繰り返し使う作業はGumroad教材へ、チーム導入や権限設計は導入相談へ進めます。
この記事を書いた人
Masa
Claude Codeの実務活用、導入設計、収益導線改善を検証しているエンジニア。10言語の技術メディアを運営中。
関連書籍・参考図書
この記事のテーマに関連する書籍を楽天ブックスで探せます。
※ 当サイトは楽天市場のアフィリエイトプログラムに参加しています。上記リンクから商品をご購入いただくと、運営者に紹介料が支払われる場合があります。
関連記事
Claude Codeに1ファイルだけ直させる指示文のつくり方
「もっと良くして」で40行も変えられた失敗から学んだ、触る範囲・検証・戻し方をセットにしたClaude Code用の依頼文テンプレートを紹介します。
Claude Code の権限拒否から復旧する: 止まった理由を次の安全手順に変える
Claude Code のコマンドが拒否されたとき、焦って許可を広げずに、拒否理由、代替手順、証拠コマンド、再試行条件へ分解する方法。
Claude Codeにビルド→スモークテスト→自動修正を回させる足場の作り方
最小スモークテストの選び方、失敗ログを食わせて直させるループ、回数上限と確認ゲートで暴走を止める方法を、コピペで動くコード付きで紹介します。