選ばれる理由③ 自社と他の選択肢の比較
依頼先の選択肢と自社を並べて見せる比較ブロック。○×の記号表にせず、同じ観点について短文どうしを突き合わせて差を語る。比較相手は切替チップ(radiogroup・矢印キー対応)で1者ずつ選ぶため、狭い画面でも横スワイプや固定列を使わずに読み切れる。自社側は12カラム上で幅広に取り、本文と同じグリッドを重ねた1枚の帯で薄いアンバーの柱を通す(塗りは常時・立ち上がるのは縦罫1本だけなのでJSに依存しない)。最終行を「向いている場面」にして相手側の利点も書き、特定企業ではなく体制の傾向である旨の注記を添える。画像素材ゼロ・依存ゼロ。
- 追加日:
- 2026-08-28
- 依存:
- なし
- tags
- #block #comparison #strengths #reasons #radiogroup #switcher #hairline #a11y #no-image #no-deps
プレビュー
BLK-44 / 選ばれる理由
どこに頼むかで変わるのは、価格だけではありません
制作会社もフリーランスも、向いている場面があります。当社がどこを引き受け、どこが他の選択肢と違うのかを、同じ6つの観点で並べました。
比較する相手
見積りの出し方何が含まれるか
当社:画面数と機能を1行ずつ並べ、含まないものも同じ表に書いて出します。追加が出た時点で、金額と納期の差分をその場でお伝えします
一般的な制作会社:一式いくら、の形でまとまって出てくることがあります。含む・含まないの線は着手後の打ち合わせで固まりがちです
進行の窓口誰と話すか
当社:相談する相手と実装する人間が同じです。仕様の意図が伝言で薄まらないので、判断が要る場面でも往復が1回で済みます
一般的な制作会社:窓口はディレクターで、実装は別の担当が受け持ちます。細かい仕様は人を介して伝わります
任せられる範囲設計から運用まで
当社:情報設計・デザイン・実装・公開後の計測までを続けて引き受けます。途中で担当が変わらないので、決めた理由がそのまま残ります
一般的な制作会社:工程ごとに強い担当が付きます。一方で、前の工程で何を捨てたかは要約されて次へ渡ります
公開したあと更新と改善
当社:自分で直せる場所を一覧にして渡し、どこまで編集できるかを公開前に合意します。数字を見て次を決める場も一緒に用意します
一般的な制作会社:保守は別契約になることが多く、軽微な差し替えでも見積りと順番待ちが挟まります
もしもの時引き継ぎと停止
当社:サーバー・ドメイン・管理画面の権限は最初からお客様名義にします。作ったものは全て引き渡すので、他社へ移す判断もできます
一般的な制作会社:環境が制作側の管理下にまとまっていると、移管できるかどうかが契約条項に左右されます
向いている場面選び分けの目安
当社:小さく作って公開後に育てたいとき。決める人と作る人の距離が近いほど、判断の速さがそのまま品質になります
一般的な制作会社:関わる部署が多い大規模な案件。分業の厚みと同時進行できる人数が、そのまま強みになります
営業・進行・実装が分かれた分業の体制を前提にしています。※ 比較対象は特定の企業ではなく、体制の一般的な傾向を示したものです。実際の進め方は個々の相手によって異なります
体制について相談する"use client";
import { useEffect, useId, useRef, useState } from "react";
/**
* 選ばれる理由③ 自社と他の選択肢の比較(BLK-44)
*
* 差別化: 既存の比較ブロック(BLK-33)は プラン×機能を1枚の <table> に敷き詰めて
* 全列を同時に見せる記号マトリクスで、狭い幅では先頭列を sticky にしたまま横スワイプさせる。
* 本ブロックは表を使わず、比較相手を1者ずつ選び替えて観点ごとに短文どうしを突き合わせる
* 二者対置の読み物にした(記号ではなく文で差を語る・横スワイプも sticky 列も持たない)。
* 一方的に理由を述べる BLK-16 / 数字1本を宣言する BLK-05 とは、
* 「相手を立てて突き合わせる」点と切替操作の有無で分かれる。
*
* 意図的に外した型(ギャラリー全体で飽和していたもの):
* - 英字キッカー(大文字+広い字間)を置かない。標本IDは和文ラベルと同じ小さな地の文に沈める
* - 角丸をゼロにする(切替チップもCTAも直角)。柱と罫線だけで面を作る
* - 行末の矢印リンクを使わない。締めは注記+直角のCTAボタン1つ
* - 行ごとの translate-y スタッガー出現を使わない。動くのは自社列の縦罫1本だけ
*
* 汎用技法メモ(web-design-playbook 還流用):
*
* 1. 「主役の列」は表のセル単位ではなく、同じグリッドを重ねた1枚の帯で作る
* <table> では列全体の背景を1要素で塗れないためセルごとにクラスを配ることになるが、
* grid なら absolute inset-0 の**同一カラム定義のグリッド**をもう1枚重ね、
* col-start-4 col-span-5 の div を1つ置くだけで途切れない柱になる。
* gap も同じ値にすれば本文カラムとピクセル単位で一致する。行を足しても柱は自動で伸びる。
*
* 2. 柱の塗りは常時・動くのは縦罫だけにする
* 出現アニメーションで塗り自体をスケールさせると、JSが動かない環境で
* 「主役化」そのものが消える。塗りは静的に置き、原点 origin-bottom の
* w-0.5 の縦罫だけを scale-y で立ち上げる。動かすのは transform のみ。
*
* 3. 比較相手は radiogroup(タブではない)
* 切り替わるのはパネルではなく「比較の設定」なので、role="radiogroup" +
* role="radio" の roving tabindex にする。矢印キーは左右と上下の両方を受ける
* (狭い幅では縦積みに見えるため、視覚の並びと操作方向がズレない)。Home/End も足す。
*
* 4. 列見出しに頼らず、各文へ sr-only の話者を付ける
* 狭い幅では列見出しが消えるので、視覚的な列ラベルだけに依存すると
* 音声で「誰の話か」が失われる。各文の先頭に sr-only で「当社:」「相手名:」を置き、
* 見えるラベルは aria-hidden にして二重読み上げを防ぐ。
*
* 5. 切替の演出は key 付け替え+CSS animation、停止は同じ <style> 内で
* transition は切替直後の1フレームを取りこぼす。相手のセルに
* key={相手id + 観点id} を与えて再マウントし、animation-delay を行ごとに
* 40ms ずらして上から差し替わって見せる。fill-mode: backwards で待機中も不可視にする。
* DOM末尾の <style> は head のユーティリティより後勝ちするので、
* prefers-reduced-motion の打ち消しも同じ <style> 内に !important で置く。
*
* 6. 比較の相手を貶さない設計にする
* 最終行を「向いている場面」にして相手側にも利点を書くと、比較の信頼度が上がる。
* あわせて「特定企業ではなく体制の傾向」である旨の注記を必ず添える。
*/
type Rival = {
id: string;
/** 切替チップと列見出しに出る名前 */
label: string;
/** チップの下に添える体制の説明 */
note: string;
};
type Point = {
id: string;
/** 比較の観点(行の見出し) */
axis: string;
/** 観点の補足 */
sub: string;
ours: string;
/** 相手ID → その相手についての一文 */
rivals: Record<string, string>;
};
const RIVALS: Rival[] = [
{
id: "agency",
label: "一般的な制作会社",
note: "営業・進行・実装が分かれた分業の体制",
},
{
id: "freelance",
label: "フリーランス",
note: "個人が単独で請け負う体制",
},
];
const POINTS: Point[] = [
{
id: "estimate",
axis: "見積りの出し方",
sub: "何が含まれるか",
ours: "画面数と機能を1行ずつ並べ、含まないものも同じ表に書いて出します。追加が出た時点で、金額と納期の差分をその場でお伝えします",
rivals: {
agency:
"一式いくら、の形でまとまって出てくることがあります。含む・含まないの線は着手後の打ち合わせで固まりがちです",
freelance:
"相場感で早く答えてもらえる反面、範囲の線引きが口頭のまま進むことがあります",
},
},
{
id: "contact",
axis: "進行の窓口",
sub: "誰と話すか",
ours: "相談する相手と実装する人間が同じです。仕様の意図が伝言で薄まらないので、判断が要る場面でも往復が1回で済みます",
rivals: {
agency:
"窓口はディレクターで、実装は別の担当が受け持ちます。細かい仕様は人を介して伝わります",
freelance:
"本人と直接話せますが、稼働が他案件と重なると返信の間隔が読みにくくなります",
},
},
{
id: "scope",
axis: "任せられる範囲",
sub: "設計から運用まで",
ours: "情報設計・デザイン・実装・公開後の計測までを続けて引き受けます。途中で担当が変わらないので、決めた理由がそのまま残ります",
rivals: {
agency:
"工程ごとに強い担当が付きます。一方で、前の工程で何を捨てたかは要約されて次へ渡ります",
freelance:
"得意な工程は速い一方、設計から運用まで通すと守備範囲を超える場面が出ます",
},
},
{
id: "after",
axis: "公開したあと",
sub: "更新と改善",
ours: "自分で直せる場所を一覧にして渡し、どこまで編集できるかを公開前に合意します。数字を見て次を決める場も一緒に用意します",
rivals: {
agency:
"保守は別契約になることが多く、軽微な差し替えでも見積りと順番待ちが挟まります",
freelance:
"小さな修正は速いものの、体制が1人なので長い窓口としては続きにくい場合があります",
},
},
{
id: "handover",
axis: "もしもの時",
sub: "引き継ぎと停止",
ours: "サーバー・ドメイン・管理画面の権限は最初からお客様名義にします。作ったものは全て引き渡すので、他社へ移す判断もできます",
rivals: {
agency:
"環境が制作側の管理下にまとまっていると、移管できるかどうかが契約条項に左右されます",
freelance:
"連絡が途絶えると引き継ぎ先を探すところから始まります。手元に資産が残っているかで差が出ます",
},
},
{
id: "fit",
axis: "向いている場面",
sub: "選び分けの目安",
ours: "小さく作って公開後に育てたいとき。決める人と作る人の距離が近いほど、判断の速さがそのまま品質になります",
rivals: {
agency:
"関わる部署が多い大規模な案件。分業の厚みと同時進行できる人数が、そのまま強みになります",
freelance:
"作るものと範囲が最初から決まっている単発の依頼。速さと費用で有利になります",
},
},
];
const NOTE =
"※ 比較対象は特定の企業ではなく、体制の一般的な傾向を示したものです。実際の進め方は個々の相手によって異なります";
export function CompareUsVsOthers() {
const headingId = useId();
const switcherId = useId();
const [rivalId, setRivalId] = useState(RIVALS[0].id);
const [drawn, setDrawn] = useState(false);
const stageRef = useRef<HTMLDivElement | null>(null);
const chipRefs = useRef<(HTMLButtonElement | null)[]>([]);
const rival = RIVALS.find((item) => item.id === rivalId) ?? RIVALS[0];
useEffect(() => {
const node = stageRef.current;
if (!node || typeof IntersectionObserver === "undefined") {
setDrawn(true);
return;
}
const observer = new IntersectionObserver(
(entries) => {
if (entries.some((entry) => entry.isIntersecting)) {
setDrawn(true);
observer.disconnect();
}
},
{ threshold: 0.2 },
);
observer.observe(node);
return () => observer.disconnect();
}, []);
function moveTo(next: number) {
const target = (next + RIVALS.length) % RIVALS.length;
setRivalId(RIVALS[target].id);
chipRefs.current[target]?.focus();
}
function handleKeyDown(event: React.KeyboardEvent<HTMLButtonElement>, index: number) {
if (event.key === "ArrowRight" || event.key === "ArrowDown") {
event.preventDefault();
moveTo(index + 1);
} else if (event.key === "ArrowLeft" || event.key === "ArrowUp") {
event.preventDefault();
moveTo(index - 1);
} else if (event.key === "Home") {
event.preventDefault();
moveTo(0);
} else if (event.key === "End") {
event.preventDefault();
moveTo(RIVALS.length - 1);
}
}
return (
<section
aria-labelledby={headingId}
className="@container w-full max-w-[900px] bg-white px-1 text-gray-900"
>
<header className="border-l-2 border-amber-600 pl-5 @2xl:pl-6">
<p className="text-[11px] leading-none text-gray-400">BLK-44 / 選ばれる理由</p>
<h2
id={headingId}
className="mt-3 text-[clamp(1.6rem,3.4vw,2.3rem)] leading-[1.4] font-bold tracking-tight text-gray-900"
>
どこに頼むかで変わるのは、価格だけではありません
</h2>
<p className="mt-4 max-w-[48ch] text-[15px] leading-[1.95] font-medium text-gray-600">
制作会社もフリーランスも、向いている場面があります。当社がどこを引き受け、どこが他の選択肢と違うのかを、同じ6つの観点で並べました。
</p>
</header>
<div className="mt-8 flex flex-col gap-3 @2xl:flex-row @2xl:items-center @2xl:gap-5">
<p id={switcherId} className="text-[12px] font-bold text-gray-500">
比較する相手
</p>
<div role="radiogroup" aria-labelledby={switcherId} className="flex flex-wrap gap-2">
{RIVALS.map((item, index) => {
const selected = item.id === rival.id;
return (
<button
key={item.id}
ref={(node) => {
chipRefs.current[index] = node;
}}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
onClick={() => setRivalId(item.id)}
onKeyDown={(event) => handleKeyDown(event, index)}
className={`border px-4 py-2 text-[13px] font-semibold transition-[color,background-color,border-color] duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 ${
selected
? "border-gray-900 bg-gray-900 text-white"
: "border-gray-300 bg-white text-gray-600 hover:border-gray-900 hover:text-gray-900"
}`}
>
{item.label}
</button>
);
})}
</div>
</div>
<div ref={stageRef} className="relative mt-6 @2xl:mt-8">
{/* 自社列の柱: 本文と同じ12カラム・同じgapのグリッドを重ねて1枚の帯で塗る */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 hidden @2xl:grid @2xl:grid-cols-12 @2xl:gap-x-6"
>
<div className="relative col-span-5 col-start-4 bg-amber-50/70">
<span
className={`absolute inset-y-0 left-0 w-0.5 origin-bottom bg-amber-600 transition-transform duration-[900ms] ease-out motion-reduce:scale-y-100 motion-reduce:transition-none ${
drawn ? "scale-y-100" : "scale-y-0"
}`}
/>
</div>
</div>
<div className="relative hidden pt-5 pb-3 @2xl:grid @2xl:grid-cols-12 @2xl:gap-x-6">
<p className="col-span-3 text-[11px] text-gray-400">比較の観点</p>
<p className="col-span-5 col-start-4 pl-6 text-sm font-bold text-amber-700">当社</p>
<p
key={rival.id}
className="blk44-swap col-span-4 col-start-9 text-sm font-bold text-gray-500"
>
{rival.label}
</p>
</div>
<ul className="relative border-b border-gray-200">
{POINTS.map((point, index) => (
<li
key={point.id}
className="grid grid-cols-1 gap-3 border-t border-gray-200 py-6 @2xl:grid-cols-12 @2xl:gap-x-6 @2xl:gap-y-0 @2xl:py-7"
>
<h3 className="text-[13px] leading-[1.7] font-bold text-gray-900 @2xl:col-span-3">
{point.axis}
<span className="mt-0.5 block text-[11px] font-normal text-gray-400">
{point.sub}
</span>
</h3>
<p className="border-l-2 border-amber-600 bg-amber-50/70 py-3 pl-4 text-[13.5px] leading-[1.95] font-medium text-gray-800 @2xl:col-span-5 @2xl:col-start-4 @2xl:border-l-0 @2xl:bg-transparent @2xl:py-0 @2xl:pr-6 @2xl:pl-6">
<span className="sr-only">当社:</span>
<span
aria-hidden="true"
className="mb-1 block text-[11px] font-bold text-amber-700 @2xl:hidden"
>
当社
</span>
{point.ours}
</p>
<p
key={`${rival.id}-${point.id}`}
style={{ animationDelay: `${index * 40}ms` }}
className="blk44-swap border-l border-gray-200 pl-4 text-[12.5px] leading-[1.95] text-gray-500 @2xl:col-span-4 @2xl:col-start-9 @2xl:border-l-0 @2xl:pl-0"
>
<span className="sr-only">{rival.label}:</span>
<span
aria-hidden="true"
className="mb-1 block text-[11px] font-bold text-gray-400 @2xl:hidden"
>
{rival.label}
</span>
{point.rivals[rival.id]}
</p>
</li>
))}
</ul>
</div>
<div className="mt-8 flex flex-col gap-6 @2xl:flex-row @2xl:items-end @2xl:justify-between">
<p className="max-w-[52ch] text-[11px] leading-[1.9] text-gray-400">
{rival.note}を前提にしています。{NOTE}
</p>
<a
href="#contact"
className="inline-flex shrink-0 items-center justify-center bg-amber-600 px-6 py-3 text-sm font-bold text-white transition-[background-color] duration-150 hover:bg-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
>
体制について相談する
</a>
</div>
<style>{`
@keyframes blk44-swap {
from { opacity: 0; transform: translateX(6px); }
to { opacity: 1; transform: none; }
}
.blk44-swap { animation: blk44-swap 260ms ease-out backwards; }
@media (prefers-reduced-motion: reduce) {
.blk44-swap { animation: none !important; }
}
`}</style>
</section>
);
}
export default CompareUsVsOthers;
shadcn CLI でプロジェクトに追加
npx shadcn@latest add https://designs.first-ch.com/r/compare-us-vs-others.json