シリーズ ゼロからWebサイトを作ろう! 番外編 第2回

JavaScriptを少しだけ ―
インタラクションを加えよう

完成したcafé SOELのサイトに、操作への反応を加えます。ハンバーガーメニューの開閉、ARIA属性の更新、スクロール連動アニメーションまで、最小限のJavaScriptで整えていきましょう。

本編と前回のおさらい

📖 前回のおさらい
  • 本編全10回で、café SOELのWebサイトを公開できる状態まで作り上げました
  • 番外編第1回では、CSSアニメーションと fade-in-up クラスを追加しました
  • スクロール連動の動きには、JavaScriptで is-visible クラスを付ける必要がありました

本編の最後で、あなたのサイトは世界に公開できる状態になりました。 そして前回、CSSだけで「どう動くか」の準備をしましたね。

今回は、その準備に命を吹き込みます。 JavaScriptは、HTMLとCSSで作った画面に「反応」を与える言語です。 たとえば、ボタンを押したらメニューが開く。 スクロールしたら要素がふわっと現れる。 そういう小さな体験を担当します。

🎯 今回のゴール
  • js/main.js を作成し、HTMLから読み込む
  • ハンバーガーメニューをクリックで開閉できるようにする
  • aria-expanded をJavaScriptで更新し、状態を正しく伝える
  • ナビリンクをクリックしたら、モバイルメニューを閉じる
  • 前回準備したスクロール連動アニメーションを動かす
👀 完成形デモ
本編で作った完成形はこちらです。今回のJavaScriptは、この完成形をさらに気持ちよく操作できるようにするための一歩です。
完成形デモを見る →

JavaScriptの役割を、まず小さくつかむ

HTMLは構造、CSSは見た目。 ではJavaScriptは何でしょう。 私はよく「照明のスイッチ」に例えます。

部屋そのものを作るのがHTML。 壁紙や家具を整えるのがCSS。 そして、スイッチを押したら明かりがつく。 その「操作に反応する部分」がJavaScriptです。

JavaScriptは、ページに命令する言語。大事なのは、最初から大きく書こうとしないこと。

今回使う考え方は、たった3つです。

1

要素を見つける

document.querySelector() で、HTMLの中から操作したい要素を探します。

2

イベントを待つ

addEventListener() で、「クリックされたら」などの操作を待ちます。

3

クラスや属性を変える

classList.toggle()setAttribute() で、見た目や状態を切り替えます。

🔰 初心者向け補足
JavaScriptは一気に難しく感じやすい言語です。でも今回やることは、「探す → 待つ → 変える」だけ。 まずはこの流れを体で覚えれば大丈夫です。

js/main.jsを作ってHTMLから読み込む

まずはJavaScriptを書くファイルを用意します。 プロジェクトの中に js フォルダを作り、 その中に main.js を作成してください。

📁 今回追加するファイル
my-website/
├── index.html
├── css/
│   └── style.css
├── img/
│   └── ...
└── js/
    └── main.js

次に、index.html からこのJavaScriptファイルを読み込みます。 場所は </body> の直前でも動きますが、 今回は現場でもよく使う defer 付きの読み込みにしましょう。

index.html — <head> 内に追加
<!-- ↓ CSSのlinkタグの近くに追加 -->
<script src="js/main.js" defer></script>

defer は、「HTMLを読み終わってからJavaScriptを実行してね」という指定です。 これがあると、JavaScriptがHTMLより先に動いてしまって 「探したい要素がまだ存在しない」という事故を防げます。

💡 KANONのワンポイント
昔は </body> の直前にscriptを書くことが多くありました。 今でも間違いではありません。ただ、外部ファイルを defer で読み込む形は、 HTMLの見通しがよく、実務でも扱いやすいです。

ハンバーガーメニューをクリックで開閉する

ここから、いよいよJavaScriptを書いていきます。 まず操作したい要素は2つ。 メニューボタンと、ナビゲーションです。

js/main.js — 要素を取得する
// ハンバーガーメニューの要素を取得
const menuToggle = document.querySelector('.menu-toggle');
const nav = document.querySelector('.nav');

querySelector() は、CSSセレクタと同じ書き方で要素を探せます。 '.menu-toggle' なら、class名が menu-toggle の要素。 '.nav' なら、class名が nav の要素です。

⚠️ よくあるミス
もし本編のHTMLで <nav>class="nav" が付いていない場合は、 追加してください。CSSのレスポンシブメニューも .nav を前提にしています。
index.html — navにclassを確認
<nav class="nav" aria-label="メインナビゲーション">
  <!-- nav-list が入る -->
</nav>

次に、ボタンがクリックされたら is-open クラスを付け外しします。 第6回・第7回でCSS側にはすでに、 .nav.is-open.menu-toggle.is-open の見た目を用意していました。

js/main.js — クリックで開閉する
menuToggle.addEventListener('click', () => {
  menuToggle.classList.toggle('is-open');
  nav.classList.toggle('is-open');
});

保存して、ブラウザで確認してみましょう。 画面幅を狭くしてハンバーガーメニューをクリックすると、 メニューが開閉するはずです。

▶ Live Demo — ハンバーガーメニューの開閉
café SOEL

aria-expandedも一緒に更新する

メニューは開閉できるようになりました。 でも、ここで大事なことがあります。 見た目だけでなく、「今メニューが開いているかどうか」を 支援技術にも伝えること。

そのために使うのが aria-expanded です。 false なら閉じている。 true なら開いている。 ボタンの状態を伝える属性です。

index.html — ボタンのARIA属性を確認
<button
  class="menu-toggle"
  type="button"
  aria-label="メニューを開く"
  aria-expanded="false"
>
  <span class="menu-toggle-bar"></span>
  <span class="menu-toggle-bar"></span>
  <span class="menu-toggle-bar"></span>
</button>

JavaScript側では、クラスを切り替えたあとに 「今開いているか」を調べます。 そして、その結果を aria-expanded に反映します。

js/main.js — aria-expandedを更新
menuToggle.addEventListener('click', () => {
  menuToggle.classList.toggle('is-open');
  nav.classList.toggle('is-open');

  // メニューが開いているかどうかを判定
  const isOpen = menuToggle.classList.contains('is-open');

  // 状態をARIA属性に反映
  menuToggle.setAttribute('aria-expanded', isOpen);
  menuToggle.setAttribute(
    'aria-label',
    isOpen ? 'メニューを閉じる' : 'メニューを開く'
  );
});

アクセシビリティは、あとから飾るものではなく、動きを作る瞬間に一緒に設計するもの。

ナビリンクをクリックしたらメニューを閉じる

モバイルメニューでは、リンクを押したあともメニューが開いたままだと、 画面がふさがってしまいます。 そこで、ナビリンクをクリックしたら自動で閉じる処理を追加します。

js/main.js — ナビリンクを取得
// ナビリンクをすべて取得
const navLinks = document.querySelectorAll('.nav-link');

querySelectorAll() は、条件に合う要素をすべて取得します。 今回なら、Concept、Menu、Gallery、Accessの4つのリンクですね。

js/main.js — リンククリックで閉じる
navLinks.forEach((link) => {
  link.addEventListener('click', () => {
    menuToggle.classList.remove('is-open');
    nav.classList.remove('is-open');
    menuToggle.setAttribute('aria-expanded', 'false');
    menuToggle.setAttribute('aria-label', 'メニューを開く');
  });
});

ここでは toggle() ではなく remove() を使っています。 理由は、「クリックしたら必ず閉じたい」から。 状態を反転させるのではなく、閉じた状態に固定するわけです。

💡 KANONのワンポイント
UIを作るときは「切り替える」のか「必ずその状態にする」のかを分けて考えると、バグが減ります。 ボタンは開閉なので toggle()。 リンククリック後は必ず閉じたいので remove()。 小さな判断ですが、実務ではとても大切です。

スクロール連動アニメーションを動かす

番外編第1回で、私たちは fade-in-upis-visible のCSSを用意しました。 でもCSSだけでは、「画面に入ったらクラスを付ける」という判断はできません。

そこで使うのが IntersectionObserver です。 これは、要素が画面内に入ったかどうかを監視するための仕組み。 スクロール連動アニメーションでは、とてもよく使われます。

js/main.js — スクロール監視
// スクロール連動アニメーション
const fadeElements = document.querySelectorAll('.fade-in-up');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('is-visible');
      observer.unobserve(entry.target);
    }
  });
}, {
  threshold: 0.15
});

fadeElements.forEach((element) => {
  observer.observe(element);
});

threshold: 0.15 は、 要素が15%見えたら反応する、という意味です。 早すぎず、遅すぎず。 スクロール演出では扱いやすい値です。

observer.unobserve(entry.target) は、 一度表示された要素の監視をやめるための処理です。 これを入れておくと、同じアニメーションが何度も再生されず、 パフォーマンスにもやさしくなります。

📌 覚えておこう
IntersectionObserverは、スクロールイベントを直接監視するより効率的です。 昔は scroll イベントで位置を計算することも多かったのですが、 今はIntersectionObserverを使うほうが安全で読みやすい場面が増えました。

ここまでのmain.js全体

では、今回書いたJavaScriptをひとつにまとめます。 うまく動かないときは、まずこのコードと見比べてみてください。

js/main.js — 今回の完成コード
// ===================================
// café SOEL — Main JavaScript
// 番外編②:インタラクション
// ===================================

// ---------- ハンバーガーメニュー ----------

const menuToggle = document.querySelector('.menu-toggle');
const nav = document.querySelector('.nav');
const navLinks = document.querySelectorAll('.nav-link');

if (menuToggle && nav) {
  menuToggle.addEventListener('click', () => {
    menuToggle.classList.toggle('is-open');
    nav.classList.toggle('is-open');

    const isOpen = menuToggle.classList.contains('is-open');

    menuToggle.setAttribute('aria-expanded', isOpen);
    menuToggle.setAttribute(
      'aria-label',
      isOpen ? 'メニューを閉じる' : 'メニューを開く'
    );
  });

  navLinks.forEach((link) => {
    link.addEventListener('click', () => {
      menuToggle.classList.remove('is-open');
      nav.classList.remove('is-open');
      menuToggle.setAttribute('aria-expanded', 'false');
      menuToggle.setAttribute('aria-label', 'メニューを開く');
    });
  });
}

// ---------- スクロール連動アニメーション ----------

const fadeElements = document.querySelectorAll('.fade-in-up');

if (fadeElements.length > 0) {
  const observer = new IntersectionObserver((entries) => {
    entries.forEach((entry) => {
      if (entry.isIntersecting) {
        entry.target.classList.add('is-visible');
        observer.unobserve(entry.target);
      }
    });
  }, {
    threshold: 0.15
  });

  fadeElements.forEach((element) => {
    observer.observe(element);
  });
}

if (menuToggle && nav) のような条件を入れているのは、 要素が見つからなかったときにエラーで止まらないようにするためです。 小さな保険ですが、ページが増えたときにも安心です。

👀 確認してみましょう
保存したらブラウザをリロードしてください。モバイル幅でメニューが開閉し、リンクを押すと閉じる。 さらに、スクロールすると前回追加した fade-in-up 要素がふわっと表示されるはずです。

今回のまとめ

  • JavaScriptは、HTMLとCSSで作った画面に「操作への反応」を加えるための言語です
  • querySelector() で要素を探し、addEventListener() で操作を待ちます
  • classList.toggle() を使うと、クリックに合わせてクラスを付け外しできます
  • aria-expanded は、メニューが開いているか閉じているかを支援技術へ伝える大切な属性です
  • ナビリンクをクリックした後は、remove() で必ず閉じた状態にすると扱いやすくなります
  • IntersectionObserver を使うと、要素が画面に入ったタイミングでアニメーションを開始できます
  • ここまでできたなら、静的なサイトに小さなインタラクションを加える基礎はもう身についています