強み・ベントーグリッド(不均等タイルの主従)
「選ばれる理由」を縦に積まず、3カラム×4行=12セルの不均等モザイクに置く強みブロック。タイルの面積そのものを情報量に割り当て、2×2の主役だけが本文と補足指標を持ち、1×1は言い切り1行だけを持つ(面積=情報量のルールを先に決めておくのが要点)。grid-template-areasを書かずDOM順=読み順のまま自動配置で「2×2+1×2+2×1+1×1×2+2×1」が穴なく埋まり、折り返しはビューポートではなくコンテナクエリなので狭い枠に埋め込んでも崩れない。形ごとに中の組み方を変える(横長はテキスト|図版の横並び、縦長は図版を下端へ、正方は図版を上・見出しを下端)。カード感を消すため罫線はhairline1色・角丸2px・影なしに固定し、主役は薄いアンバーの塗りだけで示す。各タイルの小図版はdivのborderとbgだけのCSS図形で、ホバーで図版だけがtransformで微動する(タイルはリンクではないので文字と罫線は動かさない)。画像素材ゼロ・JSゼロ・依存ゼロ。
- 追加日:
- 2026-09-18
- 依存:
- なし
- tags
- #block #strengths #reasons #service #bento #uneven-grid #container-query #hairline #css-shapes #no-image #no-js #no-deps
プレビュー
BLK-66標本 / 強み・6面
強みは、同じ大きさで並べない
選んでいただく理由を6つ。いちばん多くのご相談で決め手になっているものだけを大きく取り、残りは短い言い切りで並べます。面積の大きいタイルほど、書いてある量も多くなります。
- 初動
相談の翌営業日に、たたき台の画面が出ます
打ち合わせで伺った要望をその場で構成に落とし、翌営業日までにトップページ1枚の下書きをお見せします。文字は仮でも、並び順と見出しの強さは本番の想定で置きます。言葉だけで詰めずに画面で合わせるので、方向違いのまま数週間進むことがありません。
- 初回のたたき台
- 1営業日
- 公開までの目安
- 5週
- 原稿
文章は、こちらで書き起こします
打ち合わせの録音から本文の草案を作り、赤入れだけで進められる状態にしてお渡しします。
- 速度
表示の速さは、公開前に数値で確かめます
回線の細い環境を模した計測で、主要3ページの読み込みを確認してから公開します。
スマホの画面から設計します
写真は撮影から引き受けます
- 保守
保守は、上限額を決めてから始めます
月内に対応する時間と範囲を先に決め、超えるときは着手前にご連絡します。
記載の日数は直近1年の実績の中央値です。内容によって前後する場合は、着手前にお伝えします。
サービスの流れを見るimport { Montserrat } from "next/font/google";
const montserrat = Montserrat({
weight: ["500", "600"],
subsets: ["latin"],
display: "swap",
});
/**
* 強み・ベントーグリッド(不均等タイルの主従)
* 既存の強み系は「行を縦に積む」型(BLK-16 の非対称12カラム・BLK-05 の数字1本)と
* 「切り替えて1つずつ見せる」型(BLK-36/BLK-44)が既出のため、本ブロックは二軸のモザイクで
* **タイルの面積そのものを情報量に割り当てた**(大=詳細+指標+図版 / 小=言い切り1行のみ)。
* 不揃いグリッドは BLK-31 にもあるが、あちらは同形カード+主役1枚の商品一覧型。ここは
* 4形状(2×2 / 1×2 / 2×1 / 1×1)で中の組み方まで形ごとに変え、JS・画像・依存はゼロ。
*
* 汎用技法メモ(web-design-playbook 還流用):
* - ベントー(不均等スパン)の埋め方: 3カラム×4行=12セルを「2×2 + 1×2 + 2×1 + 1×1 + 1×1 + 2×1」で
* ちょうど埋める。grid-template-areas を書かなくても、**DOM順=読み順のまま自動配置**に任せれば
* この順序で穴なく埋まる(row-sparse の配置カーソルは前へ戻らないため、大きいタイルを先に置くのが条件)。
* dense を付けると読み順とセル順がずれるので使わない。
* - スパンはブレークポイント変数にだけ付ける: 1カラム時に col-span-2 が残っていると
* `grid-column: span 2` が暗黙の2列目を作ってしまう。@md:col-span-2 のように必ず変数側へ付ける。
* 列数ごとに埋まる形が変わるので、2列時は「2×1 + 1×1×4 + 2×1」に配り替える。
* - 折り返しはビューポートではなくコンテナ基準(@container + @md:/@2xl:)。埋め込み先が狭い枠でも
* 「広い画面なのに箱は狭い」ズレが構造的に起きない。
* - 面積=情報量のルールを決めておく: ラベル+見出し+本文+指標(2セル以上)/見出し+本文(2セル)/
* 見出しのみ(1セル)。タイルごとに書ける量を気分で変えると、ベントーは単なる不揃いの箱になる。
* - 形ごとに内部の組み方を変える: 横長は「テキスト|図版」の横並び、縦長は下端に図版を落とす、
* 正方は図版を上・見出しを下端。全タイルを同じ縦積みにすると面積差が活きない。
* - カード感を殺す: 罫線は hairline 1色(gray-200)だけ、radius は 2px、影なし。主役は塗り(薄アンバー)で
* 示し、罫線の色や太さを変えない。
* - 図版はCSS図形のみ(div の border と bg の重ね合わせ)。ホバーで図版だけが transform で微動し、
* 文字と罫線は動かさない(タイルはリンクではないので、クリックできそうな見た目にしない)。
*/
type Shape = "hero" | "tall" | "wide" | "small";
type Strength = {
/** 2セル以上のタイルにだけ付く和文ラベル(強みは順序を持たないので番号は振らない) */
label?: string;
title: string;
/** 面積に応じた本文。1セルのタイルは持たない */
body?: string;
/** 主役タイルだけが持つ補足指標 */
notes?: { term: string; value: string }[];
shape: Shape;
fig: FigKind;
/** スパンは必ずブレークポイント変数側に付ける(1カラム時に暗黙列を作らないため) */
span: string;
};
const strengths: Strength[] = [
{
label: "初動",
title: "相談の翌営業日に、たたき台の画面が出ます",
body: "打ち合わせで伺った要望をその場で構成に落とし、翌営業日までにトップページ1枚の下書きをお見せします。文字は仮でも、並び順と見出しの強さは本番の想定で置きます。言葉だけで詰めずに画面で合わせるので、方向違いのまま数週間進むことがありません。",
notes: [
{ term: "初回のたたき台", value: "1営業日" },
{ term: "公開までの目安", value: "5週" },
],
shape: "hero",
fig: "plates",
span: "@md:col-span-2 @2xl:row-span-2",
},
{
label: "原稿",
title: "文章は、こちらで書き起こします",
body: "打ち合わせの録音から本文の草案を作り、赤入れだけで進められる状態にしてお渡しします。",
shape: "tall",
fig: "stack",
span: "@2xl:row-span-2",
},
{
label: "速度",
title: "表示の速さは、公開前に数値で確かめます",
body: "回線の細い環境を模した計測で、主要3ページの読み込みを確認してから公開します。",
shape: "wide",
fig: "meter",
span: "@2xl:col-span-2",
},
{
title: "スマホの画面から設計します",
shape: "small",
fig: "devices",
span: "",
},
{
title: "写真は撮影から引き受けます",
shape: "small",
fig: "crop",
span: "",
},
{
label: "保守",
title: "保守は、上限額を決めてから始めます",
body: "月内に対応する時間と範囲を先に決め、超えるときは着手前にご連絡します。",
shape: "wide",
fig: "gauge",
span: "@md:col-span-2",
},
];
type FigKind = "plates" | "stack" | "meter" | "devices" | "crop" | "gauge";
/** 図版はCSS図形だけで作る(画像・SVG・依存なし)。動くのは transform のみ。 */
function TileFigure({ kind }: { kind: FigKind }) {
const move = "transition-transform duration-500 ease-out motion-reduce:transition-none";
if (kind === "plates") {
return (
<div aria-hidden="true" className="relative h-[92px] w-[146px] shrink-0">
<span
className={`absolute top-0 left-0 h-[72px] w-[104px] rounded-[2px] border border-gray-300 bg-white group-hover:-translate-x-1.5 motion-reduce:translate-x-0 ${move}`}
/>
<div
className={`absolute top-[20px] left-[38px] h-[72px] w-[104px] rounded-[2px] border border-gray-400 bg-white p-2.5 group-hover:translate-x-1.5 group-hover:-translate-y-1 motion-reduce:translate-x-0 motion-reduce:translate-y-0 ${move}`}
>
<span className="block h-2 w-8 bg-amber-500" />
<span className="mt-2.5 block h-px w-full bg-gray-300" />
<span className="mt-2 block h-px w-full bg-gray-300" />
<span className="mt-2 block h-px w-3/5 bg-gray-300" />
</div>
</div>
);
}
if (kind === "stack") {
return (
<div aria-hidden="true" className="relative flex w-full max-w-[132px] flex-col gap-2 pl-3">
<span className="absolute top-0 bottom-0 left-0 w-px bg-gray-300" />
<span
className={`h-4 w-full rounded-[2px] border border-gray-300 bg-white group-hover:translate-x-1 motion-reduce:translate-x-0 ${move}`}
/>
<span
className={`h-4 w-[72%] rounded-[2px] border border-gray-300 bg-white delay-75 group-hover:translate-x-1 motion-reduce:translate-x-0 ${move}`}
/>
<span
className={`h-4 w-[46%] rounded-[2px] border border-amber-300 bg-amber-50 delay-150 group-hover:translate-x-1 motion-reduce:translate-x-0 ${move}`}
/>
</div>
);
}
if (kind === "meter") {
return (
<div aria-hidden="true" className="w-[132px] shrink-0">
<div className="flex items-end justify-between">
{[0, 1, 2, 3].map((i) => (
<span key={i} className="h-2.5 w-px bg-gray-300" />
))}
</div>
<div className="mt-1.5 h-1.5 w-full bg-gray-100">
<span
className={`block h-full w-[64%] origin-left scale-x-90 bg-amber-500 group-hover:scale-x-100 motion-reduce:scale-x-100 ${move}`}
/>
</div>
</div>
);
}
if (kind === "devices") {
return (
<div aria-hidden="true" className="flex items-end gap-2">
<span
className={`h-11 w-[26px] rounded-[2px] border border-amber-400 bg-amber-50 group-hover:-translate-y-1 motion-reduce:translate-y-0 ${move}`}
/>
<span className="h-7 w-12 rounded-[2px] border border-gray-300 bg-white" />
</div>
);
}
if (kind === "crop") {
return (
<div aria-hidden="true" className="relative h-12 w-12">
<span
className={`absolute top-0 left-0 h-3 w-3 border-t border-l border-gray-400 group-hover:-translate-x-1 group-hover:-translate-y-1 motion-reduce:translate-x-0 motion-reduce:translate-y-0 ${move}`}
/>
<span
className={`absolute top-0 right-0 h-3 w-3 border-t border-r border-gray-400 group-hover:translate-x-1 group-hover:-translate-y-1 motion-reduce:translate-x-0 motion-reduce:translate-y-0 ${move}`}
/>
<span
className={`absolute bottom-0 left-0 h-3 w-3 border-b border-l border-gray-400 group-hover:-translate-x-1 group-hover:translate-y-1 motion-reduce:translate-x-0 motion-reduce:translate-y-0 ${move}`}
/>
<span
className={`absolute right-0 bottom-0 h-3 w-3 border-r border-b border-gray-400 group-hover:translate-x-1 group-hover:translate-y-1 motion-reduce:translate-x-0 motion-reduce:translate-y-0 ${move}`}
/>
<span className="absolute top-1/2 left-1/2 h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2 rounded-full bg-amber-500" />
</div>
);
}
return (
<div aria-hidden="true" className="w-[132px] shrink-0">
<div className="flex items-end justify-between">
{[0, 1, 2, 3, 4, 5, 6, 7, 8].map((i) => (
<span key={i} className={i === 4 ? "h-3 w-px bg-gray-400" : "h-2 w-px bg-gray-200"} />
))}
</div>
<div className="relative mt-1.5 h-px w-full bg-gray-300">
<span
className={`absolute -top-[3px] left-[38%] h-[7px] w-[7px] rounded-full bg-amber-600 group-hover:translate-x-2 motion-reduce:translate-x-0 ${move}`}
/>
</div>
</div>
);
}
/** 2セル以上のタイルにだけ置く和文ラベル(英字キッカーは使わない) */
function TileLabel({ text }: { text: string }) {
return (
<span className="flex items-center gap-2 text-[11px] font-medium text-gray-500">
<span aria-hidden="true" className="h-[3px] w-[3px] bg-amber-600" />
{text}
</span>
);
}
function TileBody({ item }: { item: Strength }) {
// 形ごとに内部の組み方を変える(全部を同じ縦積みにすると面積差が活きない)
if (item.shape === "hero") {
return (
<>
{item.label ? <TileLabel text={item.label} /> : null}
<h3 className="mt-3 text-[1.0625rem] leading-[1.6] font-bold tracking-tight text-balance text-gray-900 @md:text-[1.15rem] @2xl:text-[1.3rem]">
{item.title}
</h3>
<p className="mt-3 text-[13px] leading-[2] text-gray-600">{item.body}</p>
<div className="mt-6 flex flex-wrap items-end justify-between gap-x-8 gap-y-5 @md:mt-auto @md:pt-8">
<dl className="flex gap-x-8 gap-y-3">
{item.notes?.map((n) => (
<div key={n.term}>
<dt className="text-[11px] text-gray-500">{n.term}</dt>
<dd
className={`${montserrat.className} mt-1 text-[1.25rem] leading-none font-semibold text-amber-700 tabular-nums`}
>
{n.value}
</dd>
</div>
))}
</dl>
<TileFigure kind={item.fig} />
</div>
</>
);
}
if (item.shape === "tall") {
return (
<>
{item.label ? <TileLabel text={item.label} /> : null}
<h3 className="mt-3 text-[0.95rem] leading-[1.65] font-bold tracking-tight text-balance text-gray-900 @2xl:text-[1rem]">
{item.title}
</h3>
<p className="mt-2.5 text-[12.5px] leading-[1.95] text-gray-600">{item.body}</p>
<div className="mt-6 @2xl:mt-auto @2xl:pt-8">
<TileFigure kind={item.fig} />
</div>
</>
);
}
if (item.shape === "wide") {
return (
<div className="flex flex-col gap-5 @md:flex-row @md:items-center @md:justify-between @md:gap-6">
<div className="min-w-0">
{item.label ? <TileLabel text={item.label} /> : null}
<h3 className="mt-3 text-[0.95rem] leading-[1.65] font-bold tracking-tight text-balance text-gray-900 @2xl:text-[1rem]">
{item.title}
</h3>
<p className="mt-2.5 text-[12.5px] leading-[1.95] text-gray-600">{item.body}</p>
</div>
<TileFigure kind={item.fig} />
</div>
);
}
// 1セル: 図版を上、見出しを下端に落として「言い切り1行」だけを持たせる
return (
<>
<TileFigure kind={item.fig} />
<h3 className="mt-auto pt-6 text-[0.9rem] leading-[1.7] font-bold tracking-tight text-balance text-gray-900 @2xl:text-[0.95rem]">
{item.title}
</h3>
</>
);
}
export default function StrengthBentoGrid() {
return (
<section
aria-labelledby="strength-bento-grid-heading"
className="@container w-full max-w-5xl bg-white px-1"
>
<header className="max-w-[46rem]">
<p className="flex items-center gap-2.5 text-[11px] text-gray-400">
<span aria-hidden="true" className="h-px w-5 bg-amber-600" />
<span className={`${montserrat.className} font-semibold text-gray-500`}>BLK-66</span>
<span>標本 / 強み・6面</span>
</p>
<h2
id="strength-bento-grid-heading"
className="mt-4 text-[clamp(1.6rem,3.4vw,2.35rem)] leading-[1.35] font-bold tracking-tight text-balance text-gray-900"
>
強みは、同じ大きさで並べない
</h2>
<p className="mt-5 text-[14.5px] leading-[1.95] font-medium text-gray-600">
選んでいただく理由を6つ。いちばん多くのご相談で決め手になっているものだけを大きく取り、残りは短い言い切りで並べます。面積の大きいタイルほど、書いてある量も多くなります。
</p>
</header>
{/* ベントー: 3カラム×4行=12セルを DOM順(=読み順)のまま自動配置で埋める */}
<ul className="mt-10 grid grid-cols-1 gap-2.5 @md:grid-cols-2 @md:gap-3 @2xl:grid-cols-3 @2xl:auto-rows-[minmax(7.5rem,auto)]">
{strengths.map((item) => (
<li
key={item.title}
className={`group flex flex-col rounded-[2px] border border-gray-200 p-5 @2xl:p-6 ${
item.shape === "hero" ? "bg-amber-50 @2xl:p-8" : "bg-white"
} ${item.span}`}
>
<TileBody item={item} />
</li>
))}
</ul>
<div className="mt-8 flex flex-col gap-4 border-t border-gray-200 pt-6 @md:flex-row @md:items-baseline @md:justify-between">
<p className="text-[12.5px] leading-[1.9] text-gray-500">
記載の日数は直近1年の実績の中央値です。内容によって前後する場合は、着手前にお伝えします。
</p>
<a
href="#"
className="shrink-0 text-sm font-semibold text-gray-900 underline decoration-amber-600 decoration-2 underline-offset-[6px] transition-colors duration-300 hover:text-amber-700 focus-visible:rounded-[2px] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 motion-reduce:transition-none"
>
サービスの流れを見る
</a>
</div>
</section>
);
}
shadcn CLI でプロジェクトに追加
npx shadcn@latest add https://designs.first-ch.com/r/strength-bento-grid.json