🍆

【React | Next.js】あまり面倒じゃない方法で開閉アニメーション付きアコーディオンを作る

2023/08/23に公開

2023-08-28追記-->
max-heightもheightも変わらんくね...?
なんでタイトルでイキってしまったのだろうか・・・。
<--追記終了

開閉アニメーションの付いたアコーディオンって実装面倒くさいですよね......。
っていうのは実は思い込みで、 max-heightCSS Variables を組み合わせれば超簡単に実装できます。

※動作確認はWindows各種ブラウザとAndroid Chromeのみです。試した方がいれば、コメ欄でSafariの挙動バグが無いか教えていただけると嬉しいです!

完成イメージ

CodeSandboxでサンプルを見る

※なぜか埋め込みできなかったのでリンクで失礼します。

※この記事のコードでは不要なスタイルを取り除いているので、この見た目通りにはならないかと思います。

必要なライブラリをインストールする

npm install clsx react-resize-detector
  • clsx: JSX内でclass名の分岐が簡単に記述するために使います。
  • react-resize-detector: リサイズ処理をお任せするために使います。

【本題】開閉アニメーション付きのアコーディオンを書く

Accordion.tsx
import { useState } from "react";
import clsx from "clsx";
import { useResizeDetector } from "react-resize-detector";

// アコーディオンのコンテンツの型
export interface ContentsProps {
  question: string;
  answer: string;
}

// 描画用のコンポーネント
const Accordion = () => {
  const contents: ContentsProps[] = [
    { question: "質問1", answer: "質問1の回答です。" },
    { question: "質問2", answer: "質問2の回答です。" },
  ];

  return (
    <div>
      <AccordionContainer contents={contents} />
    </div>
  );
};

// アコーディオンの項目を縦並びで整列しておく。
const AccordionContainer = ({ contents }: { contents: ContentsProps[] }) => {
  return (
    <div className="flex flex-col">
      {contents.map((theItem) => (
        <AccordionItem key={theItem.question} item={theItem} />
      ))}
    </div>
  );
};

const AccordionItem = ({ item }: { item: ContentsProps }) => {
  const [isOpen, setIsOpen] = useState(false); // 【状態】開閉を記憶
  const { height, ref } = useResizeDetector(); // react-resize-detector

  return (
    <dl className="overflow-hidden bg-white">
      <dt
        className="flex items-center cursor-pointer select-none"
        onClick={() => {
          setIsOpen(!isOpen);
        }}
      >
        <span>{item.question}</span>
      </dt>
      <dd
        style={{ "--content-height": height + "px" } as React.CSSProperties}
        className={`
	  overflow-hidden transition-[max-height] duration-500 ease-out-expo
	  ${clsx(isOpen === true ? "max-h-[var(--content-height)]" : "max-h-0")}
	`}
      >
        <div ref={ref}>
          <p className="border-t border-gray">{item.answer}</p>
        </div>
      </dd>
    </dl>
  );
};

export { Accordion };
globals.css
[hidden="until-found"] {
  @apply block;
}

たったこれだけで transition付きのアコーディオンが実現可能です。

※ ease-out-expo はtailwind.config.tsにて transitionTimingFunctionに "out-expo": "cubic-bezier(0.16, 1, 0.3, 1)", を追加しています。

実装のポイント

max-height の transition でアニメーションを行っています。
Tailwind CSSで言うと、max-h-0 から max-h-[var(--content-height)] に切り替わることでアニメーションが動くという仕組みです。

CSS変数への値の登録は style={{ "--content-height": height + "px" } as React.CSSProperties} で行っています。

コンテンツの高さは padding を含めたいので、空divに ref={ref} を付けて取得しています。

max-heightは神!!

Discussion