メインコンテンツまでスキップ
  • Created:
  • Updated:
  • Author:
    Takeshi Takatsudo

CSS 3Dトランスフォーム

問題

開発者がカードフリップ、回転パネル、キューブなどの3Dエフェクトを試みると、原因不明のバグに遭遇します。カードの裏側が鏡像として透けて見える、回転が3Dではなく平面的に見える、エフェクト全体が2D平面に潰れてしまう、などです。これらのバグは紛らわしいものです。なぜなら、個々のプロパティ(transformbackface-visibility)は単体では正しく見えるからです。根本原因は、CSS 3Dトランスフォームが4つのプロパティ — perspectivetransform-style: preserve-3dbackface-visibilityperspective-origin — の連携したシステムを必要とし、いずれか1つでも省略や誤配置があると錯覚が壊れるということです。

解決方法

CSS 3Dトランスフォームは、各プロパティに特定の役割がある4つのプロパティのシステムとして機能します。

  1. perspective を親コンテナに設定し、3Dの視距離を確立する
  2. transform-style: preserve-3d を回転する要素に設定し、子要素が共有3D空間でレンダリングされるようにする
  3. backface-visibility: hidden をカードの各面に設定し、回転して裏向きになったときに非表示にする
  4. perspective-origin を親に設定し、消失点をずらす

4つすべてが存在し、正しい要素に配置されている必要があります。perspectiveperspective-origin(ビューイングコンテナ)に設定します。transform-style回転される要素に設定します。backface-visibility は裏向きになったときに隠すべき個々の面に設定します。

基本原則

perspective が奥行きを作る

perspective がないと、回転はフラットな2D投影で行われます。rotateY(45deg)scaleX() のように要素を水平方向に圧縮するだけです。親コンテナに perspective を追加すると、設定された距離に仮想カメラが作成され、近い端が大きく、遠い端が小さく見えるようになります。これは現実世界の遠近法と同じです。

小さい値(200〜400px)は劇的で誇張された奥行きを作ります。大きい値(800〜1200px)は繊細で自然な3Dになります。カードエフェクトのデフォルトとしては perspective: 1000px が適切です。

preserve-3d が子要素を3D空間に保つ

デフォルトでは、CSSはトランスフォームされた子要素を親の2D平面にフラット化します。これが transform-style: flat です。表と裏のあるカードを構築する場合、両面が同じ3D空間に存在する必要があります。共有の親(カードラッパー)に transform-style: preserve-3d を設定すると、このフラット化が防止され、180deg 回転した子要素が同じ平面で重なるのではなく、実際に兄弟要素の背後に位置するようになります。

backface-visibility が裏面を隠す

すべてのHTML要素には表面と裏面があります。デフォルトでは裏面が表示されます。表面の鏡像としてレンダリングされます。カードフリップの場合、これは表と裏のコンテンツが同時に透けて見えることを意味します。各カード面に backface-visibility: hidden を設定すると、90度を超えて回転したときに非表示になり、視聴者に向いている面だけが表示されます。

perspective-origin が消失点をずらす

perspective-origin は、親コンテナに対する視聴者の目の位置を制御します。デフォルトは 50% 50%(中央)です。top left にずらすと、左上の要素が近くに見え、右下の要素がより後退して見えます。傾いたカードのグリッドでオフセンターの視点が必要な場合に便利です。

ライブプレビュー

ホバーでカードフリップ

クラシックなカードフリップは4つのプロパティすべてを使います。親が perspective を提供し、カードラッパーが preserve-3d を使ってホバー時に回転し、各面が backface-visibility: hidden を使って正面を向いているときだけ表示されます。

ホバーでカードフリップ

パースペクティブの比較

小さい perspective 値は極端な遠近感を作り、大きい値はより繊細な効果を生みます。以下の2つのパネルはどちらも同じ rotateY(40deg) トランスフォームです。親の perspective 値だけが異なります。

パースペクティブ: 低い値 (200px) vs 高い値 (1000px)

3Dキューブ

CSSキューブは translateZrotateX/rotateY で配置された6つの面を使います。親が perspective を提供し、キューブラッパーが preserve-3d を使います。ホバーでキューブを回転させます。

ホバー回転する3Dキューブ

パースペクティブオリジン

perspective-origin は視聴者の目の位置をずらします。以下では、同じ傾いたカードのグリッドを3つの異なる視点から見ています。各グループにホバーすると、傾いたカードの遠近感がどのように異なるかを確認できます。

パースペクティブオリジン: 左上 vs 中央 vs 右下

モーション軽減対応のフリップカード

本番用のカードフリップは prefers-reduced-motion を尊重すべきです。ユーザーがモーション軽減を好む場合、カードは3D回転の代わりにオパシティによるクロスフェードで表と裏を切り替えます。

モーション軽減対応のアクセシブルなカードフリップ

AIがよくやるミス

  • 親に perspective がないperspective を親のスタンドアロンプロパティではなく、要素自体の transform ショートハンドの一部として適用してしまう。transform 内の perspective() 関数はその単一の要素にのみ影響し、子要素には影響しません。
  • transform-style: preserve-3d を忘れる — これがないと、子要素は親の2D平面にフラット化されます。カードフリップの構造は正しく見えますが、両面が同じ平面にレンダリングされ、回転するのではなく重なります。
  • backface-visibility を間違った要素に配置する — 回転ラッパーではなく、個々の面に設定する必要があります。ラッパーに設定すると、各面を選択的に隠すのではなく、カード全体が裏返ったときに隠れてしまいます。
  • 裏面を事前に回転していない — 裏面は初期状態で transform: rotateY(180deg) が必要です。これにより、最初は視聴者から離れた方向を向き、ラッパーが回転したときに表示されるようになります。
  • preserve-3d コンテナに overflow: hidden を使うoverflow: hiddentransform-style: flat を強制し、3Dエフェクトを暗黙的に壊します。クリッピングが必要な場合は、preserve-3d を使わない外側のラッパーに適用しましょう。
  • prefers-reduced-motion を無視する — 3D回転は動揺を引き起こす可能性があります。本番のカードフリップは、ユーザーがモーション軽減を好む場合にオパシティのクロスフェードにフォールバックすべきです。

使い分け

  • カードフリップ — ホバーやクリックで追加情報を表示する商品カード、フラッシュカード、プロフィールカード
  • 3Dショーケース — 画像ギャラリーやフィーチャーハイライト用の回転キューブやプリズム
  • パースペクティブティルト — カーソルに向かって少し傾くホバーエフェクトで触覚的な感触を出す
  • ヒーローセクション — 大きな perspective 値を使ったスクロール時のドラマチックな3D登場アニメーション

注意点

  • perspectiveperspective-origin は回転要素ではなくに設定する必要があります
  • preserve-3d 要素に overflow: hidden を設定すると、暗黙的に transform-style: flat に戻ります
  • backface-visibility: hidden は、祖先に transform-style: preserve-3d がないと効果がありません
  • ネストされた preserve-3d 要素は perspective を合成し、予期しない歪みを生む可能性があります
  • Mobile Safariには歴史的に preserve-3d のバグがあります。実際のiOSデバイスでテストしましょう

参考リンク