【Next.js】FAQ アコーディオンメニューの作り方を開閉アニメーションまで解説

公開日: 2026年9月11日 · 読了目安 14 分

Next.js の LP に、よくある質問(FAQ)のアコーディオンメニューを置く手順です。details と summary だけの開閉、1つだけ開く排他制御、grid を使った滑らかな開閉アニメーション、FAQPage 構造化データの付け方まで、コード付きで解説します。

FAQ をアコーディオンメニューにして縦長を防ぐ

Next.js で LP を作っていて、料金表の下によくある質問を置いた。すると答えまで全部表示されて、ページが縦に長くなりすぎた。

そんなときの定番の解決策は、アコーディオンメニューです。質問だけを並べ、押した質問の答えだけを開きます。

作り方の答えは次のとおりです。最初は、HTML に標準である開閉の部品(details と summary)だけで作れます。

JavaScript は要らず、サーバーで組み立てる部品(サーバーコンポーネント)のまま置けます。

1つだけ開く動きや滑らかな開閉アニメーションが欲しくなったら、FAQ のファイルだけをブラウザで動く部品(クライアントコンポーネント)にします。

この記事で分かることは次のとおりです。

  • HTML 標準の開閉部品で作る、いちばん簡単な FAQ アコーディオンメニュー

  • 1つ開くと他が閉じる「排他的」なアコーディオンの作り方(2通り)

  • 高さが分からない答えを、滑らかに開閉するアニメーション

  • FAQPage 構造化データの付け方と、SEO で気をつけること

FAQ をアコーディオンメニューにする理由

よくある質問は、LP の中で「最後の不安を消す」場所です。

読者は料金を見たあと、申し込む直前に「解約はいつでもできるのか」「無料で試せるのか」を確かめに来ます。

そのとき、自分の質問がここにあるかを1秒で判断できることが大切です。

答えまで全部開いたままだと、8問で画面数枚分の長文になります。

読者は自分の質問を探す前に疲れてしまい、問い合わせボタンまで届きません。

質問だけが並んでいれば、一覧として流し読みできます。気になる1問だけを開けばよくなります。

そのため、閉じた行に書く質問は短くします。

「当サービスのご利用にあたって多く寄せられるお問い合わせについて」は、見出しではなく前置きです。

「無料で試せますか」「解約はいつでもできますか」のように、訪問者が口にする言葉で書きます。

Next.js で FAQ アコーディオンを作る3つの方法

同じ FAQ でも、必要な動きによって手数が変わります。手数の少ない順に3段階です。

  • Step1: HTML 標準の開閉部品で開閉する(JavaScript なし、複数同時に開く)

  • Step2: 1つだけ開く排他的なアコーディオンにする(属性を1つ足すか、開いている番号を覚える)

  • Step3: 開閉に滑らかなアニメーションを付ける(ブラウザで動かす印の "use client" が必要)

迷ったら Step1 で5問ほど公開してください。同時に開くのが気になったら Step2、開くときの動きを整えたくなったら Step3 に進みます。

Step1:details で FAQ アコーディオンを作る

details と summary は、ブラウザ標準の開閉部品です。キーボード操作と、スクリーンリーダーへの開閉状態の伝達が最初から付いてきます。

サーバーで組み立てる部品のまま置けて、答えのテキストも最初から HTML に含まれます。

components/Faq.tsx
const items = [
    { q: "無料で試せますか?", a: "14日間はすべての機能を無料で使えます。カードの登録は不要です。" },
    { q: "解約はいつでもできますか?", a: "設定画面からいつでも解約できます。次の請求日以降は課金されません。" },
    { q: "請求書払いに対応していますか?", a: "Team プランのみ対応しています。お問い合わせからご相談ください。" },
  ];

  export function Faq() {
    return (
      <section className="mx-auto max-w-2xl px-6 py-24">
        <h2 className="text-3xl font-semibold">よくある質問</h2>
        <div className="mt-10 divide-y divide-zinc-200 border-y border-zinc-200">
          {items.map((item) => (
            <details key={item.q} className="group py-2">
              <summary className="flex cursor-pointer list-none items-center justify-between gap-4 py-3 font-medium [&::-webkit-details-marker]:hidden">
                {item.q}
                <span aria-hidden="true" className="transition-transform group-open:rotate-45">+</span>
              </summary>
              <p className="pb-3 text-sm leading-relaxed text-zinc-600">{item.a}</p>
            </details>
          ))}
        </div>
      </section>
    );
  }

標準の三角マークを消して、自分のアイコンにする

質問の行には、ブラウザ標準の三角マークが付きます。このマークは list-none で消えます。

Safari では、[&::-webkit-details-marker]:hidden も必要です。

代わりに右端に + のアイコンを置き、Tailwind の group-open:rotate-45 で開いたときに × に回します。

FAQ の質問は1行、答えは1〜3文に収める

質問の行に2文入れると、閉じた一覧が読みにくくなります。質問は短い1文、答えは1〜3文が目安です。

リンクが必要なら答えの中に置きます。答えが長くなる質問は、別ページのヘルプに分けた方が読みやすくなります。

Step2:1つだけ開く FAQ アコーディオンにする

Step1 では、いくつでも同時に開けます。LP の FAQ では「1問読んだら他は閉じる」方が、目線が1つの答えに残ります。

1つだけ開く「排他的」なアコーディオンにする方法は2つあります。

方法A:details に同じ name を付ける

details に同じ name 属性を付けると、ブラウザが「同時に1つだけ開く」動きにしてくれます。JavaScript は要りません。

Chrome・Safari・Firefox の新しいバージョンで使えます。対応していない古いブラウザでは、Step1 と同じく複数開くだけなので、壊れることはありません。

components/Faq.tsx(details の部分だけ変更)
<details key={item.q} name="faq" className="group py-2">
    {/* summary と答えは Step1 と同じ */}
  </details>

方法B:開いている番号を state に持つ

開いている質問の番号を1つだけ state(部品が覚えておく値)に持ち、同じ質問をもう一度押したら閉じます。

古いブラウザでも確実に1つだけ開き、開閉のアニメーションも自由に付けられます。この形は次の Step3 のコードにまとめています。

キーボードの矢印キーで質問を移動する操作まで揃えたい場合は、shadcn/ui の Accordion(Radix UI がもと)を使うのも早い方法です。

部品としての開閉はこちらで足ります。見出しとの2カラムや背景などのセクションの見た目は、自分で組みます。

Step3:FAQ アコーディオンに開閉アニメーションを付ける

答えの高さは、文章の長さで変わります。そのため、高さ 0 から「自動の高さ」へは CSS の transition(なめらかに変化させる指定)が効きません。

そこで答えを grid で包み、行の高さの指定(grid-template-rows)を 0fr から 1fr に変えます。中身の高さが分からなくても、滑らかに開閉できます。

components/FaqAccordion.tsx
"use client";

  import { useId, useState } from "react";

  type Item = { q: string; a: string };

  export function FaqAccordion({ items }: { items: Item[] }) {
    const [open, setOpen] = useState<number | null>(0); // 最初の1問だけ開いておく
    const baseId = useId();

    return (
      <div className="divide-y divide-zinc-200 border-y border-zinc-200">
        {items.map((item, i) => {
          const isOpen = open === i;
          const buttonId = baseId + "-q-" + i;
          const panelId = baseId + "-a-" + i;
          return (
            <div key={item.q}>
              <h3>
                <button
                  id={buttonId}
                  type="button"
                  aria-expanded={isOpen}
                  aria-controls={panelId}
                  onClick={() => setOpen(isOpen ? null : i)}
                  className="flex min-h-11 w-full items-center justify-between gap-4 py-4 text-left font-medium"
                >
                  {item.q}
                  <span
                    aria-hidden="true"
                    className={"transition-transform duration-300 " + (isOpen ? "rotate-45" : "")}
                  >
                    +
                  </span>
                </button>
              </h3>
              <div
                id={panelId}
                role="region"
                aria-labelledby={buttonId}
                inert={!isOpen}
                className={
                  "grid transition-[grid-template-rows] duration-300 ease-out motion-reduce:transition-none " +
                  (isOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]")
                }
              >
                <div className="overflow-hidden">
                  <p className="pb-4 text-sm leading-relaxed text-zinc-600">{item.a}</p>
                </div>
              </div>
            </div>
          );
        })}
      </div>
    );
  }

コードのポイント

  • 行の高さの割合を 0 と 1 で切り替え、中の箱ではみ出した部分を隠す。これで高さが自動のまま開閉が動く

  • 閉じた答えも HTML に残るので、検索エンジンにもスクリーンリーダーにも内容が届く

  • inert を付けると、閉じた答えの中のリンクにキーボードで入らなくなる(React 19 から使える属性。React 18 では閉じたときに hidden を付ける方法に替える)

  • 動きを減らす設定にしている人には、アニメーションを止める指定も入れている

質問が6問を超える場合は、各回答の role="region" を外しても構いません。

質問をボタンにして開閉の状態を伝える

div に onClick を付けただけの開閉は、キーボードで操作できません。開いているかどうかも読み上げられません。

質問はボタン要素(button)にします。aria-expanded と aria-controls で、開閉の状態と答えの領域を結びつけます。

ボタンを見出し(h3)で包むと、見出しの一覧からも質問をたどれます。

Motion で FAQ アコーディオンを動かしたい場合

Motion(旧 Framer Motion)を使っているプロジェクトなら、AnimatePresence でも同じ動きを作れます。height: "auto" へアニメーションさせる方法です。

ただし閉じたときに答えが DOM から消えます。検索エンジンに答えを読ませたいなら、上の grid の方法が安心です。

Motion の基本は https://design-layer.com/ja/articles/nextjs-framer-motion-guide で解説しています。

FAQ アコーディオンの構造化データと SEO の注意点

よくある質問には、schema.org の FAQPage という構造化データを付けられます。

Next.js では、画面に出している質問の配列から JSON-LD を作り、script タグで出力します。画面と構造化データで同じ配列を使えば、内容がずれません。

components/FaqJsonLd.tsx
type Item = { q: string; a: string };

  export function FaqJsonLd({ items }: { items: Item[] }) {
    const jsonLd = {
      "@context": "https://schema.org",
      "@type": "FAQPage",
      mainEntity: items.map((item) => ({
        "@type": "Question",
        name: item.q,
        acceptedAnswer: { "@type": "Answer", text: item.a },
      })),
    };

    return (
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{
          __html: JSON.stringify(jsonLd).replace(/</g, "\\u003c"),
        }}
      />
    );
  }

FAQ のリッチリザルトは 2026年5月に廃止された

Google は 2023年8月に、FAQ のリッチリザルト(検索結果の折りたたみ)の表示を、政府や医療機関のサイトに絞りました。

さらに 2026年5月7日からは、すべてのサイトで表示されなくなっています。出典は Google 検索セントラルの更新履歴です(https://developers.google.com/search/updates )。

FAQPage の構造化データを置くこと自体は問題ありません。ただし、検索結果に FAQ の表示は出ません。

付けるかどうかは、内容を機械に正しく伝える補助として判断してください。

閉じていても答えは HTML に入れておく

標準の開閉部品や Step3 の方法なら、閉じた答えも最初から HTML に含まれます。

開いたときだけ答えを描画する作りにすると、検索エンジンに答えが読まれない場合があります。また、画面に出していない質問を JSON-LD だけに書くのは避けてください。

FAQ アコーディオンでよくある失敗と公開前チェック

  • 質問を社内用語で書く。「プランのクォータは?」より「1日に何回使えますか?」の方が、検索してきた人の言葉に近い

  • 答えで新しい売り文句を書く。FAQ の役目は不安を消すことで、ヒーローのコピーの再掲ではない

  • 20問並べる。LP では読まれない。実際に聞かれることだけ残し、残りはヘルプページへリンクする

  • 最初から全部開いておく。開いておくなら、料金や無料期間など一番多い不安の1問だけにする

  • スマホでタップ領域が狭い。質問の行の高さは 44px 以上にする

  • page.tsx 全体に "use client" を書く。FAQ のファイルだけに切り出す

置き場所は、料金表のあと、最後の申し込みボタンやフッターの前が扱いやすい位置です。料金表の作り方は https://design-layer.com/ja/articles/nextjs-pricing-table-guide で解説しています。

完成した FAQ セクションをコピーして使う方法

DesignLayer の「ナンバー付きFAQパネル」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる
DesignLayer の「ナンバー付きFAQパネル」の詳細ページ。左でライブプレビューを確かめ、右の「Cursor 用にコピー」でソースをコピーできる

次のようなセクション全体の見た目まで作り込むと、Step3 の開閉を書いたあとも調整に時間がかかります。

  • 左に見出し、右にアコーディオンを置く

  • 暗い背景にする

  • 料金表のあとに自然に続ける

その場合は、完成した FAQ セクションをブラウザで実際に開閉して確かめる方法もあります。React と Tailwind のコードを、そのまま自分の Next.js に載せます。

DesignLayer のカタログでは、ライブプレビューで動きを見てから、Copy for Cursor で AI エージェントに渡せます。

閲覧にカードは不要で、無料アカウントでは、無料サンプル10個を無料アカウントで回数制限なくコピーでき、動画プロンプトは月1回です。Free では MCP に接続できません。カタログ全体は30日ビルドパス($19の1回払い、自動更新なし)、Lifetime Access(先着100名の$79の1回払い)、Plus(月額 $9、いつでも解約可)です。カタログは https://design-layer.com/ja/components から開けます。

載せたあとの仕事は、質問・答え・見出しの差し替えです。SaaS やツールの LP に向いています。

100問を超えるサポートや、ログイン後のヘルプには、検索付きのドキュメントの方が向いています。

FAQ アコーディオンについてよくある質問

  • 最初は、HTML 標準の開閉部品(details と summary)だけで作れます。ライブラリは要りません。

    1つだけ開く動きなら、name 属性を足すだけです。開閉のアニメーションまで付けるなら、FAQ 用にブラウザで動く部品を1つ作ります。

まとめ:Next.js で FAQ アコーディオンを作る手順

  • FAQ はアコーディオンメニューにすると、質問を流し読みでき、ページが縦に伸びない

  • 最初は HTML 標準の開閉部品だけで作れる。JavaScript もライブラリも要らない

  • 1つだけ開くなら、同じ名前の属性を付けるか、開いている番号を覚えておく

  • 開閉アニメーションは、答えを包む行の高さを変えて作る

  • FAQPage の構造化データは画面と同じ配列から作る。FAQ のリッチリザルトは 2026年5月に廃止され、どのサイトでも出ない

まずは Step1 のコードに、自分のサービスで実際に聞かれる質問を5問入れてみてください。FAQ の後に置く申し込みボタンの作り方は https://design-layer.com/ja/articles/nextjs-cta-section-guide で解説しています。

関連ガイド