アンカーリンクでアコーディオンを初期表示で開く方法|JavaScript実装ガイド

FAQページを作ったけど、リンクから開いたときに、特定の質問を最初から開いた状態にしたい…
FAQページなどでは、特定の質問をピンポイントで案内したい場面がよくあります。その際、URLを開いた時点で該当のアコーディオンが開いていれば、ユーザーは探すことなく、すぐに目的の情報を確認できます。
前回の記事では、アンカーリンクをクリックしたときにアコーディオンを開く方法を紹介しました。

今回はその応用として、ページにアクセスしたタイミングで、指定したアコーディオンを開いた状態にする方法を解説します。
- スムーススクロールなどの演出は行わない(応用例として後半で紹介します)
- ブラウザ標準のアンカー挙動をそのまま利用する
- JavaScriptは、既存の開閉処理に加えて、初期表示時の状態制御を行う
初期表示でアコーディオンを開くための処理を、順を追って見ていきましょう!
なお、アコーディオンの基本的な実装については、以下の記事で解説しているので、あわせて参考にしてみてください。

完成デモの紹介
まずは、完成デモを確認してみましょう。以下のリンクから、それぞれ異なるFAQ項目を指定してページを開くことができます。

商品の発送に関するQ&Aを開いた状態で、デモページを表示
支払い方法の変更に関するQ&Aを開いた状態で、デモページを表示
アカウント統合に関するQ&Aを開いた状態で、デモページを表示
いずれのリンクでも、URLのアンカー(#)に対応した項目が、ページ表示時に自動で開いた状態になります。スクロールは制御しておらず、アンカー(#)によるブラウザのデフォルト挙動を利用しています。
コード全体(HTML / CSS / JavaScript)
まずは、今回のデモで使用しているコード全体を確認します。各コードにはポイントとなる部分にコメントを入れているので、処理の流れを意識しながら見てみてください。
HTMLコード
HTMLでは、各アコーディオン項目にidを付け、アンカー(#)で直接指定できるようにしています。初期表示の制御に関係するのは、このidの指定部分です。
<!-- FAQ本体エリア -->
<section class="faq-section">
<h2 class="faq-heading">よくある質問</h2>
<!-- 質問と回答のリスト -->
<ul class="faq-accordion">
<!--
各<li>が1つの質問・回答ブロック
アンカー(#)で直接指定できるように、各アコーディオン項目に id を付与している
※ 初期表示の制御に関係するポイント
-->
<li id="shipping">
<!-- 質問部分(クリックで開閉) -->
<div class="faq-accordion-q">
<button type="button">
<span class="faq-accordion-q-prefix">Q.</span>
<span class="faq-accordion-q-content">ダミー:商品の発送は注文から何日くらいで行われますか?</span>
<span class="faq-accordion-q-icon"></span>
</button>
</div>
<!-- 回答部分(開閉対象) -->
<div class="faq-accordion-a">
<div class="faq-accordion-a-inner">
<div class="faq-accordion-a-prefix">A.</div>
<div class="faq-accordion-a-content"><!-- 回答内容(省略) --></div>
</div>
</div>
</li>
<!-- 初期表示制御用の id -->
<li id="payment">
<div class="faq-accordion-q">
<button type="button">
<span class="faq-accordion-q-prefix">Q.</span>
<span class="faq-accordion-q-content">ダミー:支払い方法を途中で変更したい場合、どの画面から手続きをすればよいですか?</span>
<span class="faq-accordion-q-icon"></span>
</button>
</div>
<div class="faq-accordion-a">
<div class="faq-accordion-a-inner">
<div class="faq-accordion-a-prefix">A.</div>
<div class="faq-accordion-a-content"><!-- 回答内容(省略) --></div>
</div>
</div>
</li>
<!-- 初期表示制御用の id -->
<li id="account">
<div class="faq-accordion-q">
<button type="button">
<span class="faq-accordion-q-prefix">Q.</span>
<span class="faq-accordion-q-content">ダミー:複数のアカウントを1つに統合することは可能ですか?</span>
<span class="faq-accordion-q-icon"></span>
</button>
</div>
<div class="faq-accordion-a">
<div class="faq-accordion-a-inner">
<div class="faq-accordion-a-prefix">A.</div>
<div class="faq-accordion-a-content"><!-- 回答内容(省略) --></div>
</div>
</div>
</li>
</ul>
</section>
CSSコード
CSSでは、アコーディオンの開閉に関わるスタイルと、開いた状態を示すためのクラス指定を行っています。is-openクラスが付与されると、アイコンが「+」から「−」に切り替わるようになっています。
/* ================================
FAQセクション見出し
================================ */
.faq-heading {
color: #192753;
font-weight: 700;
line-height: 1.5;
text-align: center;
}
@media (min-width: 768px) {
.faq-heading {
margin-bottom: 30px;
font-size: 30px;
}
}
@media (max-width: 767px) {
.faq-heading {
margin-bottom: 25px;
font-size: 24px;
}
}
/* ================================
FAQアコーディオン(Q&A部分)
================================ */
/* 質問リスト全体 */
.faq-accordion {
list-style: none;
border-top: 1px solid rgba(25, 39, 83, 0.2);
}
/* Q. の表示(左側固定配置) */
.faq-accordion-q-prefix {
display: block;
position: absolute;
font-family: "Roboto", sans-serif;
font-weight: 500;
font-variation-settings: "wdth" 75;
line-height: 1;
}
@media (min-width: 768px) {
.faq-accordion-q-prefix {
left: 20px;
top: 24px;
font-size: 30px;
}
}
@media (max-width: 767px) {
.faq-accordion-q-prefix {
left: 10px;
top: 20px;
font-size: 24px;
}
}
/* 質問テキスト */
.faq-accordion-q-content {
display: block;
font-weight: 400;
line-height: 1.6;
}
@media (min-width: 768px) {
.faq-accordion-q-content {
font-size: 20px;
}
}
@media (max-width: 767px) {
.faq-accordion-q-content {
font-size: 16px;
}
}
/* 開閉アイコン(+/−) */
.faq-accordion-q-icon {
display: block;
position: absolute;
top: 50%;
transform: translateY(-50%);
}
@media (min-width: 768px) {
.faq-accordion-q-icon {
right: 20px;
width: 20px;
height: 20px;
}
}
@media (max-width: 767px) {
.faq-accordion-q-icon {
right: 10px;
width: 16px;
height: 16px;
}
}
.faq-accordion-q-icon::before,
.faq-accordion-q-icon::after {
content: "";
display: block;
position: absolute;
left: 0;
width: 100%;
height: 2px;
background: #333;
}
@media (min-width: 768px) {
.faq-accordion-q-icon::before,
.faq-accordion-q-icon::after {
top: 9px;
}
}
@media (max-width: 767px) {
.faq-accordion-q-icon::before,
.faq-accordion-q-icon::after {
top: 7px;
}
}
.faq-accordion-q-icon::after {
transform: rotate(90deg);
transition: transform 400ms;
}
/* 質問ボタン本体 */
.faq-accordion-q button {
display: block;
position: relative;
width: 100%;
box-sizing: border-box;
border: none;
background: none;
color: inherit;
font: inherit;
text-align: left;
appearance: none;
cursor: pointer;
transition: opacity 400ms;
}
@media (min-width: 768px) {
.faq-accordion-q button {
padding: 25px 60px 25px 57px;
}
}
@media (max-width: 767px) {
.faq-accordion-q button {
padding: 20px 36px 20px 41px;
}
}
@media (min-width: 768px) {
.faq-accordion-q button:hover,
.faq-accordion-q button:active {
opacity: 0.5;
}
}
/* 回答部分:非表示が初期状態 */
.faq-accordion-a {
display: none;
overflow: hidden; /* 開閉アニメ用 */
}
/* 回答内の余白調整(Q.との位置関係を保つ) */
.faq-accordion-a-inner {
position: relative;
}
@media (min-width: 768px) {
.faq-accordion-a-inner {
padding: 5px 0 30px 57px;
}
}
@media (max-width: 767px) {
.faq-accordion-a-inner {
padding: 5px 0 25px 41px;
}
}
/* A. の表示(左側固定配置) */
.faq-accordion-a-prefix {
position: absolute;
font-family: "Roboto", sans-serif;
font-weight: 500;
font-variation-settings: "wdth" 75;
line-height: 1;
}
@media (min-width: 768px) {
.faq-accordion-a-prefix {
left: 20px;
top: 1px;
font-size: 30px;
}
}
@media (max-width: 767px) {
.faq-accordion-a-prefix {
left: 10px;
top: 5px;
font-size: 24px;
}
}
/* 各項目の区切り線 */
.faq-accordion > li {
border-bottom: 1px solid rgba(25, 39, 83, 0.2);
}
/* 開いた状態でアイコンを−に変化 */
.faq-accordion > li.is-open .faq-accordion-q-icon::after {
transform: rotate(0deg);
}
/* ==============================
その他の装飾は本題外のため省略
============================== */
JavaScriptコード
JavaScriptでは、クリックによるアコーディオンの開閉処理に加えて、ページアクセス時にアンカー(#)を判定し、該当項目を開いた状態にする処理を実装しています。スクロールの制御は行わず、アンカーによる移動はブラウザの標準挙動に任せています。
/* =======================================
FAQアコーディオン(Qクリックで開閉)
======================================= */
document.querySelectorAll('.faq-accordion').forEach((accordion) => {
// アニメーション関連の設定値
const props = {
isAnimating: false, // 開閉アニメーション中の多重操作(連打・別項目クリックなど)を防ぐフラグ
slideDuration: 400,
slideEasing: 'cubic-bezier(0.215, 0.61, 0.355, 1)', // CSS的なイージング
};
/**
* 回答を開くアニメーション
*/
function answerShow(li) {
props.isAnimating = true;
li.classList.add('is-open');
const answer = li.querySelector('.faq-accordion-a');
answer.style.display = 'block'; // 高さ計測のため一時的に表示
const startHeight = 0;
const endHeight = answer.scrollHeight; // コンテンツ本来の高さ
// 高さを0→コンテンツ高さまでアニメーション
answer.animate([
{ height: `${startHeight}px` },
{ height: `${endHeight}px` }
], {
duration: props.slideDuration,
easing: props.slideEasing
}).onfinish = () => {
answer.style.height = ''; // 高さをautoに戻す
props.isAnimating = false;
};
}
/**
* 回答を閉じるアニメーション
*/
function answerHide(li) {
props.isAnimating = true;
li.classList.remove('is-open');
const answer = li.querySelector('.faq-accordion-a');
const startHeight = answer.scrollHeight;
const endHeight = 0;
// 高さをコンテンツ高さ→0にアニメーション
answer.animate([
{ height: `${startHeight}px` },
{ height: `${endHeight}px` }
], {
duration: props.slideDuration,
easing: props.slideEasing
}).onfinish = () => {
answer.style.display = ''; // display:none に戻る
answer.style.height = '';
props.isAnimating = false;
};
}
// 各質問ボタンに開閉イベントを設定
accordion.querySelectorAll('li').forEach((li) => {
li.querySelector('.faq-accordion-q button').addEventListener('click', () => {
if (!props.isAnimating) { // アニメ中は操作無効
if (!li.classList.contains('is-open')) {
answerShow(li); // 閉じている → 開く
} else {
answerHide(li); // 開いている → 閉じる
}
}
});
});
});
/* =======================================
初期表示時の処理(アンカー指定で開いた状態にする)
======================================= */
document.addEventListener("DOMContentLoaded", () => {
// URLの「#」以降(アンカー名)を取得
const hash = location.hash.slice(1);
// アンカー指定がない場合は何もしない
if (!hash) return;
// アンカー名と同じ id を持つFAQ項目(li)を取得
const targetLi = document.getElementById(hash);
// 対応する要素が存在しない場合は処理を中断
if (!targetLi) return;
// 対象のFAQ項目の+アイコンを−表示に切り替える
targetLi.classList.add('is-open');
// 回答部分を即時表示させる
const answer = targetLi.querySelector('.faq-accordion-a');
answer.style.display = 'block';
});
コードのポイント解説
ここでは、今回追加した「初期表示時の処理」を中心に、実装のポイントを解説します。
ポイント:初期表示時に指定のアコーディオンを開く仕組み
初期表示時の処理は、アンカー(#)をもとに対象のアコーディオンを特定し、開いた状態にするだけのシンプルな仕組みです。
1)location.hashから#を取り除いてidと一致させる
const hash = location.hash.slice(1);
if (!hash) return;
location.hashは#shippingのように#を含んだ文字列で取得されます。一方、getElementById()に渡すのはshippingのように#を除いたid名なので、slice(1)で先頭1文字を削っています。
また、アンカー指定がない場合は何もする必要がないので、早めにreturnして処理を終了します。
2)idが一致するliを取得する
const targetLi = document.getElementById(hash);
if (!targetLi) return;
HTML側では各FAQ項目にidを付けています。
<li id="shipping">...</li>
<li id="payment">...</li>
<li id="account">...</li>
そのため、URLが.../#paymentならpaymentのliが取得できます。もし存在しないアンカー(例:#foo)でアクセスされた場合に備えて、見つからなければ処理を中断しています。
3)is-openを付けて「開いている状態」にする(+→−)
targetLi.classList.add('is-open');
この処理では、対象のFAQ項目にis-openクラスを付与しています。CSS側では、このクラスが付くことで、アイコンが「+」から「−」に切り替わります。
.faq-accordion > li.is-open .faq-accordion-q-icon::after {
transform: rotate(0deg);
}
また、クリック時の開閉判定でもis-openを使っているため、初期表示でもis-openを付けておくことで状態がズレないようにしています。
4)回答部分をdisplay: block;にして中身を表示する
const answer = targetLi.querySelector('.faq-accordion-a');
answer.style.display = 'block';
CSSでは、回答部分が初期状態で非表示になっています。
.faq-accordion-a {
display: none;
}
初期表示時は、開閉アニメーションは行わず、display: block;を指定して、回答をそのまま表示しています。
なお、スクロール位置の制御は行っていません。アンカー(#)を指定したときに目的の位置まで移動するのは、ブラウザの標準挙動です。
応用例:CSSだけでスムーススクロールしたい場合
アンカー移動をスムーススクロールにしたいだけであれば、JavaScriptを使わずに、CSSだけで対応することもできます。
html {
scroll-behavior: smooth;
}
このように指定しておくと、アンカー(#)を指定したときの移動が、自動的にスムーススクロールになります。今回のように、スクロール処理をブラウザの標準挙動に任せている構成であれば、最小限の指定で簡単に導入できる方法です。
ただし、この方法にはいくつか制限があります。
- スクロールのイージング(動きの加速・減速)を細かく調整できない
- 追従ヘッダーなどがある場合に、スクロール位置を補正できない
より細かな制御が必要な場合は、JavaScriptでスクロール処理を実装する方法もありますが、このあたりは別の記事で紹介予定です。
まとめ
本記事では、ページにアクセスした時点で、指定したアコーディオンを開いた状態にする方法を紹介しました。
スクロールはアンカー(#)によるブラウザの標準挙動に任せ、JavaScriptでは初期表示の状態制御だけを行っています。
まずは最小構成で実装し、必要に応じて制御を追加していく考え方の参考になれば幸いです。
押していただけると励みになります!
