概要
SampleCardSkeleton(単体)と SampleCardSkeletonGrid(グリッド)の各状態を Storybook CSF 3.0 形式でストーリー化する。count props の違い・グリッド幅・loading.tsx 相当の全画面版など複数バリエーションをカタログにして、animate-pulse の見た目をデザインレビューで確認できるようにする。
インストール
npx storybook@latest init
実装
対象コンポーネント(再掲)
// components/SampleCardSkeleton.tsx
export function SampleCardSkeleton() {
return (
<div className="animate-pulse rounded-lg border border-gray-100 p-4 space-y-3">
<div className="h-5 w-3/4 rounded bg-gray-200" />
<div className="h-4 w-full rounded bg-gray-200" />
<div className="h-4 w-5/6 rounded bg-gray-200" />
<div className="flex gap-2 pt-1">
<div className="h-5 w-16 rounded-full bg-gray-200" />
<div className="h-5 w-12 rounded-full bg-gray-200" />
</div>
</div>
);
}
type GridProps = { count?: number };
export function SampleCardSkeletonGrid({ count = 6 }: GridProps) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{Array.from({ length: count }).map((_, i) => (
<SampleCardSkeleton key={i} />
))}
</div>
);
}
ストーリーファイル
// components/SampleCardSkeleton.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { SampleCardSkeleton, SampleCardSkeletonGrid } from "./SampleCardSkeleton";
const meta = {
title: "Sample/SampleCardSkeleton",
component: SampleCardSkeleton,
tags: ["autodocs"],
parameters: {
layout: "padded",
},
} satisfies Meta<typeof SampleCardSkeleton>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const InCard: Story = {
decorators: [
(Story) => (
<div className="max-w-sm">
<Story />
</div>
),
],
};
グリッドのストーリー
// components/SampleCardSkeletonGrid.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { SampleCardSkeletonGrid } from "./SampleCardSkeleton";
const meta = {
title: "Sample/SampleCardSkeletonGrid",
component: SampleCardSkeletonGrid,
tags: ["autodocs"],
parameters: {
layout: "fullscreen",
},
argTypes: {
count: { control: { type: "number", min: 1, max: 12 } },
},
} satisfies Meta<typeof SampleCardSkeletonGrid>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: { count: 6 },
decorators: [
(Story) => (
<div className="p-6">
<Story />
</div>
),
],
};
export const ThreeItems: Story = {
args: { count: 3 },
decorators: [
(Story) => (
<div className="p-6">
<Story />
</div>
),
],
};
export const SingleItem: Story = {
args: { count: 1 },
decorators: [
(Story) => (
<div className="p-6 max-w-sm">
<Story />
</div>
),
],
};
loading.tsx 相当のページレベルストーリー
// app/samples/Loading.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { SampleCardSkeletonGrid } from "@/components/SampleCardSkeleton";
const meta = {
title: "Pages/SamplesLoading",
component: SampleCardSkeletonGrid,
parameters: {
layout: "fullscreen",
},
} satisfies Meta<typeof SampleCardSkeletonGrid>;
export default meta;
type Story = StoryObj<typeof meta>;
export const LoadingPage: Story = {
args: { count: 9 },
decorators: [
(Story) => (
<main className="mx-auto max-w-5xl px-4 py-8">
<div className="mb-6 h-8 w-48 animate-pulse rounded bg-gray-200" />
<Story />
</main>
),
],
};
ポイント
satisfies Meta<typeof Component>で props 型を推論させる。as Metaより型安全で、argsのオートコンプリートが利くtags: ["autodocs"]を付けると Storybook がargsとargTypesから自動でドキュメントページを生成する。countprops の説明を補足するにはargTypesのdescriptionフィールドを追加するdecoratorsでラッパーを加えることでコンポーネント自体は変えずに、表示幅や padding などのコンテキストを各ストーリーで調整できるargTypes: { count: { control: { type: "number" } } }で Controls パネルからインタラクティブに件数を変更できる。デザイナーが Storybook 上で件数バリエーションを確認しやすくなるanimate-pulseは Tailwind の CSS アニメーションのためストーリー上でもアニメーションが動く。静止画の確認ではなく実際の動きを見てレビューできる- ページレベルのストーリー(
LoadingPage)を作ることで、loading.tsxが実際に表示される状態をデザインレビューで再現できる。Suspense境界やloading.tsx単体のレイアウト確認に使う