Blog

Blog

見た目の良いカードを作る

公開日
2025-01-26

横並びにした時に高さが揃うカードを作成しました。

作るもの

横並びの時にカードの高さ、要素の位置の高さを合わせるカードを作成しました。

カード内の要素ですらばらけると見た目が悪いと思っちゃいます。そのために作成しました。そうです自己満です。

使用技術とか

  • TypeScript
  • React
  • TailwindCSS
  • clsx

とりあえずお勉強

さて、今回のようにタイトル、説明、ボタンの位置すら合わせる場合それぞれの要素の高さを合わせる必要があります。
そこで私の脳みそがはじき出した方法はゴリ押し高さ指定です。我ながら素晴らしい脳みそです。
しかし、今回は表示するデータをCMSから取ってくるという性質上あまりよろしくありません。文章が一定の長さであることを保証できないためですね(表示する要素に文字数等で制約を設けるというのも考えましたが)。というか綺麗な実装じゃないですよね。

そんなこんなでネットサーフィンしていた所、見つけました。SubGridです。
超簡単に言いますと、行列(Grid)の入れ子です。

上の画像の場合、カードを配置する親要素を2列1行のGridで扱ったとします。当然のことながらカード内の要素はそろいません。各カードで内側はバラバラの高さになります。
SubGridで考える場合まず親要素を2列3行(タイトル、説明、ボタンの3つ)のGridとします。次にカード内の要素を1列3行のグリッドで配置します。これが行列の入れ子です。こうすることで、各要素の高さは揃い私は気持ちよくなります。

以前から同じようなデザインはあったようで、JavaScriptで高さを計算しなんやかんやしてたようです。

コード全体

とりあえずカードを作成します。

import clsx from "clsx";


type Props = {
  children: React.ReactNode;
  className?: string;
};

const Card: React.FC<Props> = ({
  children,
  className = "",
}) => {
  return (
    <div className={clsx(
      "rounded-lg bg-card/30 text-card-foreground shadow-md transition duration-300",
      "hover:bg-card/50 hover:shadow-xl hover:scale-[1.01]",
      "dark:bg-card/40 dark:text-card-foreground shadow-zinc-400/20", 
      "dark:hover:bg-card/60 dark:hover:shadow-zinc-400/40",
      className,
    )}>
      {children}
    </div>
  );
}

export default Card; 

枠組みやアニメーションのみを定義します。サイズ等はclassNameで受け取るような形にします。

次にこのカードを使用し並べるコンポーネントを作成します。今回は1列の場合と2列の場合を想定して作成します。

import Link from "next/link";
import clsx from "clsx";
import Arrow from "./Arrow";
import Card from "./Card";


type Content = {
  title: string;
  describe: string;
  href: string;
};

type Props = {
  cardContents: Content[];
};

const CardStack: React.FC<Props> = ({ cardContents }) => {
  const rowSpan = cardContents.length * 3;
  const mdRowSpan = (rowSpan % 2 == 0 ? rowSpan : rowSpan + 3) / 2;

  return (
    <div className={clsx(
      "grid grid-rows-9 py-5",
      "grid-cols-1 grid-rows-[repeat(${rowSpan},minmax(0,1fr))] gap-10",
      "md:grid-cols-2 md:grid-rows-[repeat(${mdRowSpan},minmax(0,1fr))]",
    )}>
      {cardContents.map((content, index) => (
        <Card key={index} className="px-2 py-3 grid grid-rows-subgrid row-span-3 max-w-72 gap-4">
          <h3 className="text-2xl">{content.title}</h3>
          <div className="flex gap-0 px-2">
            <p className="text-xl break-all">{content.describe}</p>
          </div>
          <Link href={content.link} className="ml-auto pl-2 pr-3 py-0 pb-1 border-2 border-muted rounded-md">
            <Arrow>
              <span className="text-xl">more</span>
            </Arrow>
          </Link>
        </Card>
      ))}
    </div>
  );
};

export default CardStack;

<Arrow>コンポーネントはボタン部分の矢印です。今回は関係ないため省略します。

細かく

  const rowSpan = cardContents.length * 3;
  const mdRowSpan = (rowSpan % 2 == 0 ? rowSpan : rowSpan + 3) / 2;

  return (
    <div className={clsx(
      "grid grid-rows-9 py-5",
      "grid-cols-1 grid-rows-[repeat(${rowSpan},minmax(0,1fr))] gap-10",
      "md:grid-cols-2 md:grid-rows-[repeat(${mdRowSpan},minmax(0,1fr))]",
    )}>

まず要素数を取得し、親要素で指定する行の数を求めます。1つのカードで3行使用するため3をかけています。2列構成になるときは単純に2で割っています。

要素数が5つの場合2で割り切れないので帳尻合わせしてます。

      {cardContents.map((content, index) => (
        <Card key={index} className="px-2 py-3 grid grid-rows-subgrid row-span-3 max-w-72 gap-4">
          <h3 className="text-2xl">{content.title}</h3>
          <div className="flex gap-0 px-2">
            <p className="text-xl break-all">{content.describe}</p>
          </div>
          <Link href={content.link} className="ml-auto pl-2 pr-3 py-0 pb-1 border-2 border-muted rounded-md">
            <Arrow>
              <span className="text-xl">more</span>
            </Arrow>
          </Link>
        </Card>
      ))}

mapで取り出し1つづつ要素を配置していきます。
grid-rows-subgridで行にsubgridを適用し、row-span-3で3行使用することを示します。
あとはいい感じに配置して完成です。

まとめ

subgridでいい感じの見た目のカードを作成しました。

とても便利です。親行列で指定したgapを子行列で上書きできるのでわりと自由にスタイルを調整できます。是非使ってみてください。

最終更新日
2025-01-26