BLK-33Blocks
Pricing Comparison Matrix
A pricing block that presents plans against features in a single table. The recommended column is highlighted as a continuous pale amber pillar with a badge, while supported items use a hairline check and unsupported ones a soft dash to keep the overall density even. Figures use tabular numerals, and on narrow screens the feature column stays sticky while the plans scroll horizontally.
- Added:
- 2026-08-21
- Dependencies:
- None
- tags
- #pricing #table #comparison #section #brand #responsive
Preview
Plan comparison
プランごとの対応範囲
表示価格は税別・初期制作費の目安です。
| 機能・サポート | ライト¥10万〜小規模サイト向け | おすすめスタンダード¥30万〜本格サイト向け | プレミアム¥50万〜伴走型の制作 |
|---|---|---|---|
| 制作・実装 | |||
| ページ数 | 5ページ | 10ページ | 無制限 |
| スマホ最適化 | 対応 | 対応 | 対応 |
| オリジナルデザインテンプレート非使用 | 非対応 | 対応 | 対応 |
| 撮影・原稿作成 | 非対応 | 非対応 | 対応 |
| 運用・機能 | |||
| 更新機能(CMS) | 非対応 | 対応 | 対応 |
| お問い合わせフォーム | 1件 | 3件 | 無制限 |
| お知らせ・ブログ | 非対応 | 対応 | 対応 |
| 多言語ページ | 非対応 | 非対応 | 対応 |
| サポート | |||
| 公開後サポート | 1ヶ月 | 3ヶ月 | 12ヶ月 |
| アクセス解析レポート | 非対応 | 非対応 | 月1回 |
| 定例ミーティング | 非対応 | 隔月 | 毎月 |
| ご要望に応じて項目単位の追加も承ります。 | 相談するこのプランで相談 | 相談するこのプランで相談 | 相談するこのプランで相談 |
← 横にスワイプして全プランを比較
"use client";
import { useEffect, useRef, useState } from "react";
/**
* 料金②機能比較マトリクス(BLK-33)
*
* 汎用技法メモ(web-design-playbook 還流用):
*
* 1. 比較表は「カード羅列」ではなく1枚の table で組む
* プラン×機能は本質的に2次元データなので、<table> + scope 属性で組むと
* スクリーンリーダーが「ライト / CMS導入 / 非対応」と読み上げてくれる。
* div グリッドで作ると行と列の対応が音声で失われる。
* グループ見出しは <tbody> を分けて <th scope="colgroup"> を立てる。
*
* 2. 推奨列のハイライトは「セル単位のクラス」で縦に貫く
* 列全体に色を当てる CSS(<col> の background)は border-collapse 下で
* ボーダーとの重なり順が不安定なので使わない。各行のそのセルへ
* bg-amber-50/70 + border-x border-amber-200 を配り、
* 最上段に border-t、最下段に border-b を足すと1本の柱になる。
* グループ見出し行にも空セルを置いて柱を切らさないのが肝。
*
* 3. 横スワイプは overflow-x-auto + 先頭列 sticky
* 表は table-fixed + min-w で列幅を固定する(auto レイアウトだと狭い幅で
* 先頭列だけが伸び、値の列が枠外へ押し出されて読めなくなる)。包む div に overflow-x-auto。
* 機能名の列(先頭列)だけ sticky left-0 + bg-white にすると、
* 横スクロール中も「何の行か」が消えない。bg-white は横スクロールが起きる幅でだけ要るので、
* 表が収まるコンテナ幅では @2xl:bg-transparent で戻す(白い帯が地に浮くのを避ける)。
* スクロール領域は tabIndex={0} + role="region" にしてキーボードでも送れるようにする
* (ブラウザの「スクロール可能領域はフォーカス可能に」の要件)。
*
* 4. 記号の設計: 対応=細線チェック / 非対応=淡いダッシュ
* 塗りのチェックと ✕ を並べると「✕」が強すぎて非対応が欠点に見える。
* stroke-width 1.25 の細線チェックと、h-px の短い罫線(ダッシュ)に落とすと
* 表全体の濃度が均され、罫線と余白のグルーピングが主役になる。
* 記号だけでは音声で伝わらないので sr-only で「対応 / 非対応」を必ず添える。
*
* 5. 数字は tabular-nums
* 価格・ページ数など桁が変わる値は font-variant-numeric: tabular-nums(Tailwind の
* tabular-nums)で字送りを固定する。列で縦に揃うと比較の視線移動が減る。
*
* 6. モーションは行単位のスタッガー1回だけ
* IntersectionObserver で1度だけ可視化し、transform/opacity のみを
* 行インデックス×40ms でずらす。motion-reduce: で初期状態を打ち消して即時表示にする
* (transition-none だけでは opacity-0 のまま固まる)。
*/
type Cell = boolean | string;
type Plan = {
key: string;
name: string;
price: string;
unit: string;
lead: string;
featured?: boolean;
};
type Group = {
label: string;
rows: { label: string; note?: string; cells: [Cell, Cell, Cell] }[];
};
const plans: Plan[] = [
{ key: "light", name: "ライト", price: "10", unit: "万〜", lead: "小規模サイト向け" },
{
key: "standard",
name: "スタンダード",
price: "30",
unit: "万〜",
lead: "本格サイト向け",
featured: true,
},
{ key: "premium", name: "プレミアム", price: "50", unit: "万〜", lead: "伴走型の制作" },
];
const groups: Group[] = [
{
label: "制作・実装",
rows: [
{ label: "ページ数", cells: ["5ページ", "10ページ", "無制限"] },
{ label: "スマホ最適化", cells: [true, true, true] },
{ label: "オリジナルデザイン", note: "テンプレート非使用", cells: [false, true, true] },
{ label: "撮影・原稿作成", cells: [false, false, true] },
],
},
{
label: "運用・機能",
rows: [
{ label: "更新機能(CMS)", cells: [false, true, true] },
{ label: "お問い合わせフォーム", cells: ["1件", "3件", "無制限"] },
{ label: "お知らせ・ブログ", cells: [false, true, true] },
{ label: "多言語ページ", cells: [false, false, true] },
],
},
{
label: "サポート",
rows: [
{ label: "公開後サポート", cells: ["1ヶ月", "3ヶ月", "12ヶ月"] },
{ label: "アクセス解析レポート", cells: [false, false, "月1回"] },
{ label: "定例ミーティング", cells: [false, "隔月", "毎月"] },
],
},
];
function CheckMark({ featured }: { featured?: boolean }) {
return (
<>
<svg
viewBox="0 0 20 20"
fill="none"
stroke="currentColor"
strokeWidth={1.25}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
className={`mx-auto size-[18px] ${featured ? "text-amber-600" : "text-gray-700"}`}
>
<path d="M4 10.4 8.2 14.6 16 5.8" />
</svg>
<span className="sr-only">対応</span>
</>
);
}
function DashMark() {
return (
<>
<span className="mx-auto block h-px w-3.5 bg-gray-300" aria-hidden="true" />
<span className="sr-only">非対応</span>
</>
);
}
export default function PricingCompareTable() {
const rootRef = useRef<HTMLElement | null>(null);
const [inView, setInView] = useState(false);
useEffect(() => {
const node = rootRef.current;
if (!node) return;
const io = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
setInView(true);
io.disconnect();
}
},
{ threshold: 0.1 },
);
io.observe(node);
return () => io.disconnect();
}, []);
const reveal = (index: number) =>
({
transitionDelay: `${120 + index * 40}ms`,
}) as const;
const revealClass = `transition-[opacity,transform] duration-500 ease-out motion-reduce:transition-none motion-reduce:translate-y-0 motion-reduce:opacity-100 ${
inView ? "translate-y-0 opacity-100" : "translate-y-2 opacity-0"
}`;
/** 推奨列を縦に貫くアンバーの柱(セル単位で配る) */
const col = (plan: Plan) =>
plan.featured ? "border-x border-amber-200 bg-amber-50/70" : "";
let rowIndex = 0;
return (
<section
ref={rootRef}
className="@container w-full max-w-[720px] text-gray-900"
aria-labelledby="cmp-title"
>
<p className="text-[11px] font-bold tracking-[0.28em] text-amber-600 uppercase">
Plan comparison
</p>
<div className="mt-3 flex flex-col gap-2 @xl:flex-row @xl:items-end @xl:justify-between">
<h2 id="cmp-title" className="text-2xl font-bold tracking-tight @xl:text-[28px]">
プランごとの対応範囲
</h2>
<p className="text-xs leading-relaxed text-gray-500">
表示価格は税別・初期制作費の目安です。
</p>
</div>
<div
role="region"
aria-label="料金プラン比較表(横スクロールできます)"
tabIndex={0}
className="mt-6 overflow-x-auto rounded-xl focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
>
<table className="w-full min-w-[540px] table-fixed border-collapse text-left">
<caption className="sr-only">
ライト・スタンダード・プレミアムの3プランと、制作・運用・サポート各項目の対応表
</caption>
<thead>
<tr>
<th
scope="col"
className="sticky left-0 z-10 w-[30%] bg-white @2xl:bg-transparent pb-4 align-bottom text-xs font-semibold text-gray-400"
>
機能・サポート
</th>
{plans.map((plan, i) => (
<th
key={plan.key}
scope="col"
style={reveal(i)}
className={`w-[23.3%] rounded-t-xl px-3 pt-4 pb-4 text-center align-top ${revealClass} ${
plan.featured ? "border-t border-x border-amber-200 bg-amber-50/70" : ""
}`}
>
<span className="flex h-5 items-center justify-center">
{plan.featured && (
<span className="rounded-full bg-amber-600 px-2.5 py-0.5 text-[10px] font-bold tracking-[0.12em] text-white">
おすすめ
</span>
)}
</span>
<span className="mt-2 block text-sm font-bold text-gray-900">{plan.name}</span>
<span className="mt-1.5 flex items-baseline justify-center gap-0.5">
<span className="text-xs font-medium text-gray-500">¥</span>
<span className="text-2xl font-bold tracking-tight tabular-nums text-gray-900">
{plan.price}
</span>
<span className="text-xs font-medium text-gray-500">{plan.unit}</span>
</span>
<span className="mt-1 block text-[11px] leading-relaxed font-normal text-gray-500">
{plan.lead}
</span>
</th>
))}
</tr>
</thead>
{groups.map((group) => (
<tbody key={group.label}>
<tr>
<th
scope="colgroup"
className="sticky left-0 z-10 bg-white pt-6 @2xl:bg-transparent pb-2 text-[11px] font-bold tracking-[0.16em] text-gray-400 uppercase"
>
<span className="flex items-center gap-2">
<span className="h-px w-4 bg-amber-600" aria-hidden="true" />
{group.label}
</span>
</th>
{plans.map((plan) => (
<td key={plan.key} className={col(plan)} />
))}
</tr>
{group.rows.map((row) => {
const index = rowIndex++;
return (
<tr key={row.label} style={reveal(index)} className={revealClass}>
<th
scope="row"
className="sticky left-0 z-10 border-t border-gray-100 bg-white py-3 @2xl:bg-transparent pr-4 text-[13px] leading-snug font-medium text-gray-700"
>
{row.label}
{row.note && (
<span className="mt-0.5 block text-[11px] font-normal text-gray-400">
{row.note}
</span>
)}
</th>
{row.cells.map((cell, i) => {
const plan = plans[i];
return (
<td
key={plan.key}
className={`border-t border-gray-100 px-3 py-3 text-center text-[13px] tabular-nums ${
typeof cell === "string" ? "font-medium text-gray-800" : ""
} ${col(plan)}`}
>
{cell === true ? (
<CheckMark featured={plan.featured} />
) : cell === false ? (
<DashMark />
) : (
cell
)}
</td>
);
})}
</tr>
);
})}
</tbody>
))}
<tfoot>
<tr>
<td className="sticky left-0 z-10 bg-white pt-5 @2xl:bg-transparent text-[11px] leading-relaxed text-gray-400 align-top">
ご要望に応じて項目単位の追加も承ります。
</td>
{plans.map((plan, i) => (
<td
key={plan.key}
style={reveal(rowIndex + i)}
className={`rounded-b-xl px-3 pt-5 pb-5 text-center ${revealClass} ${
plan.featured ? "border-x border-b border-amber-200 bg-amber-50/70" : ""
}`}
>
<a
href="#"
className={`inline-flex items-center justify-center rounded-lg px-4 py-2 text-xs font-semibold whitespace-nowrap transition-colors focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 ${
plan.featured
? "bg-amber-600 text-white hover:bg-amber-700"
: "border border-gray-300 bg-white text-gray-800 hover:bg-gray-50"
}`}
>
<span className="@2xl:hidden">相談する</span>
<span className="hidden @2xl:inline">このプランで相談</span>
</a>
</td>
))}
</tr>
</tfoot>
</table>
</div>
<p className="mt-4 text-[11px] text-gray-400 @2xl:hidden">← 横にスワイプして全プランを比較</p>
</section>
);
}
Add to your project via shadcn CLI
npx shadcn@latest add https://designs.first-ch.com/r/pricing-compare-table.json