アクションバーを本文左側に置く構成のイメージです。
記事末尾に集まっていた、はてなスター・はてなブックマーク・X共有・URLコピーを、PCでは本文カードの左側へ縦一列で置くようにした。読み進めている途中でも操作しやすくなり、記事の最後まで戻る手間を減らせる。
このページでは、見た目だけでなく、はてなブログの標準ボタンをどう扱うか、どの画面幅で表示を止めるかまで含めて整理する。
- この記事でできるようになること
- 変更前の課題
- 仕上がり
- 実装方針
- 完成版のJavaScript
- 動的に生成されるはてなスターを扱う
- 完成版のCSS
- 画面幅が狭いときは本文を優先する
- レスポンシブ設計の整理
- 検証項目
- まとめ
- 関連記事
この記事でできるようになること
- はてなスター・はてなブックマーク・X共有・URLコピーを本文左側へ配置する
- はてなブログが後から生成する既存のスター追加ボタンを、独自ボタンから操作する
- スター追加ボタンがまだ見つからない場合に、記事末尾へ自然にフォールバックする
- 本文に近づきすぎる画面幅では、アクションバーを非表示にして読書領域を守る
- PCとモバイルで、操作性と本文幅の優先順位を切り替える
変更前の課題
はてなブログ標準のスター・ブックマーク・共有ボタンは、記事末尾にまとまっている。読み終わる前に反応したくなっても、そのためにページ下部まで移動しなければならない。
記事が長くなるほど、この移動が小さなストレスになる。そこで、記事を読みながら操作できるよう、本文の左側にアクションバーを追加した。
ただし、画面幅が十分でない状態で左側へ押し出すと、本文やサイドバーと重なる。操作ボタンを常に表示するのではなく、本文を優先して表示範囲を決めることが重要だった。
仕上がり
PC表示:読書領域の外側に4つの操作を置く
実サイトでの表示例。本文カードの左側に、スター・ブックマーク・X共有・URLコピーのボタンが縦に並ぶ。
実装上のポイント
ここでいう「固定」は、position: fixedで画面全体に貼り付ける意味ではない。実装ではposition: stickyを使い、記事コンテナの範囲内でスクロールに追従させている。
記事の外までボタンが残り続けないため、本文との関係が崩れにくい。一方で、親要素の高さやoverflowの指定によっては、stickyが効かなくなるので注意が必要だ。
実装方針
body.page-entryが付いた記事詳細ページだけで実行する.entryまたは.hentryと、#mainまたは#wrapperを取得する.floating-actionsというアクションバーをJavaScriptで生成し、mainの先頭へ挿入する- 4つの操作を、独自ボタンまたはリンクとして配置する
- はてなスターだけは自前実装せず、標準の追加ボタンへクリックを委譲する
1320px以下では非表示にして、本文との重なりを避ける
完成版のJavaScript
先に完成版を載せる。はてなブログの「デザイン」→「カスタマイズ」→「フッタ」へ貼り付ければ動く構成にした。
<script> (() => { 'use strict'; const ICONS = { star: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`, hatebu: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m19 21-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z"></path></svg>`, x: `<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"></path></svg>`, copy: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`, copied: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>` }; const handleStarClick = () => { const originalStarButton = document.querySelector( '.hatena-star-add-button' ); if (originalStarButton) { originalStarButton.click(); return; } const entryFooter = document.querySelector('.entry-footer'); if (entryFooter) { entryFooter.scrollIntoView({ behavior: 'smooth' }); } }; const handleCopyUrl = async (button) => { try { await navigator.clipboard.writeText(window.location.href); button.classList.add('copied'); button.innerHTML = ICONS.copied; setTimeout(() => { button.classList.remove('copied'); button.innerHTML = ICONS.copy; }, 1500); } catch (error) { console.error('URLのコピーに失敗しました:', error); } }; const initFloatingActionBar = () => { if (!document.body.classList.contains('page-entry')) { return; } const entry = document.querySelector('.entry') || document.querySelector('.hentry'); const main = document.querySelector('#main') || document.querySelector('#wrapper'); if (!entry || !main) { return; } const currentUrl = window.location.href; const pageUrlEncoded = encodeURIComponent(currentUrl); const pageTitleEncoded = encodeURIComponent(document.title); const hatebuUrl = 'https://b.hatena.ne.jp/entry/' + currentUrl; const xShareUrl = 'https://twitter.com/intent/tweet?url=' + pageUrlEncoded + '&text=' + pageTitleEncoded; const actionsBar = document.createElement('div'); actionsBar.className = 'floating-actions'; actionsBar.setAttribute('aria-label', '記事アクション'); actionsBar.innerHTML = ` <button type="button" class="article-action-btn btn-star" title="はてなスターを付ける" aria-label="はてなスターを付ける"> ${ICONS.star} </button> <a href="${hatebuUrl}" target="_blank" rel="noopener noreferrer" class="article-action-btn btn-hatebu" title="はてなブックマークに追加" aria-label="はてなブックマークに追加"> ${ICONS.hatebu} </a> <a href="${xShareUrl}" target="_blank" rel="noopener noreferrer" class="article-action-btn btn-x" title="Xでポスト" aria-label="Xでポスト"> ${ICONS.x} </a> <button type="button" class="article-action-btn btn-copy-url" title="記事URLをコピー" aria-label="記事URLをコピー"> ${ICONS.copy} </button> `; main.insertBefore(actionsBar, main.firstChild); const starButton = actionsBar.querySelector('.btn-star'); if (starButton) { starButton.addEventListener('click', handleStarClick); } const copyButton = actionsBar.querySelector('.btn-copy-url'); if (copyButton) { copyButton.addEventListener('click', () => { handleCopyUrl(copyButton); }); } }; if (document.readyState === 'loading') { document.addEventListener( 'DOMContentLoaded', initFloatingActionBar ); } else { initFloatingActionBar(); } })(); </script>
元コードはGitHubのfooter.htmlで管理している。
操作の役割は次のとおり。
- はてなスター:標準のスター追加ボタンを代理クリックする
- はてなブックマーク:
b.hatena.ne.jp/entry/へ記事URLを渡す - X共有:
twitter.com/intent/tweetへ記事URLとタイトルを渡す - URLコピー:
navigator.clipboard.writeText()で現在のURLをコピーする
動的に生成されるはてなスターを扱う
はてなスターの追加ボタンは、ページ読み込み直後から存在するとは限らない。読み込み時に一度だけ取得して保存すると、ボタンがまだ生成されていない状態を記憶してしまう。
そこで、スター用ボタンがクリックされた瞬間に.hatena-star-add-buttonを検索する。
const handleStarClick = () => { const originalStarButton = document.querySelector('.hatena-star-add-button'); if (originalStarButton) { originalStarButton.click(); return; } const entryFooter = document.querySelector('.entry-footer'); if (entryFooter) { entryFooter.scrollIntoView({ behavior: 'smooth' }); } };
見つかった場合は、はてなブログ標準の処理へ任せる。見つからない場合はエラーを表示せず、記事末尾へスムーススクロールする。
この「クリック時に再検索する」作りが、動的生成される標準UIを独自UIから扱うときのポイントになる。
完成版のCSS
CSSは、はてなブログの「デザイン」→「カスタマイズ」→「デザインCSS」へ追加する。
アクションバーは本文カードの左側へ少しはみ出す。float: leftとmargin-left: -54pxを組み合わせ、記事本文のレイアウトを大きく変えずに操作列を外側へ出した。
/* 記事ページ以外では表示しない */ body:not(.page-entry) .floating-actions { display: none !important; } /* 記事本文の左側へ配置 */ .page-entry .floating-actions { position: sticky; top: 80px; float: left; margin-top: 0; margin-left: -54px; z-index: 90; display: flex; flex-direction: column; align-items: center; gap: 10px; } /* 操作ボタン共通 */ .article-action-btn { display: flex; align-items: center; justify-content: center; width: 38px; height: 38px; border: 1px solid var(--ntn-border); border-radius: var(--ntn-radius); background: var(--ntn-surface); color: var(--ntn-muted); box-shadow: var(--ntn-shadow-sm); cursor: pointer; text-decoration: none; transition: all 0.15s ease; } .article-action-btn:hover { background: var(--ntn-bg-hover); color: var(--ntn-text); border-color: var(--ntn-border-strong); } /* URLコピー成功時 */ .article-action-btn.copied { background: var(--callout-green-bg); color: #2b7a3e; border-color: rgba(43, 122, 62, 0.25); }
このCSSは、サイト全体で定義済みの--ntn-*変数を参照している。別テーマへ移植する場合は、背景色・文字色・境界線・影を固定値へ置き換えれば使える。
画面幅が狭いときは本文を優先する
左側へはみ出すレイアウトは、画面幅が足りないと本文やサイドバーへ重なりやすい。そこで、現在の実装では1320px以下でアクションバーを非表示にしている。
@media (max-width: 1320px) { .floating-actions { display: none !important; } }
これは機能を無理に残すための調整ではなく、本文を読みやすく保つための判断である。狭い画面では、記事末尾にある標準の導線を使う方が、本文を圧迫しない。
モバイル表示:アクションバーを隠して本文幅を守る
600px表示の実サイト例。アクションバーは表示せず、タイトルや本文を画面幅に合わせて読めるようにしている。
レスポンシブ設計の整理
| 画面幅 | アクションバー | 判断 |
|---|---|---|
1321px以上 |
表示 | 本文カードの外側に操作列を置く |
1320px以下 |
非表示 | 本文やサイドバーとの重なりを避ける |
| モバイル | 非表示 | 操作列より本文幅とタップしやすさを優先する |
ブレークポイントを細かく増やすより、「操作列を置くための余白があるか」を基準に表示・非表示を決めた。
検証項目
- ☐ PC幅(
1321px以上)で、本文カードの左側に4つのボタンが表示されるか - ☐ スター追加ボタンが後から生成された場合でも、スター操作が標準ボタンへ届くか
- ☐ スター追加ボタンが見つからない場合、記事末尾へスムーススクロールするか
- ☐ ブックマークリンクが現在の記事URLを開くか
- ☐ X共有リンクに記事URLとタイトルが渡るか
- ☐ URLコピー後にボタンの表示が一時的に完了状態へ変わるか
- ☐
1320px以下でアクションバーが非表示になるか - ☐ 一覧ページやアーカイブページにはアクションバーが表示されないか
- ☐ サイドバーや本文と操作ボタンが重ならないか
まとめ
記事末尾にあったスター・ブックマーク・共有・URLコピーの導線を、PCでは本文左側へ縦一列で配置した。
ポイントは、はてなスターを自前で再実装せず、クリック時に標準の.hatena-star-add-buttonを検索して代理操作すること。そして、ボタンが見つからない場合のフォールバックを用意して、動的生成のタイミング差を読者へ見せないことである。
見た目の「固定」にはposition: stickyを使い、本文との距離が近づく1320px以下では非表示にする。操作性と読みやすさの境界を、CSSの表示ルールとして決めた。


