Bloggerでボトムナビゲーションを作成する

2025/08/15

Blogger 設定

 夏休みの宿題としてブログの改修作業を行いました。普段PCでこのブログを作成しているため気が付かなかったですが、スマホサイトなどの幅が狭い状態で自分のブログを見るとメニュー画面が分かりにくいということが分かりました。

具体的に言うと下記のような状態でPCサイトの場合はメニューを表示するようにしているのですが、スマホサイトの場合はハンバーガーアイコンでアイコンをクリックするとメニューが展開されて押せるようになっているのですが、ユーザー目線だとこれが分かりにくいと思いました。(このサイトのアクセス数の半分はスマホ経由で見ていただいているみたいでしたので、このタイミングで改善しました。)


いろいろと調べてみると最近のスマホサイトはボトムナビゲーションが主流ということでしたのでボトムナビゲーションを作成してスマホからアクセスしていただいている方向けに操作性の向上をはかりたいと思います。

また、私はHTMLにあまり触れてこなかったため、今回の記事はAIに教えていただきながら実装をしてます。間違っている点があればご教示お願いします。

また、作業する前に必ずバックアップを取るようにお願いします。

そもそもボトムナビゲーションとは

スマートフォン画面の下のほうに固定されているメニューのことをいいます。例えばXでいうと下記のように画面下部に表示されているナビゲーションのことを示します。

ボトムナビゲーションの例(X)

スマホの画面サイズなどにもよると思いますが、画面下部はAndroidの場合だとホームボタンなどが配置されているため、多くの方は画面下部を触りやすい持ち方をしていると思います。

画面下部にあることでスマホでの操作性を高め、ユーザーが辿りつきたいページにアクセスしやすくする補助ができるようになります。


HTMLを記述する

最初にHTMLでメニューの骨格を作成します。今回は"ホーム"、"メニュー"、"シェア"、"検索"、"トップへ"の5項目を作成しました。まず初めにHTMLでメニュー画面の枠組みを作成します。詳細は後程紹介しますが、メニュー画面及び検索画面はプルアップメニューを採用しているため、プルアップ用の骨組みも作成してます。

下記コードを</body>タグの直前に挿入してください。

<!--[START] ボトムナビゲーション -->
<nav class='bottom-nav'>
<a class='nav-item' href='/'>
<i class='fa-solid fa-house'/>
<span class='nav-text'>ホーム</span>
</a>

<button class='nav-item' id='js-menu-open' type='button'>
<i class='fa-solid fa-bars'/>
<span class='nav-text'>メニュー</span>
</button>


<button class='nav-item' id='js-share'>
<i class='fa-solid fa-share-nodes'/>
<span class='nav-text'>シェア</span>
</button>
<button class='nav-item' id='js-search-open' type='button'>
<i class='fa-solid fa-magnifying-glass'/>
<span class='nav-text'>検索</span>
</button>
<button class='nav-item' id='js-page-top' type='button'>
<i class='fa-solid fa-arrow-up'/>
<span class='nav-text'>トップへ</span>
</button>
<!--[START] 検索時に出るボトムナビゲーション -->

<div class='bottom-sheet' id='js-search-sheet'>
<div class='bottom-sheet-header'>
<h2 class='bottom-sheet-title'>検索</h2>
<button class='bottom-sheet-close' id='js-search-close' type='button'>
<i class='fa-solid fa-xmark'/>
</button>
</div>
<div class='bottom-sheet-content'>
<form action='/search' class='search-form' method='get'>
<input class='search-input' name='q' placeholder='キーワードを入力' type='search'/>
<button class='search-submit' type='submit'>
<i class='fa-solid fa-magnifying-glass'/>
</button>
</form>
<div class='search-history'>
</div>
</div>
</div>
<!--[END] 検索時に出るボトムナビゲーション -->

<div class='dropup-menu' id='js-menu'>
<div class='menu-header'>
<h3 class='menu-title'>メニュー</h3>
<button id='js-menu-close' type='button'>
<i class='fa-solid fa-xmark'/>
</button>
</div>
<ul class='menu-list' id='js-menu-list'/>
</div>

<!--[START] カテゴリー表示時に出るボトムナビゲーション -->

<!--[END] カテゴリー表示時に出るボトムナビゲーション -->
</nav>
<!--[END] ボトムナビゲーション -->
ここで実施していることはボトムナビゲーションを表示するアイコンなどの情報やこの後CSSやJavascriptで項目を編集できるようにするためのidやクラス属性を付与しています。

ホームボタンについてはhrefでホームのリンクへ飛ばすだけなので、これ以上特に追加の処理をしないのですが、その他のボタンについてはJavaScriptで動作を作成してます。

また、今回FontAwsomeというものを使用しています。Fontawsomeの対応がしていない方は下記の記事を参考に設定をお願いします。

CSSで見た目を整える

次にCSSを記述していきます。やっていることは先ほど設定した項目たちに付加情報を加えていき見栄えなどを整えていきます。やっていることは基本的にコメントに記載しましたので、コメントを参考にしていただけると幸いです。
下記CSSをテーマ→カスタマイズ→詳細設定→CSSを追加から追加します。

/* ===============ボトムナビゲーション(START)=============== */

/* ページ最下部のコンテンツがメニューに隠れないように、ページ全体の下に余白を作る */
body {
padding-bottom: 70px; /* メニューの高さ分くらい */
}

/* デフォルトスタイルの削除 */
.bottom-nav button {
background: none; /* 背景色を透明にする */
border: none; /* 枠線をなくす */
font: inherit; /* 親要素のフォントを継承 */
color: inherit; /* 親要素の文字色を継承 */
cursor: pointer; /*aタグの挙動と合わせてマウスオン時のカーソルを指に変える*/
}

/* メニュー全体のスタイル */
.bottom-nav {
/* ① 位置を画面下に固定 */
position: fixed;
bottom: 0;
left: 0;
right: 0; /* width: 100%; とほぼ同じ意味 */
z-index: 1000; /* 他の要素より手前に表示 */

/* ② 中の要素を横並びに */
display: flex;

/* ③ 見た目のデザイン */
background-color: rgba(255, 255, 255, 0.96);/* 背景色を白に(第4引数は透明度) */
border-top: 1px solid #e0e0e0; /* 上に薄いグレーの線を入れる */
box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.05); /* うっすらと影をつける */
}

/* 各メニュー項目のスタイル */
.bottom-nav a.nav-item,
.bottom-nav button.nav-item {
/* ④ 5つの項目を均等な幅にする */
flex: 1;
/* ⑤ アイコンとテキストを縦に並べて中央揃えに */
display: flex;
flex-direction: column;/* 中の要素を縦並びに */
justify-content: center;/* 縦方向の中央揃え */
align-items: center;/* 横方向の中央揃え */
/* ⑥ 細かい見た目の調整 */
padding: 8px 0;/* 上下に少し余白を入れる */
color: #555555;/* 文字とアイコンの色 */
text-decoration: none;/* リンクの下線を消す */
font-size: 10px; /* 文字のサイズ */

}




/* アイコンのスタイル */
.nav-item i {
font-size: 20px; /* アイコンのサイズ */
margin-bottom: 4px; /* アイコンとテキストの間の余白 */
}
#search-modal-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
background: #fff;
padding: 15px;
display: none; /* 初期状態では非表示 */
align-items: center;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
z-index: 9999;/*広告のレイヤーに負けないように(ただし、効果なさそう?)*/
box-sizing: border-box;
}
#search-modal-container.active {
display: flex;
flex-direction: column;
justify-content: center;
}


/* ===============ボトムナビゲーション(END)=============== */


/* ===============ボトムナビゲーション用の検索画面用ボトムシートの基本スタイル(START) =============== */
.bottom-sheet {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
max-height: 90vh; /* 画面の高さの90%まで */

background-color: #fff;
border-top-left-radius: 16px;
border-top-right-radius: 16px;
box-shadow: 0 -4px 12px rgba(0, 0, 0, 0.15);
transform: translateY(100%); /* 画面下部に完全に隠す */
transition: transform 0.3s ease-in-out;
z-index: 1000;
display: flex;
flex-direction: column;
}

/* ボトムシートが表示されたときのスタイル */
.bottom-sheet.is-active {
transform: translateY(0);
bottom: 53px;/*表示された際にボトムメニューとかぶらないように高さ調整*/
}

/* ヘッダーとコンテンツのスタイル */
.bottom-sheet-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
}

.bottom-sheet-content {
padding: 16px;
overflow-y: auto; /* コンテンツがはみ出したらスクロール可能に */
}

/* 検索フォームのスタイル */
.search-form {
display: flex;
gap: 8px;
}

.search-input {
flex-grow: 1;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 8px;
}

.search-submit {
padding: 8px 12px;
border-radius: 8px;
border: none;
background-color: #007bff;
color: #fff;
}
/* ===============ボトムナビゲーション用の検索画面用ボトムシートの基本スタイル(END) =============== */

/* ===============ボトムナビゲーション用のカテゴリー用のスタイル(START) ===============
/* カテゴリーメニューの初期状態を非表示にする */
.dropup-menu {
/* 画面外に配置して見えないようにする */
transform: translateY(100%);
/* アニメーションを滑らかにする */
transition: transform 0.3s ease-in-out;
/* 初期状態では透過度を0にして見えなくする */
opacity: 0;
/* ポインターイベントを無効にし、クリックできないようにする */
pointer-events: none;
/* 他のコンテンツの上に表示されるようにする */
position: fixed;
bottom: 53px; /* ボトムナビゲーションの高さに応じて調整 */
left: 0;
width: 100%;
z-index: 999;
/* その他のスタイル(背景色、パディングなど) */
background-color: #fff;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
}

/* is-activeクラスがついたときにメニューを表示する */
.dropup-menu.is-active {
/* 画面内に移動して見えるようにする */
transform: translateY(0);
/* 透過度を1に戻す */
opacity: 1;
/* ポインターイベントを有効にし、クリックできるようにする */
pointer-events: auto;
}
.menu-header {
/* 子要素(h3とbutton)を横に並べる */
display: flex;

/* 要素を両端に配置する */
justify-content: space-between;

/* 縦方向の中央に揃える */
align-items: center;

/* 上下に少し余白を入れる */
padding: 16px;

/* 下に境界線を入れる */
border-bottom: 1px solid #eee;
}

/* バツボタンの見た目を調整 */
#js-menu-close {
font-size: 20px; /* アイコンのサイズを大きくする */
color: #888; /* アイコンの色をグレーにする */
cursor: pointer; /* マウスホバー時にカーソルを指にする */
}
/* ===============ボトムナビゲーション用のカテゴリー用のスタイル(END) ===============
ここで何個か大事な設定を紹介します。まずはボトムナビゲーションを一番下に固定するための処理です。基本的にボトムナビゲーションは画面スクロールと連動させないで常時画面下部に固定したいために下記のような記述をして固定してます。
  position: fixed;

  bottom: 0;
最初に別のウィンドウで出すとお伝えした、メニュ-画面と、検索画面についてはtranslateYでY方向に100%下げるという記述をして、画面外に飛ばしてます。この後紹介するJavaScriptでボタンが押された際にこの設定値を変更する記述することで、ボタンを押したら画面の下から検索画面が出るなどの動作を実施できるようになります。

  transform: translateY(100%); /* 画面下部に完全に隠す */
また、今回は白背景に透過度をつけたボトムメニューを作成しましたが、サイトによっては白背景ではない方がいいという方がいると思います。そのような人は下記のrgba()の引数を調整してください。
background-color: rgba(255, 255, 255, 0.96);/* 背景色を白に(第4引数は透明度) */
  border-top: 1px solid #e0e0e0; /* 上に薄いグレーの線を入れる */
  box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.05); /* うっすらと影をつける */
下記コードがボタンが押された際の画面の制御に使っている項目となります。bottom:53pxとなっている箇所が表示する位置を調整するようなイメージで記述してます。今回の設定の場合は何回か試して53pxでボトムナビゲーションとかぶらない位置になったため、53pxという値を採用してます。(計算すれば出せるかもしれないですが、脳筋設定をしました。)もし、ボトムメニューのサイズなどを変更した場合はこの値も変えるのを忘れないようにお願いします。
.bottom-sheet.is-active {
  transform: translateY(0);
  bottom: 53px;/*表示された際にボトムメニューとかぶらないように高さ調整*/
}

JavaScriptで動作を記述する

最後にJavaScriptによるコードです。最初に追加したHTMLのコードの下にscriptタグを追加した上で下記のJavaScriptのコードを追加します。
<script>
  // ページの読み込みが完了してから処理を開始
  document.addEventListener('DOMContentLoaded', function() {
  
    // =================================================================
    // 【ここからトップへ戻る用の処理】
    // =================================================================

    //【トップへ戻るボタンの処理】
    const pageTopBtn = document.getElementById('js-page-top');
    if (pageTopBtn) {
      pageTopBtn.addEventListener('click', function(e) {
        window.scrollTo({
          top: 0,
          behavior: 'smooth' // スムーズにスクロール
        });
      });
    }

    // =================================================================
    // 【ここまでトップへ戻る用の処理】
    // =================================================================

  
    // =================================================================
    // 【ここからシェアボタン用の処理】
    // =================================================================

  //【シェアボタンの処理】
    const shareBtn = document.getElementById('js-share');
    if (shareBtn) {
      shareBtn.addEventListener('click', function(e) {

        // Web Share APIが使えるかチェック
        if (navigator.share) {
        	navigator.share({
            	title: document.title, // ページのタイトル
            	text: 'こちらの記事をシェアします', // 説明文
            	url: location.href // 今いるページのURL
            })
          .then(() => console.log('シェア成功'))
          .catch((error) => console.log('シェア失敗', error));
        } 
        else {
          // Web Share APIが使えないブラウザ用のメッセージ
          alert('お使いのブラウザはシェア機能に対応していません。');
        }
      });
    }

    // =================================================================
    // 【ここまでシェアボタン用の処理】
    // =================================================================

  
    // =================================================================
    // 【ここから検索メニュー用の処理】
    // =================================================================

  //ボトムメニューの検索ボタンの処理

  		const searchOpenBtn = document.getElementById('js-search-open');
  		const searchCloseBtn = document.getElementById('js-search-close');
  		const searchSheet = document.getElementById('js-search-sheet');

  // 検索ボタンを押したらボトムシートを表示
  if (searchOpenBtn && searchSheet) {
    searchOpenBtn.addEventListener('click', () => {
  	searchSheet.classList.add('is-active');
    });
  }

  // 閉じるボタンを押したらボトムシートを非表示
  if (searchCloseBtn && searchSheet) {
    searchCloseBtn.addEventListener('click', () => {
      searchSheet.classList.remove('is-active');
    });
  }
  // 検索メニュー外のクリックで検索メニューを閉じる
	document.addEventListener('click', (event) => {
  		if (searchSheet && searchOpenBtn && !searchSheet.contains(event.target) &&!searchOpenBtn.contains(event.target)) {
  	  		searchSheet.classList.remove('is-active');
  		}
	});

    // =================================================================
    // 【ここまで検索メニュー用の処理】
    // =================================================================
  

    // =================================================================
    // 【ここからカテゴリメニュー用の処理】
    // =================================================================
// カテゴリーメニューの開閉ボタンとメニュー本体の要素を取得
const menuOpenBtn = document.getElementById('js-menu-open');
const menuCloseBtn = document.getElementById('js-menu-close');
const menu = document.getElementById('js-menu');
const menuList = document.getElementById('js-menu-list');
const navigationWidget = document.getElementById('PageList1'); // ナビゲーションウィジェットのID

// ナビゲーションウィジェットからリンクをコピー
if (navigationWidget && menuList) {
  const navigationLinks = navigationWidget.querySelectorAll('.widget-content > ul > li > a');
  navigationLinks.forEach(link => {
    const newLi = document.createElement('li');
    const newA = document.createElement('a');
    newA.href = link.href;
    newA.textContent = link.textContent;
    newLi.appendChild(newA);
    menuList.appendChild(newLi);
  });
}

// カテゴリーメニューを開く
if (menuOpenBtn && menu) {
  menuOpenBtn.addEventListener('click', () => {
    menu.classList.add('is-active');
  });
}

// メニューを閉じる
if (menuCloseBtn && menu) {
  menuCloseBtn.addEventListener('click', () => {
    menu.classList.remove('is-active');
  });
}

// メニュー外のクリックでメニューを閉じる
document.addEventListener('click', (event) => {
  if (menu && menuOpenBtn && !menu.contains(event.target) && !menuOpenBtn.contains(event.target)) {
    menu.classList.remove('is-active');
  }
});
  
  // =================================================================
  // 【ここまでカテゴリメニュー用の処理】
  // =================================================================
  
 });
  
  
</script>
ここの処理の解説が今回の解説のメインとなります。ホームボタンについては最初に述べた通り特にリンクで飛ばしているだけのためJavascriptによる動作の規定をしていません。それ以外のボタンについて動作を紹介させていただきます。

トップへ戻る処理

順番が前後してしまいますが、一番動作が簡単なトップへ戻る処理から説明します。まず下記処理で、HTMLからjs-page-topという要素(ボタン)があるかを探して、pageTopBtnという変数に格納します。
const pageTopBtn = document.getElementById('js-page-top');
次にボタンが存在するかを確認しする処理を記述してます。この処理を入れている理由は万が一ボタンの要素がなかった場合に、エラーが発生してサイトが開けなくなるというのを防ぐためです。
const pageTopBtn = document.getElementById('js-page-top');
最後にボタンが押下された際の挙動を記述します。ここではwindow.scrollという文書内の特定の位置までスクロールさせるための記述を行います。ここではopx(ページの一番上)にsmooth(滑らかに動かす)という指示をしてます。

pageTopBtn.addEventListener('click', function(e) {
  window.scrollTo({
    top: 0,
    behavior: 'smooth'
  });
});
スクロールボタンの解説は以上です。

シェアボタンの動作

続いてシェアボタンの動作の説明をします。最初にボタンを見つけて、存在するかの確認をしているのはトップへ戻る処理と同じなので、ここでは説明を省略します。また、今回のシェア機能はブラウザに備わっているShare機能を使って動作を実現させてます。私が使える環境はEdgeとChoromeの2つでその2つは使えることは確認できましたが、それ以外の環境では使えることを確認できてません。もし、使えない場合はブラウザが原因なのかを切り分けるために、Choromeなどで動作確認を実施お願いします。
前置きが長くなりましたが、まず最初にユーザーが使用しているブラウザがSare機能を備えているか確認します。最初に紹介した下記のnavigator.shareの部分で機能が使えるか確認し、使える場合は必要な情報を
shareBtn.addEventListener('click', function(e) {
  if (navigator.share) {
    // Web Share APIが使える場合
  } else {
    // 使えない場合
  }
});
機能が使えない場合は使えない旨をユーザーに通知し、使える場合はシェア機能に必要なURL情報や記事のタイトル情報を下記処理にて渡します。

navigator.share({
  title: document.title,       // ページのタイトル
  text: 'こちらの記事をシェアします', // 共有時の説明文
  url: location.href         // 今見ているページのURL
})
.then(() => console.log('シェア成功'))
.catch((error) => console.log('シェア失敗'));

検索ボタンの動作

次に検索ボタンにのJavascriptについて解説します。要素が存在するかなどの処理はトップへ戻る処理と同様のため、ここでは説明は省略します。

下記の処理でボタンが押された場合にserchSheetという名前で定義した検索用の画面の要素(js-search-sheet)にis-activeというクラスを追加してます。

// 検索ボタンをクリックしたら
searchOpenBtn.addEventListener('click', () => {
  // 検索画面の要素に 'is-active' クラスを追加する
  searchSheet.classList.add('is-active');
});
この記述をすることでCSSで記述した下記処理が有効になり隠していた画面がtranslateYが0になるため、非表示していた検索画面がプルアップ表示されるという仕組みになります。
.bottom-sheet.is-active {
  transform: translateY(0); /* 移動をリセット */
  bottom: 53px; /* ナビゲーションを隠さないように位置を調整 */
}
あとはHTML側の下記記述で検索時のURLのqパラメーター(検索キーワードの指定)を使ってブログ内のキーワード検索を実現しています。
<form action='/search' method='get'>
<input type='text' name='q'/>
<button type='submit'>検索</button>
</form>
メニューを閉じる場合は表示されるバツボタンを押下するか、メニュー外のところをクリックされた場合に閉じるようにします。閉じる処理は下記の箇所で実施しており、バツボタンを押されたり、!menuOpenBtn.contains(event.target)でメニュー画面以外を押されたりした場合にis-activeクラスを削除します。is-activeクラスを削除することで、translateY(0)の処理が無効化され、画面外に移動してユーザーから見えなくなります。
// メニューを閉じる
if (menuCloseBtn && menu) {
  menuCloseBtn.addEventListener('click', () => {
    menu.classList.remove('is-active');
  });
}

// メニュー外のクリックでメニューを閉じる
document.addEventListener('click', (event) => {
  if (menu && menuOpenBtn && !menu.contains(event.target) && !menuOpenBtn.contains(event.target)) {
    menu.classList.remove('is-active');
  }

メニューボタンの動作

最後にメニューボタンの動作です。ここの実装がうまくいかず、生成AIのGeminiと何度かやり取りして実装しました。まだ、私が理解しきれていない可能性があるため、間違った説明をしていたら教えてください。

ボタンを押した際にメニューを表示させる動作などは検索と同じなので、ここでは説明は割愛します。

ここの処理で一番特徴的なのは下記の処理です。トップナビゲーションはブロガーのナビゲーションウィジェットから生成しているのですが、2つを連動させるためにforEachという命令でnavigationLinksの中にあるリンクを1つずつ新しい<li><a>要素を作りだして、メニューに表示させます。


// ナビゲーションウィジェットからリンクをコピー
if (navigationWidget && menuList) {
  const navigationLinks = navigationWidget.querySelectorAll('.widget-content > ul > li > a');
  navigationLinks.forEach(link => {
    const newLi = document.createElement('li');
    const newA = document.createElement('a');
    newA.href = link.href;
    newA.textContent = link.textContent;
    newLi.appendChild(newA);
    menuList.appendChild(newLi);
  });
}
以上でボトムメニューの実装は終了です。

まとめ

今回作成したボトムメニューは下記のようなものになります。(画像はメニューボタンを押下した場合の画面です。)
見栄えが少し悪いですが、ボトムメニュー作成の第一歩としてはかなり上出来だと思います。気が向いたら見栄えについてもこだわっていきたいと思います。

また、今回は生成AIのGeminiを活用しながら作成しました。JavaScriptは初心者でしたが、Geminiに聞くと引数の意味などいろいろと教えてくれるため、勉強しながら実装することができました。

生成AIの登場でプログラミングを勉強する障壁が下がった気がするので、今の若い人たちの勉強方法もどんどん変わっていくのだろうなと改めて実感しました。

自己紹介

はじめまして 社会人になってからバイクやプログラミングなどを始めました。 プログラミングや整備の記事を書いていますが、独学なので間違った情報が多いかもしれません。 間違っている情報や改善点がありましたらコメントしていただけると幸いです。

X(旧Twitter)

フォローお願いします!

QooQ