夏休みの宿題としてブログの改修作業を行いました。普段PCでこのブログを作成しているため気が付かなかったですが、スマホサイトなどの幅が狭い状態で自分のブログを見るとメニュー画面が分かりにくいということが分かりました。
具体的に言うと下記のような状態でPCサイトの場合はメニューを表示するようにしているのですが、スマホサイトの場合はハンバーガーアイコンでアイコンをクリックするとメニューが展開されて押せるようになっているのですが、ユーザー目線だとこれが分かりにくいと思いました。(このサイトのアクセス数の半分はスマホ経由で見ていただいているみたいでしたので、このタイミングで改善しました。)
いろいろと調べてみると最近のスマホサイトはボトムナビゲーションが主流ということでしたのでボトムナビゲーションを作成してスマホからアクセスしていただいている方向けに操作性の向上をはかりたいと思います。
また、私はHTMLにあまり触れてこなかったため、今回の記事はAIに教えていただきながら実装をしてます。間違っている点があればご教示お願いします。
また、作業する前に必ずバックアップを取るようにお願いします。
そもそもボトムナビゲーションとは
| ボトムナビゲーションの例(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で見た目を整える
/* ===============ボトムナビゲーション(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;
transform: translateY(100%); /* 画面下部に完全に隠す */
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-sheet.is-active {
transform: translateY(0);
bottom: 53px;/*表示された際にボトムメニューとかぶらないように高さ調整*/
}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>
トップへ戻る処理
const pageTopBtn = document.getElementById('js-page-top');
const pageTopBtn = document.getElementById('js-page-top');
pageTopBtn.addEventListener('click', function(e) {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
シェアボタンの動作
shareBtn.addEventListener('click', function(e) {
if (navigator.share) {
// Web Share APIが使える場合
} else {
// 使えない場合
}
});
navigator.share({
title: document.title, // ページのタイトル
text: 'こちらの記事をシェアします', // 共有時の説明文
url: location.href // 今見ているページのURL
})
.then(() => console.log('シェア成功'))
.catch((error) => console.log('シェア失敗'));
検索ボタンの動作
下記の処理でボタンが押された場合にserchSheetという名前で定義した検索用の画面の要素(js-search-sheet)にis-activeというクラスを追加してます。
// 検索ボタンをクリックしたら
searchOpenBtn.addEventListener('click', () => {
// 検索画面の要素に 'is-active' クラスを追加する
searchSheet.classList.add('is-active');
});
.bottom-sheet.is-active {
transform: translateY(0); /* 移動をリセット */
bottom: 53px; /* ナビゲーションを隠さないように位置を調整 */
}
<form action='/search' method='get'>
<input type='text' name='q'/>
<button type='submit'>検索</button>
</form>
// メニューを閉じる
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');
}メニューボタンの動作
ボタンを押した際にメニューを表示させる動作などは検索と同じなので、ここでは説明は割愛します。
ここの処理で一番特徴的なのは下記の処理です。トップナビゲーションはブロガーのナビゲーションウィジェットから生成しているのですが、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の登場でプログラミングを勉強する障壁が下がった気がするので、今の若い人たちの勉強方法もどんどん変わっていくのだろうなと改めて実感しました。
0 件のコメント:
コメントを投稿