はじめに
記事一覧やWorksカードでは、画像を切り替えるたびに外部画像の準備が必要になることがあります。 ただ、UIの構造やアニメーションの検証段階では、実画像よりも「同じ比率で扱える仮のビジュアル」がある方が手早く試せます。
この記事では、添付画像や外部画像を使わず、React内で生成したSVG画像をカードUIに流し込みます。 見せ場は、選択した1枚だけを大きなプレビューへ差し替える瞬間です。
今回作るもの
今回は、画像カードの一覧と、大きなプレビューを持つ小さなUIパーツを作ります。
画像は data:image/svg+xml としてReact側で生成し、選択状態はReact stateで管理します。
- 今回はこれ以外: 一覧カードが順番に現れてhoverで吸い付く構成、フルスクリーン画像ギャラリー、外部画像を使ったループ演出
- 今回の主役: React stateで選択した画像を、GSAPでプレビューへなめらかに差し替える処理
- 最小単位: 3つの生成画像から1つを選び、1枚の大きなプレビューだけを更新する
- 差分: hover magneticやスクロールではなく、click / keyboard selectionをトリガーにする
- 再利用先: Worksカード、記事一覧、プロダクト比較、デザイン案のプレビュー、CMSの画像選択UI
- 分離する責務: React state、生成画像データ、選択ボタン、プレビュー、GSAPの差し替えアニメーション、CSSレイアウト
コンポーネント設計
Demo は選択中のIDだけを持ちます。
画像の生成やボタンの見た目まで Demo に詰め込むと、あとでCMSデータへ差し替えるときに触る範囲が広くなります。
FocusPreview は大きな画像とコピーの表示だけを担当します。
VisualSelector は選択ボタンの一覧だけを担当します。
createGeneratedImage() はSVG文字列をdata URLへ変換する役割に閉じています。
この分け方にしておくと、生成画像を本番画像へ置き換える場合も、VISUALS の src を持たせるだけで済みます。
アニメーションの対象も .focus-preview-image と .focus-preview-copy に限定できるため、カード一覧のDOMに副作用が広がりません。
実装のポイント
生成画像は、SVGを文字列として組み立てて encodeURIComponent() でdata URLにします。
外部ファイルを待たずに同じ比率の画像を作れるので、レイアウト検証や記事デモに向いています。
プレビューの切り替えでは、Reactの key={item.id} を画像に付けています。
これにより選択が変わるたびに画像要素が新しくなり、GSAPの fromTo() を同じ初期状態から走らせられます。
clipPath は左右を少し閉じた状態から全体表示へ戻します。
単純なフェードだけよりも、画像が差し込まれたことが分かりやすく、カード選択UIの反応としても軽く見えます。
ReactでGSAPを使う部分は gsap.context() で rootRef の内側に限定します。
記事詳細から別ページへ移動したり、選択状態が変わってeffectが再実行されたりしても、context.revert() で前回分を片付けられます。
使いどころとカスタマイズ
この部品は、画像を主役にした小さな選択UIに向いています。 たとえば、Works詳細の別案切り替え、記事サムネイルのプレビュー、プロダクトカラーの見せ分けなどです。
印象を調整するなら、まず duration と clipPath の初期値を触ります。
duration を短くすると管理画面寄りの反応になり、長くするとポートフォリオ寄りの余韻が出ます。
clipPath の 18% を大きくすると、よりマスク演出らしくなりますが、頻繁に切り替えるUIでは少し重く見えます。
サムネイル側はCSSの data-active で状態を表現しています。
React stateをDOM属性へ落としておくと、JSを増やさずに選択中の見た目を調整できます。
発展させるなら
本番では、createGeneratedImage() を外してCMSやAPIの画像URLを渡す形にできます。
その場合も、プレビュー、セレクター、アニメーションの責務を分けておけば、構造はほとんど変えずに済みます。
さらに発展させるなら、選択中の画像だけではなく、隣の候補画像を薄く重ねて奥行きを作る方法があります。 ただし、この記事の主役は「生成画像を使った選択プレビュー」なので、まずは1枚の差し替えだけに絞るのが扱いやすいです。