ポートフォリオサイトや管理画面でよく見かける「タブをクリックすると中身が切り替わり、下線やインジケーターがなめらかにスライドする」UI。ライブラリを使わずに React の useState と CSS の transition だけ で実装できることをご存知でしょうか。
この記事では、実際に自分のポートフォリオサイトに実装したタブ切り替えコンポーネントを題材に、
- タブの選択状態をどう管理するか
- インジケーター(下線)をなめらかに動かす CSS のロジック
- タブの中でさらにコンテンツを切り替える「入れ子構造」の作り方
react-scrollを使ったスクロール連動アニメーション
を、コードとあわせて解説します。React 初〜中級者の方で「タブUIを自作したい」「外部ライブラリに頼らず実装したい」という方の参考になれば幸いです。
この記事で作れるもの
- クリックしたタブに応じてコンテンツが切り替わる
- 選択中のタブの下に、位置がなめらかに移動するインジケーター(下線)が表示される
- タブ内のカードをクリックすると、詳細コンテンツへ自動スクロール+切り替わる
実装に使う技術
| 技術 | 役割 |
|---|---|
React(useState) | 現在選択中のタブ・コンテンツの状態管理 |
CSS transition | インジケーターやホバー時のなめらかな動き |
react-scroll | クリック時に対象要素へスムーズスクロール |
外部のタブライブラリは使わず、素の React と CSS だけで完結させているのがポイントです。ライブラリに依存しないため、デザインの自由度が高く、バンドルサイズも増えません。
1. タブの土台となるコンポーネントを作る
まずは、タブのボタン一覧と、選択中のコンテンツを表示する親コンポーネントを作成します。
jsx
import { useState } from "react";
const Tabs = ({ tabs, content }) => {
const [activeTab, setActiveTab] = useState(0);
return (
<section className="container" id="works">
<h2>作品</h2>
<div className="tab-container">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={tab.id === activeTab ? "active" : ""}
>
{tab.name}
</button>
))}
<div
className="tabBorder"
style={{ left: `${activeTab * 33.3}%` }}
></div>
</div>
<div>{content[activeTab]}</div>
</section>
);
};
export default Tabs;
ポイント:activeTab は「インデックス番号」で管理する
タブの状態は useState(0) のように 数値(インデックス) で管理するのがシンプルでおすすめです。文字列で管理するより、後述するインジケーターの位置計算がしやすくなります。
タブが3つある場合、それぞれのボタン幅を 33.3% と仮定し、activeTab * 33.3 を left プロパティに渡すことで、インジケーターの横位置を動的に決定しています。タブの数が変わる場合は 100 / tabs.length のように可変にしておくと汎用性が上がります。
jsx
style={{ left: `${activeTab * (100 / tabs.length)}%` }}
2. インジケーターをなめらかに動かすCSS
JavaScript側で位置(left)を切り替えているだけでは、瞬間的にワープしてしまいます。なめらかに「スライドしている」ように見せているのは、CSSの transition プロパティです。
css
.tab-container {
position: relative;
display: flex;
}
.tabBorder {
position: absolute;
bottom: 0;
width: 33.3%;
height: 3px;
background-color: #00bfff;
transition: left 0.4s ease;
}
left の値が変わるたびに、ブラウザが 0.4s かけて自動的に位置を補間してくれるため、JavaScript側でアニメーションのフレーム制御をする必要が一切ありません。「値の変化」と「見た目の動き」を分離できるのがCSSトランジションの強みです。
3. タブの中でさらにコンテンツを切り替える(入れ子構造)
このポートフォリオでは、「作品」タブの中にさらに個別の作品カードがあり、クリックすると詳細が下に表示される、という二重構造になっています。実装の考え方は同じで、コンポーネントごとに独立した useState を持たせるのがコツです。
jsx
const WorksApp = () => {
const [selectWork, setSelectWork] = useState(<Travel />);
const handleClick = (work) => {
setSelectWork(work);
};
return (
<div className="tab-contents">
<Link
activeClass="active"
to="move"
spy={true}
smooth={true}
offset={-70}
duration={500}
onClick={() => handleClick(<Travel />)}
className="works-item"
>
{/* カードの中身 */}
</Link>
{/* ...他のカード... */}
<>{selectWork}</>
</div>
);
};
ここでは useState の中身を数値やIDではなく、JSX要素そのものを保持しています。setSelectWork(<Travel />) のようにクリックされたコンポーネントをそのままstateに格納することで、切り替えロジックを {selectWork} の一行だけで完結させられるのが利点です。
小規模な構成であれば手軽ですが、コンポーネント数が増える場合は「IDだけをstateに持ち、switch文やオブジェクトマップで対応コンポーネントを引く」設計にした方が保守性は高くなります。
jsx
const components = { travel: <Travel />, mart: <Mart /> };
const [selectedId, setSelectedId] = useState("travel");
// ...
{components[selectedId]}
4. クリック時にスムーズスクロールさせる(react-scroll)
タブ内のカードをクリックした際、詳細コンテンツが表示される位置まで自動でスクロールする演出には react-scroll の Link コンポーネントを使用しています。
jsx
<Link
activeClass="active"
to="move"
spy={true}
smooth={true}
offset={-70}
duration={500}
onClick={() => handleClick(<Travel />)}
>
smooth:スクロールをアニメーション化duration:スクロールにかかる時間(ミリ秒)offset:固定ヘッダーの高さ分などをずらす調整値spy:スクロール位置に応じてactiveClassを自動付与
onClick でstateの更新、Link の機能でスクロール演出という2つの役割を1つのクリックイベントに同居させているのがこの実装の特徴です。
5. カードのホバーアニメーションも忘れずに
一覧表示されるカードには、ホバー時に拡大するアニメーションも加えています。こちらもJavaScriptは不要で、CSSのみで完結します。
css
.works-item {
transition: transform .4s ease;
}
.works-item:hover {
transform: scale(1.2, 1.2);
}
transition を先に指定しておくことで、:hover 時の transform の変化がアニメーション付きになります。JSでの状態管理が不要な視覚効果は、できる限りCSSに任せるのが実装コストを下げるコツです。
まとめ:ライブラリなしでタブアニメーションは実装できる
今回紹介した実装のポイントをまとめると以下の通りです。
- タブの選択状態は
useStateで インデックス番号 として管理する - インジケーターの位置は
leftの値を動的に変え、CSSのtransitionでなめらかに補間する - ネストしたコンテンツ切り替えは、コンポーネントごとに独立した
useStateを持たせて管理する - スクロール演出は
react-scrollのLinkに任せ、状態更新とスクロールを1つのクリックで両立させる - ホバーなど単純な視覚効果はJSではなくCSSの
transitionで実装する
タブUIライブラリを導入すると学習コストやカスタマイズの制約が発生しがちですが、今回のように useState と CSS の組み合わせだけでも、十分になめらかで見栄えの良いタブ切り替えを実装できます。ポートフォリオサイトや個人開発のUI実装の参考にしていただければと思います。
よくある質問
Q. タブの数が可変の場合、インジケーターの位置計算はどうすればいいですか? A. left: ${activeTab * 33.3}% のように固定値で計算するのではなく、100 / tabs.length で1タブあたりの幅を動的に算出すると、タブ数が変わっても崩れません。
Q. コンポーネントをそのままstateに入れる設計は問題ないですか? A. タブ数が少ない個人開発規模であれば問題ありませんが、再レンダリングの制御やコード分割の観点では、IDや文字列をstateに持ちオブジェクトマップで参照する設計の方が拡張性は高くなります。
Q. アニメーションが動かないときは何を確認すればいいですか? A. 親要素に position: relative、動かす要素に position: absolute が指定されているか、また transition の対象プロパティ名(left など)が実際に変化しているプロパティと一致しているかを確認してください。

コメント