【React | Next.js】あまり面倒じゃない方法で開閉アニメーション付きアコーディオンを作る
2023-08-28追記-->
max-heightもheightも変わらんくね...?
なんでタイトルでイキってしまったのだろうか・・・。
<--追記終了
開閉アニメーションの付いたアコーディオンって実装面倒くさいですよね......。
っていうのは実は思い込みで、 max-height
と CSS Variables
を組み合わせれば超簡単に実装できます。
※動作確認はWindows各種ブラウザとAndroid Chromeのみです。試した方がいれば、コメ欄でSafariの挙動バグが無いか教えていただけると嬉しいです!
完成イメージ
※なぜか埋め込みできなかったのでリンクで失礼します。
※この記事のコードでは不要なスタイルを取り除いているので、この見た目通りにはならないかと思います。
必要なライブラリをインストールする
npm install clsx react-resize-detector
- clsx: JSX内でclass名の分岐が簡単に記述するために使います。
- react-resize-detector: リサイズ処理をお任せするために使います。
【本題】開閉アニメーション付きのアコーディオンを書く
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 };
[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