First CH Designs
BLK-44Blocks

選ばれる理由③ 自社と他の選択肢の比較

依頼先の選択肢と自社を並べて見せる比較ブロック。○×の記号表にせず、同じ観点について短文どうしを突き合わせて差を語る。比較相手は切替チップ(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