Pain Checklist → Solution Bridge
A problem-statement block that turns the usual pain list into a real checklist — hairline rules and square checkboxes instead of cards. The list is not just readable but selectable: tick the pains that apply and the amber band below keeps only the matching remedies (with nothing ticked it drops the filter and shows all, so the band is never empty). Empathy and resolution are joined by a single hairline that scales down from its top edge into a rotate-45 arrowhead. Only one surface changes temperature — bg-amber-50/70 with border-amber-600/25 on the band, while the list stays white. Count changes are announced through one aria-live="polite" line rather than a live list. No radii, no uppercase kicker, no trailing arrow links. Zero images, zero dependencies.
- Added:
- 2026-08-29
- Dependencies:
- None
- tags
- #block #problem #pain-points #checklist #checkbox #multi-select #filter #hairline #cardless #landing-page #a11y #no-image #no-deps
Preview
BLK-45 — 課題の棚卸し
こんなお悩み、ありませんか
ご相談の入口はいつも似ています。当てはまるものにチェックを入れると、下の帯がその課題への打ち手だけに変わります。
まだ選択されていません
その課題、こう解決します
6件の打ち手
上のリストにチェックを入れると、対応する打ち手だけが残ります。いまは全件を表示しています。
問い合わせ導線の再設計
目安 2〜4週
問い合わせが月に数件で止まったまま、増える気配がないへの打ち手:電話・フォーム・チャットの入口を1画面1目的に整理し、離脱が起きている画面から順に直します
更新できる状態への作り替え
目安 4〜8週
何年も前に作ったサイトを、社内の誰も直せないまま置いているへの打ち手:触る頻度の高い箇所から管理画面に寄せ、変更手順書を納品物に含めて引き渡します
表示崩れの棚卸し
目安 1〜2週
スマートフォンで見ると文字が小さく、崩れている画面があるへの打ち手:375pxから実機の幅で全画面を点検し、崩れの一覧と直した記録を残して共有します
更新の内製化
目安 2〜3週
文言をひとつ直すだけでも、費用と日数がかかってしまうへの打ち手:よく直す箇所だけを先に切り出し、担当の方が社内で完結できる形にしてお渡しします
載せる内容を決める場から同席
初回相談から
何を載せれば選ばれるのか、社内で意見がまとまらないへの打ち手:他社の見せ方と自社の強みを並べた資料を用意し、決める打ち合わせから参加します
公開後の窓口を残す
公開後も継続
公開したあとの相談先がなく、放置する以外になくなったへの打ち手:実装した人間がそのまま窓口を続け、月次で数字の変化と次の一手をまとめて返します
チェックした内容は問い合わせフォームの相談区分に引き継がれます。相談だけで終わっても構いません。
"use client";
import { useEffect, useId, useRef, useState } from "react";
import { Montserrat } from "next/font/google";
const montserrat = Montserrat({
weight: ["500", "600", "700"],
subsets: ["latin"],
display: "swap",
});
/**
* 課題共感→解決ブリッジ(こんなお悩みありませんか)
*
* 差別化: 既存の hairline縦リスト(BLK-21)は読むだけの静的な一覧、既存の比較ブロック(BLK-44)は
* 相手を1者だけ選ぶ radiogroup で、どちらも「選んだものが下の別セクションを絞り込む」構造は持たない。
* 本ブロックは悩みを複数選べる本物のチェックリストにし、選んだ件数ぶんだけ下の転換帯(薄アンバー)へ
* 打ち手が残る=上下2ブロックが1つの操作で連動する読み物にした(横並び比較でも切替タブでもない)。
*
* 意図的に外した型(ギャラリー全体で飽和していたもの):
* - 英字キッカー(uppercase + 広い字間)を置かない。標本IDは縦罫つきの和文ラベルに沈める
* - 角丸をゼロにする(チェックボックスもCTAも直角)。面はhairlineと薄アンバーの塗りだけで作る
* - 末尾の「→」リンクを使わない。締めは直角のボタン1つと注記
* - 行ごとの translate-y スタッガー出現を使わない。スクロールで動くのは接続要素(縦罫+三角)だけ
*
* 汎用技法メモ(web-design-playbook 還流用):
*
* 1. 「共感 → 転換」は縦の接続要素で1本につなぐ
* ペイン列挙と解決提示を別セクションに切ると読者が転換に気づかない。両者の間に
* origin-top の h-10 w-px を1本置き、scale-y-0 → 100 で下へ伸ばして先端に rotate-45 の
* 三角を置くと、矢印画像なしで「ここへ落ちる」導線になる。動かすのは transform だけ。
*
* 2. peer-checked: は「入れ子の子」には効かない
* Tailwind の peer-* は一般兄弟結合子(.peer:checked ~ .x)なので、sr-only の input の
* 兄弟にしか当たらない。四角の中のレ点まで連動させたいときは state で分岐する。
* ただし focus リングだけは peer-focus-visible: で兄弟の四角に載せる
* (input を sr-only にしてもキーボード操作の可視フィードバックが消えない)。
*
* 3. 選択で「下の一覧を絞る」ときの読み上げ設計
* 件数の変化は aria-live="polite" の1文だけに集約し、リスト自体は live にしない
* (行を live にすると1件チェックするたびに全文が読み上がる)。ゼロ件のときは
* 絞り込みを解いて全件を出す=空白を見せない。
*
* 4. 和文見出しは読点で節に切り、節を inline-block で包む
* 和文は文字間ならどこでも折り返るため、幅が変わると「ありませ/んか」と語中で切れる。
* 節ごとに inline-block にしておくと、折り返しの位置が必ず読点に決まる(<br> より幅に強い)。
*
* 5. 温度差は「塗り」ではなく塗りの範囲で作る
* 転換部だけ bg-amber-50/70 + border-amber-600/25 に変え、上のリストは白のまま残す。
* アクセント色を線や文字に散らすより、面を1枚だけ替えるほうが節の切り替わりが伝わる。
*/
type Item = {
id: string;
/** 行の右端に添える分類(装飾なので aria-hidden) */
tag: string;
/** チェックリストに並ぶ悩み(一人称の言い回しにする) */
pain: string;
/** 転換帯に出る打ち手の名前 */
fix: string;
/** 打ち手の説明(1文) */
detail: string;
/** 期間・関わり方の目安 */
meta: string;
};
const ITEMS: Item[] = [
{
id: "inquiry",
tag: "集客",
pain: "問い合わせが月に数件で止まったまま、増える気配がない",
fix: "問い合わせ導線の再設計",
detail:
"電話・フォーム・チャットの入口を1画面1目的に整理し、離脱が起きている画面から順に直します",
meta: "目安 2〜4週",
},
{
id: "legacy",
tag: "保守",
pain: "何年も前に作ったサイトを、社内の誰も直せないまま置いている",
fix: "更新できる状態への作り替え",
detail:
"触る頻度の高い箇所から管理画面に寄せ、変更手順書を納品物に含めて引き渡します",
meta: "目安 4〜8週",
},
{
id: "mobile",
tag: "表示",
pain: "スマートフォンで見ると文字が小さく、崩れている画面がある",
fix: "表示崩れの棚卸し",
detail:
"375pxから実機の幅で全画面を点検し、崩れの一覧と直した記録を残して共有します",
meta: "目安 1〜2週",
},
{
id: "cost",
tag: "費用",
pain: "文言をひとつ直すだけでも、費用と日数がかかってしまう",
fix: "更新の内製化",
detail:
"よく直す箇所だけを先に切り出し、担当の方が社内で完結できる形にしてお渡しします",
meta: "目安 2〜3週",
},
{
id: "content",
tag: "設計",
pain: "何を載せれば選ばれるのか、社内で意見がまとまらない",
fix: "載せる内容を決める場から同席",
detail:
"他社の見せ方と自社の強みを並べた資料を用意し、決める打ち合わせから参加します",
meta: "初回相談から",
},
{
id: "after",
tag: "公開後",
pain: "公開したあとの相談先がなく、放置する以外になくなった",
fix: "公開後の窓口を残す",
detail:
"実装した人間がそのまま窓口を続け、月次で数字の変化と次の一手をまとめて返します",
meta: "公開後も継続",
},
];
export default function PainEmpathyBridge() {
const [picked, setPicked] = useState<Set<string>>(new Set());
const [bridgeIn, setBridgeIn] = useState(false);
const bridgeRef = useRef<HTMLDivElement>(null);
const headingId = useId();
const count = picked.size;
const shown = count > 0 ? ITEMS.filter((item) => picked.has(item.id)) : ITEMS;
// 接続要素(縦罫+三角)だけスクロールで引く。見出しや行のスタッガー出現は使わない
useEffect(() => {
const node = bridgeRef.current;
if (!node || typeof IntersectionObserver === "undefined") {
setBridgeIn(true);
return;
}
const io = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
setBridgeIn(true);
io.disconnect();
}
},
{ threshold: 0.6 },
);
io.observe(node);
return () => io.disconnect();
}, []);
const toggle = (id: string) => {
setPicked((prev) => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
return (
<section
aria-labelledby={headingId}
className="w-full max-w-5xl bg-white px-1"
>
<header className="grid grid-cols-1 gap-x-8 gap-y-4 md:grid-cols-12">
<div className="md:col-span-7">
<p className="flex items-center gap-2.5 text-[11.5px] font-semibold tracking-[0.06em] text-gray-500">
<span aria-hidden="true" className="h-3.5 w-px bg-amber-600" />
BLK-45 — 課題の棚卸し
</p>
<h2
id={headingId}
className="mt-4 text-[clamp(1.7rem,3.8vw,2.35rem)] leading-[1.35] font-bold tracking-tight text-gray-900"
>
{/* 和文見出しは inline-block で節に切ると、折り返しが必ず読点の位置で起きる */}
<span className="inline-block">こんなお悩み、</span><span className="inline-block">ありませんか</span>
</h2>
</div>
<p className="text-[13.5px] leading-[1.95] font-medium text-gray-600 md:col-span-5 md:self-end">
ご相談の入口はいつも似ています。当てはまるものにチェックを入れると、下の帯がその課題への打ち手だけに変わります。
</p>
</header>
<fieldset className="mt-9 min-w-0 border-0 p-0 md:mt-11">
<legend className="text-[12.5px] leading-[1.9] text-gray-500">
当てはまるものを選んでください(複数可)
</legend>
<div className="relative mt-4">
<ul className="grid list-none grid-cols-1 pl-0">
{ITEMS.map((item) => {
const on = picked.has(item.id);
return (
<li key={item.id} className="relative">
<span
aria-hidden="true"
className="absolute inset-x-0 top-0 h-px bg-gray-200"
/>
<label className="group flex cursor-pointer items-start gap-3.5 py-4 sm:gap-4 sm:py-[18px]">
<input
type="checkbox"
checked={on}
onChange={() => toggle(item.id)}
className="peer sr-only"
/>
<span
aria-hidden="true"
className={`mt-[3px] grid size-[18px] shrink-0 place-items-center border peer-focus-visible:outline-2 peer-focus-visible:outline-offset-[3px] peer-focus-visible:outline-amber-600 ${
on
? "border-amber-600 bg-amber-600"
: "border-gray-300 bg-white group-hover:border-amber-600/60"
}`}
>
<svg
viewBox="0 0 12 12"
className={`size-3 transition-[transform,opacity] duration-200 ease-out motion-reduce:transition-none ${
on ? "scale-100 opacity-100" : "scale-50 opacity-0"
}`}
fill="none"
stroke="#ffffff"
strokeWidth="2"
strokeLinecap="square"
>
<path d="M2.5 6.2 L5 8.6 L9.5 3.6" />
</svg>
</span>
<span
className={`min-w-0 flex-1 text-[14.5px] leading-[1.85] font-medium sm:text-[15px] ${
on ? "text-gray-900" : "text-gray-700"
}`}
>
{item.pain}
</span>
<span
aria-hidden="true"
className={`mt-[5px] shrink-0 text-[11px] leading-none font-semibold tracking-[0.08em] whitespace-nowrap ${
on ? "text-amber-700" : "text-gray-400"
}`}
>
{item.tag}
</span>
</label>
</li>
);
})}
</ul>
<span
aria-hidden="true"
className="absolute inset-x-0 bottom-0 h-px bg-gray-200"
/>
</div>
</fieldset>
<div className="mt-4 flex min-h-6 items-center justify-between gap-4">
<p aria-live="polite" className="text-[12.5px] text-gray-500">
{count > 0 ? (
<>
<span className={`${montserrat.className} tabular-nums`}>
{count}
</span>
件を選択中
</>
) : (
"まだ選択されていません"
)}
</p>
{count > 0 && (
<button
type="button"
onClick={() => setPicked(new Set())}
className="relative text-[12.5px] font-semibold text-gray-500 hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
>
選択を解除
<span
aria-hidden="true"
className="absolute inset-x-0 -bottom-1 h-px bg-gray-300"
/>
</button>
)}
</div>
{/* 共感 → 転換の接続要素(動くのはここだけ) */}
<div
ref={bridgeRef}
aria-hidden="true"
className="flex flex-col items-center pt-7 pb-1"
>
<span
className={`h-10 w-px origin-top bg-amber-600/45 transition-transform duration-700 ease-out motion-reduce:scale-y-100 motion-reduce:transition-none ${
bridgeIn ? "scale-y-100" : "scale-y-0"
}`}
/>
<span
className={`-mt-[5px] size-2 rotate-45 border-r border-b border-amber-600 transition-opacity duration-500 ease-out [transition-delay:600ms] motion-reduce:opacity-100 motion-reduce:transition-none ${
bridgeIn ? "opacity-100" : "opacity-0"
}`}
/>
</div>
{/* 転換帯: ここだけ薄アンバーに温度を変える */}
<div className="border border-amber-600/25 bg-amber-50/70 px-5 py-7 sm:px-8 sm:py-9">
<div className="flex flex-wrap items-baseline justify-between gap-x-8 gap-y-2">
<h3 className="text-[clamp(1.2rem,2.8vw,1.55rem)] leading-[1.4] font-bold tracking-tight text-gray-900">
<span className="inline-block">その課題、</span><span className="inline-block">こう解決します</span>
</h3>
<p className="text-[12px] font-semibold tracking-[0.06em] text-amber-800">
<span className={`${montserrat.className} tabular-nums`}>
{shown.length}
</span>
件の打ち手
</p>
</div>
<p className="mt-3 max-w-2xl text-[13px] leading-[1.95] text-gray-600">
{count > 0
? "選ばれた課題に対応する打ち手だけを残しています。実際の進め方は現状を伺ってから調整します。"
: "上のリストにチェックを入れると、対応する打ち手だけが残ります。いまは全件を表示しています。"}
</p>
<ul className="mt-7 grid list-none grid-cols-1 pl-0">
{shown.map((item) => (
<li key={item.id} className="relative">
<span
aria-hidden="true"
className="absolute inset-x-0 top-0 h-px bg-amber-600/20"
/>
<div className="grid grid-cols-1 gap-x-6 gap-y-1.5 py-5 md:grid-cols-[13rem_minmax(0,1fr)] md:items-baseline">
<div>
<p className="text-[14.5px] leading-[1.6] font-bold text-gray-900">
{item.fix}
</p>
<p className="mt-1 text-[11.5px] leading-none font-semibold tracking-[0.06em] text-amber-800 tabular-nums">
{item.meta}
</p>
</div>
<p className="text-[13.5px] leading-[1.95] text-gray-700">
<span className="sr-only">{item.pain}への打ち手:</span>
{item.detail}
</p>
</div>
</li>
))}
</ul>
<div className="mt-8 flex flex-wrap items-center gap-x-8 gap-y-4 border-t border-amber-600/20 pt-7">
<a
href="#"
className="inline-flex items-center bg-gray-900 px-7 py-3.5 text-[13.5px] font-bold tracking-[0.04em] text-white transition-colors duration-200 hover:bg-amber-700 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 motion-reduce:transition-none"
>
選んだ課題のまま相談する
</a>
<p className="max-w-sm text-[12px] leading-[1.9] text-gray-500">
チェックした内容は問い合わせフォームの相談区分に引き継がれます。相談だけで終わっても構いません。
</p>
</div>
</div>
</section>
);
}
Add to your project via shadcn CLI
npx shadcn@latest add https://designs.first-ch.com/r/pain-empathy-bridge.json