GODRICH

シェアボタン デザイン パターン9選 — コピペで動く

シェアボタン デザインは、訪問者がワンタップで記事を外へ送り出す最後の扉の形そのものです。ここに集めた9つは見かけだけの作り物ではなく、押すと本当にコピーされるシェアUI を HTML・CSS・JavaScript で組んだものなので、そのまま貼ればすぐ動きます。

自動再生 · タイルを押すとその項目へ · すべて1つのzip

9つは流行の順ではなく、訪問者がシェアに出会う順に並べました。読み終えた場所で出会う一行シェアバー(01)から、読んでいる間に横に付く縦バー(02)、押した瞬間に結果を伝えるコピートースト(03)へと続きます。信頼を見せるシェア数カウントアップ(04)を過ぎると、場所を節約する二つの方法、ボタン1つから展開するアイコン(05)と下部固定バー(07)が現れ、スマホでは下から上がってくるシェアシート(06)がその役を担います。最後の2つで共有を閉じます。オフラインをつなぐQRポップアップ(08)と、受け取り手を直接選ぶメール・埋め込みボックス(09)です。9つはインク #15131a・クリーム #fdf8f0・空色 #7fd4f5 という同じ色の値を共有するので、2つ3つを混ぜて使っても色がずれません。

01一行シェアバー

カードの一番下に、ラベルと44pxの丸いアイコンボタン4つが一行で並ぶ、誰もが最初に作る形です。ホバー・フォーカスでボタンが4px浮き上がり、重ねて置いた円が scale(.72) から scale(1) へ満ちていきます。44px は指が触れる最小の面積です。押したボタンは aria-pressed が押された状態のまま残り、何を選んだかを示します。

scale 링translateY 들림:focus-visible
/* リングは枠線ではなく重ねた円 — 拡大してもレイアウトを再計算しない */
.ir__ring {
  position: absolute;
  inset: 0;
  border: 2px solid $color;
  border-radius: 50%;
  transform: scale(.72);
  opacity: 0;
  transition: transform $dur-quick $easing, opacity $dur-quick $easing;
}
.ir__btn:focus-visible .ir__ring {
  transform: scale(1);
  opacity: 1;
}
.ir__btn[aria-pressed="true"] {
  background: $color;
  color: $sh-paper;
}

02記事横の縦シェアバー

マガジン型の長い記事では、シェアバーは本文の横に縦1列で並び、position: sticky でスクロールに追従します。ボタンは普段ラベルを隠しておき、ホバー・フォーカスで横へ展開します。展開は幅を伸ばすのではなく、clip-path: inset(0 100px 0 0 round 999px) で切り取ってあった右側を現すものなので、レイアウトは再計算されません。切り取っておく幅には条件があり、残りがラベルの始まる位置(余白12px+アイコン20px+その間の隙間8pxの合計40px)を超えると、折りたたんだ状態でラベルの頭文字が透けて見えます。

clip-path: inset()position: sticky시차 등장
/* 展開した幅(136px)で箱を確保し、右側100pxを切り取っておく */
.sr__link {
  width: 136px;
  height: 44px;
  clip-path: inset(0 100px 0 0 round $r-pill);
  transition: clip-path $dur-base $easing;
}
.sr__link:hover,
.sr__link:focus-visible {
  clip-path: inset(0 0 0 0 round $r-pill);
}

03リンクコピートースト

シェアの半分はコピーです。ボタンを押すと navigator.clipboard.writeText でアドレスが実際にコピーされ、下から上がってくるトーストが結果を伝えます。クリップボード API がない・書き込みが遮られた場合を見過ごさないこと。このデモは API がない時は入力欄を選択して「手動でコピーしてください」と案内し、遮られた時も確認マークの代わりにバツ印を出します。失敗にチェックを見せるのは嘘になるからです。結果の文は role="status" の一枠が読み上げます。

navigator.clipboardrole=statussteps(1, end)
function show(msg, failed) {
  text.textContent = msg;
  root.classList.toggle('is-bad', !!failed);   // 遮られた時は確認マークではなくバツ印
  root.classList.add('is-done');
}
navigator.clipboard.writeText(url).then(
  function () { show('주소를 복사했습니다', false); },
  function () { show('복사가 막혔습니다. 직접 선택해 주세요', true); }
);

04シェア数カウントアップ

何回シェアされたかを示すバッジは信頼の数字ですが、0から目標まで上げるのにJavaScriptのカウンターは要りません。@property で型を登録したカスタムプロパティとカウンターが、その場で数字を転がします。登録なしで使うと型がなく補間されず、途中で一度ぱっと変わるだけになる点がこの項目の芯です。inherits: false で登録したので、値はバッジ自身に置きます。

@propertycounter-resetscale 튐
@property --shareCount {
  syntax: "<integer>";
  inherits: false;
  initial-value: 0;
}
.cb__badge {
  counter-reset: shareCount var(--shareCount);
}
.cb__badge::after { content: counter(shareCount); }
.cb__badge--a { --shareCount: 128; }   /* .is-demo を外した状態の最終値 */

05展開するアイコンシェアボタン

ヘッダーが狭いワンページには、シェアボタンを1つだけ置きます。押すとプラットフォームのボタン4つが時間差で横に展開します。折りたたみは幅を0に縮めるのではなく、左端から clip-path で完全に切り取っておくことなので、場所は先に確保されており、展開しても隣の文字は押し出されません。畳んでいる間、ボタンは tabIndex -1 で Tab の順番から外れ、Esc で畳まれるとフォーカスはシェアボタンへ戻ります。

aria-expandedclip-path: inset()translateX
.ex__item {
  transform: translateX(-16px);
  clip-path: inset(0 100% 0 0 round 50%);
  transition: clip-path $dur-base $easing, transform $dur-base $easing;
}
.ex.is-open .ex__item {
  transform: translateX(0);
  clip-path: inset(0 0 0 0 round 50%);
}
.ex.is-open .ex__item:nth-child(2) { transition-delay: 60ms; }

06モバイルシェアシート

スマホでは下から上がってくるシートが見慣れた形です。シェアボタンを押すと role="dialog" のシートが上がり、背面が暗くなり、4つの枠はシートより遅れて浮かび上がります。アクセシビリティの骨格はフォーカストラップです。開くと最初のボタンへフォーカスが移り、Tab が最後から最初へ戻り、Esc と背面クリックで閉じ、閉じた後はフォーカスがシェアボタンへ戻ります。

translateY 바텀시트role=dialogfocus trap
// フォーカストラップ — シートが開いている間、Tab はシートの中を巡る
sheet.addEventListener('keydown', function (e) {
  if (e.key === 'Escape') { setOpen(false); return; }
  if (e.key !== 'Tab') return;
  var list = focusables(); var last = list.length - 1;
  var at = list.indexOf(document.activeElement);
  if (e.shiftKey && at <= 0)      { e.preventDefault(); list[last].focus({ preventScroll: true }); }
  else if (!e.shiftKey && at === last) { e.preventDefault(); list[0].focus({ preventScroll: true }); }
});

07下部固定シェアバー

読む流れを断たないため、バーは画面の下に付けます。バーは文書の中で position: sticky; bottom: 0 として張り付き、取っ手を押すか12px以上上へスワイプすると、アイコン5つが展開します。展開の高さは固定pxではなく grid-template-rows: 0fr → 1fr のグリッドトラックです。height: auto は中間値が計算されませんが、0fr と 1fr の間は計算されます。内側のラッパーには min-height: 0 が必要です。

grid-template-rowsaria-expandedtouch-action: pan-y
.sb__panel {
  display: grid;
  grid-template-rows: 0fr;                 /* 折りたたんだ状態 */
  overflow: hidden;
  transition: grid-template-rows $dur-base $easing;
}
.sb__inner { min-height: 0; }              /* 0fr で実際に畳むための一行 */
.sb.is-open .sb__panel { grid-template-rows: 1fr; }

08QRコード共有ポップアップ

名刺・ブース・看板など手が届かない場所では、画面のアドレスをカメラに渡します。このポップアップのQRは画像ファイルではなく、黒いマスと白いマスをグリッドで敷き詰めた29×29のモジュール行列なので、拡大してもにじまず、色を重ねる間違いも起きません。実際に描画された画面をカメラライブラリで読み直し、https://godrichstory.com が返ることまで確認しました。上をなぞるスキャン線はプレビューにだけ現れます。

role=dialogtranslateY 스캔 라인scale 팝업
// 29×29のモジュール行列。画像ではなく黒枠の座標 — 行29個の全体は zip の原本にあります
var rows = [
  "00000000000000000000000000000",
  "00111111101100010010111111100",
  "00100000100000010000100000100"
];

rows.forEach(function (line) {
  line.split('').forEach(function (bit) {
    var cell = document.createElement('span');
    cell.className = bit === '1' ? 'qp__cell is-on' : 'qp__cell';
    cells.appendChild(cell);
  });
});
grid.appendChild(cells);

09メール・埋め込み共有ボックス

受け取り手が決まっている時は、メールが直接相手に届きます。メールタブのリンクは mailto: に件名と本文が URL エンコード済みのまま込められていて、押すとほとんど書き終えた手紙が開き、埋め込みタブは読み取り専用のコード欄とコピーボタンを渡します。タブの切り替えはクリックと左右矢印ででき、下線はマス幅の単位(translateX(calc(var(--n) * 100%)))で移ります。

mailto:readonlytranslateX
// 下線は推測したpxではなく、マス番号だけを基準に動く
function move(i) {
  at = (i + tabs.length) % tabs.length;
  tabs.forEach(function (t, k) {
    t.classList.toggle('is-on', k === at);
    t.setAttribute('aria-selected', k === at ? 'true' : 'false');
    t.tabIndex = k === at ? 0 : -1;        // ロービングtabIndex
    panes[k].hidden = k !== at;
  });
  root.style.setProperty('--n', at);
}

どこで壊れるか — 落とし穴

最初に壊れたのは代表画像でした。04カウントアップでは、数字が目標(128・64・39)に届く瞬間とバッジが跳ねる瞬間を、同じサンプル枠に入れてはいけません。24フレームは2秒を4.1667%間隔で切るので、2つの出来事が1枠に集中すると「数え途中の数字」を写した場面の変化量が最も大きくなり、それが代表に選ばれるためです。現在のソースは数字が50%で目標に届き58%で跳ねるよう2つの出来事を8%間隔・サンプル2枠に分けて、値が満ちた後のバッジの跳ねが代表画像になるようにしています。

2番目は02縦バーのマス幅でした。バーが入るグリッドのマスを折りたたんだ幅44pxで取ると、展開した瞬間ラベルが本文の見出しの上に重なりました。展開した幅全体をマスとして先に確保すべきでした。同じファイルで、折りたたんだ状態の切り取り幅も92pxに取っていたせいでラベルの頭文字の左4pxが透け、残りがラベルの始まる40px(余白12px+アイコン20px+隙間8px)に収まる100pxへ広げて解決しました。

3番目は静的な状態でした。.is-demo のループでは問題ない9つのうち3つで、ループを外すと画面が空になるか重なりました。09はパネルに display: flex をクラスで与えたため [hidden] の既定 display: none に勝って2枚が同時に見え、08は手で見る時もスキャン線が残り、03は失敗の案内にアイコンがありませんでした。.ee__panel[hidden] { display: none } の再宣言、スキャン線の opacity: 0 初期値、バツ印アイコンの追加でそれぞれ直しました。計測も目視も、見るのはループの状態だけだということを身をもって学びました。

最後は06シートの背面でした。最初はシートが上がる間に linear フェードで暗くするようにしたところ、半分上がったシートが途中で切れて見える場面が代表画像に選ばれました。背面の暗さが、シートが着いた後の1フレームで steps(1, end) に切り替わるようにすると、カード全体が一度に変わるその場面が最大の変化になり、4つの枠が全部立ったシートが代表になりました。

アクセシビリティ

色の組み合わせは感で選ばず、WCAG 2.1 の相対輝度の式で組み合わせごとに直接計算しました。使わないと決めた組み合わせは、表の一番下の2行だけです。

前景 背景 実測コントラスト 本文の基準
インク #15131a 白 #ffffff 18.42:1 合格
インク #15131a クリーム #fdf8f0 17.42:1 合格
インク #15131a 空色 #7fd4f5 11.09:1 合格
スレート #5b5566 白 #ffffff 7.15:1 合格
スレート #5b5566 クリーム #fdf8f0 6.76:1 合格
白 #ffffff インク #15131a 18.42:1 合格
ミスト #bdb7c6 インク #15131a 9.43:1 合格
白 #ffffff 濃紺 #0f5f8a 6.94:1 合格
ミント #78e0b4 インク #15131a 11.51:1 合格
杏 #ffb3a0 インク #15131a 10.71:1 合格
インク #15131a オレンジ #ff4d1f 5.56:1 合格
白 #ffffff オレンジ #ff4d1f 3.32:1 不合格 — 使わない
白 #ffffff 黄 #ffd23f 1.44:1 不合格 — 使わない

こうして合格した組み合わせだけで描き直した9式が zip にそのまま入っており、開くパスワードは xbet8bb8 です。動きを減らしてほしいという設定(prefers-reduced-motion: reduce)では、9つすべてが動きだけを消し、状態と情報を残します。01はホバー・フォーカスのリングがそのまま見え、aria-pressed が選んだボタンを伝えます。02はラベルを畳んだまま止まりつつ、ホバー・フォーカスの展開は生きています。03はコピーの結果を、クリックした瞬間にトーストで伝えます。04は数字が宣言された最終値へすぐ満ちます。05と07は畳んだ状態で止まりつつ、開閉そのものは即座に応じます。06はシートが開いたまま立ち、フォーカストラップは働き続けます。08はコードが立ったままでスキャン線だけが消えます。09は選んだタブとそのパネルがそのまま見えます。

画面の構造で実際に入れた仕掛けはこうです。アイコンボタンはすべてアクセシブルな名前を持ち(ラベルが見える時はテキストで、アイコンだけの時は aria-label で)、01のトグルは aria-pressed、05・07の開閉は aria-expandedaria-controls を使います。06のシートは role="dialog"aria-modal、フォーカストラップ、Esc での閉鎖を実装し、08のポップアップも同じ役割名を持ちます。04のバッジは装飾なので aria-hidden で、数字はボタンのアクセシブルな名前の中で言葉として入っています。09は role="tablist" とロービングtabIndex でキーボードだけでタブを巡れ、コード欄は読み取り専用であることを伝えます。03の結果の文は role="status"aria-live="polite" の一枠で、画面の見えない人に同じ場所で一度だけ読まれます。コピーとダイアログのよりどころは MDN の Clipboard API 文書ARIAダイアログのパターン、コントラストの基準は W3C の最小コントラスト解説に従いました。読んでいる途中の通知が気になる方はトーストの重ね方を、画面の下から操作するバーが必要ならフローティングドックナビゲーションをお読みください。このサイトがコードをどう作り、確認しているかは紹介ページに書いてあります。

FAQ

シェアボタンは何個まで置くのがいいですか?

3つか4つです。01のようにリンク・メール・メッセージ・QR と役割の違う4つなら十分で、プラットフォームのロゴを8つ並べると逆にどれも押されません。場所が狭ければ、05のようにボタン1つに畳む方がよいです。プラットフォームの名前とロゴをそのまま持ち込むのも避けてください。商標とブランド色を無断で使うことになります。

コピーが成功したことをどうやって確かめますか?

ブラウザに尋ねるしかありません。navigator.clipboard.writeText はプロミスを返すので、03のように成功と失敗をそれぞれ受け取り、結果を言葉で伝え、失敗にはバツ印を出してください。クリップボードはシークレットモードや権限の設定で静かに遮られることがあり、成功だけを前提にした案内は嘘になります。API そのものがない古いブラウザへの備えも、もう一段の分岐で足ります。

QR は画像で入れてはいけないのですか?

入れられますが、このシリーズではそうしませんでした。08のようにモジュール行列をグリッドで描くと、拡大してもにじまず、色を重ねる間違いが起きず、追加のファイルを持たずに済みます。どの作り方でも守るべきルールは同じです。黒と白のコントラストを保ち、上に他の要素を重ねず、コードが何を収めているかを aria-label で伝えることです。08は「アドレスを収めたQRコード」と名付けています。

解凍パスワードを入力してください

パスワードはこの記事の本文の中にあります。読み進めると出てきます。