🃏

CSSでトランプみたいにカードをカッコよく重ねて並べた~い!

2024/05/18に公開

CSS でトランプゲームのようにカードを重ねて並べたくて、地味に試行錯誤したので記録を残しておきます。
Switch の 世界のアソビ大全51 のトランプゲームでの UI を参考にしています。

  1. 1 列に並べるパターン
  2. アーチ状に広げるパターン

の 2 種類のサンプルを作りました。
また、「ホバーしたカードを強調させる演出」なんかも試してみました。

このサンプルがすべてではありますが、以降で詳しく説明していきます。

カードを作る

まずはカードを作っておきます。

  • width を固定値で設定
  • 比率を保つために aspect-ratio を設定
    • 今回はブリッジサイズと呼ばれる一般的なトランプのサイズ、 58mm x 89mm の比率を採用 [1]
  • 後は適当にスタイルをつける(今回は並べ方がメインなので、カードのデザインはあまり気にしません)
カードのスタイル
div {
  width: 6rem;
  aspect-ratio: 58 / 89;
  background-color: #c00606;
  box-shadow: 0 0 6px black;
  border: 0.5rem white solid;
  border-radius: 0.5rem;
}

要素を少しずつ重ねて並べる

スクリーンショット

HTML 要素は基本的に要素の大きさを保って縦積み・横積みされるため、重ねるにはひと工夫が必要です。

今回は以下の条件を想定します。

  • コンテナ(親要素)の幅に収まるように、枚数に応じて重なり幅を変えて配置する
  • 最小の重なり幅を設定する(カード枚数が少ないときでも多少重なるようにする)

HTML 構造としては以下のように、li の中に div(カード)を配置します。
(フレックスアイテムと widthを設定したカードの要素が、異なる要素であることが重要です)

HTML
<ul>
  <!-- 
    フレックスアイテムとカード自体が親子になるように、
    liの中にdiv(カード)を配置する
   -->
  <li><div></div></li>
  <li><div></div></li>
  <li><div></div></li>
</ul>

flex レイアウトを使用することで、子要素の幅が狭まるように設定します。
ただし、親を flex にするだけでは li が狭くなってくれません。div(カード)の大きさを保とうとしてしまうためです。
そこで limin-width: 0; を設定して、縮むようにしてあげます。
すべての子要素が縮むようにしてしまうと、最後の 1 個が親要素の範囲からはみ出してしまうので、最後の 1 個だけは幅を保持するようにします。

CSS
ul {
  display: flex;
  justify-content: center;
  max-width: 24rem;
}

li:not(:last-child) {
  /* :not(:last-child) に適用することで、基本的にすべて縮めて、最後の1枚だけ幅を保持する */
  min-width: 0;
  /* 最小の重なり幅。枚数が少ないときでもこの幅は重ねる */
  margin-right: -2rem;
}

以下のように最後の 1 枚だけがカードの幅をそのまま保持し、それ以外は均等に狭い幅が割り当てられます。
わかりやすさのために、一旦枠だけ確認すると以下のスクリーンショットのような配置になります。

スクリーンショット

カードには幅を固定値指定しているため、 li の領域をはみ出して配置され、結果として重なって配置されます! 🎴🎴🎴

ホバーしたときにカードを強調する

ホバーしたときに、カードを抜き出して表示する演出を追加してみます。

  • 他のカードを押しのけて、見える領域を大きくする
  • 少し上に移動させる

ホバー時のスタイル
li {  
  /* ホバー時アニメーション */
  transition-property: flex-shrink, translate;
  transition-duration: 0.2s;
  /* ホバーしたカードが前面に出ないようにする */
  isolation: isolate;
}

li:hover {
  flex-shrink: 0;
  translate: 0 -1rem;
}

他のカードを押しのけて、見える領域を大きくする」ために、flex-shrink0 に設定します。
これは min-width: 0; で縮めた幅を元に戻すことで、このカードに割り当てられる領域を広げるためです。
min-width: auto には transition が効かないため、flex-shrink を使っています。

少し上に移動させる」ために、translate を使っています。
translate (transform)によってスタッキングコンテキストが生成されて、他のカードよりも前面に表示されてしまいます。これを避けるため、すべての liisolation: isolate; を設定してあらかじめすべての li にスタッキングコンテキストを生成しておきます。
(逆に、前面に表示したい場合はこれを削除すれば OK です)

アーチ状に広げる

CSSで作られたカードが6枚配置されたスクリーンショット。1枚ずつ角度をずらして配置されている

アーチ状に広げるには、三角関数(sin, cos)を使います。
CSS の三角関数の活用は ICS MEDIA さんの記事が参考になります。
基本的な使い方の説明は以下の記事を参照してください。

https://ics.media/entry/230126/

それぞれのカードをどのくらい傾けるか計算するため、「全体が何枚であるか」と「それぞれが何枚目であるか」の情報が必要になるので、CSS 変数を与える必要があります。
HTML 上で各要素に style 属性で渡します。
(テンプレートエンジンや JS フレームワークを使っている場合はループ変数を使って渡すと良いでしょう。)

HTML(CSS変数を使って枚数を渡す)
<ul style="--length: 3;">
  <li><div style="--index: 0;"></div></li>
  <li><div style="--index: 1;"></div></li>
  <li><div style="--index: 2;"></div></li>
</ul>

CSS では以下のように、CSS 変数を使って計算します。

CSS
div {
  /* アーチ状に並べる */
  --degree: calc((var(--index) - ((var(--length) - 1) / 2)) * 6deg);
  rotate: var(--degree);
  translate: 0 calc((1 - cos(var(--degree))) * 300%);
}

--degree は、そのカードをどのくらい傾けるかを計算しています。
(var(--length) - 1) / 2 で中央のカードのインデックスを求めています。
それを引いているので、中央のカードが 0deg になり、端に行くほど傾きが大きくなります。
カードが 1 枚並ぶごとに 6deg ずつ傾けています(カードの間隔に合わせて目分量で設定)。

傾けるだけではアーチ状にならないので、外側に行くほど下にずらす必要があります。
ここで三角関数でどれだけずらすか計算します。

カードを円周上に配置することを想定し、直線上に並べたときの y 方向の位置との差を計算して、y 方向にtranslate する距離を求めます(厳密には x 方向にもずらすべきですが、そこまで見た目に影響ないのでここでは無視します)。
単位円を想定すれば、ずらす距離は 1 - cos(var(--degree))(下図の 1-cos(\theta))で求められます。
下の図は計算式に至るイメージ図です。 θ--degree に対応します。

計算式を説明した図。中央にまっすぐ配置するカードを基準として、θ°傾けるカードは、下に1-cos(θ)ずらせばよいことを示している。

円の半径をカードの大きさに適したサイズとするため、最後に 300%(カードの縦幅の 3 倍)をかけます(目分量で 300% という適当な値を入れています)。

まとめ

CSS でトランプのようにカードを重ねて並べる方法を紹介しました。
よければ Web サイトの表現や演出で使ってみたりしてください 🃏🎴🃁

脚注
  1. https://www.oasis-shop.co.jp/other/trump02.html ↩︎

GitHubで編集を提案

Discussion