First CH Designs
BLK-15Blocks

実績・事例(1件フィーチャー/重ねスタック)

実績を一覧するのではなく、直近の1件だけを大きく掘り下げるブロック。左は画面キャプチャ3枚をわずかにずらして重ねたスタック(画像素材ゼロ・ブラウザクロム+ワイヤーフレームで自作)で、hoverすると扇状に開く。右はクライアントのロックアップとhairline罫線の諸元リスト。下は課題→打ち手→結果の順に読ませる縦レジャーで、結果の行にだけ数値3点を添える。カテゴリ絞り込みは持たせず、グリッド一覧(BLK-06 work-grid)の対として使う。重なりはtranslate/rotate/scaleだけで作るためリフローが起きず、IntersectionObserverのスタッガー・リビールを含め全モーションがmotion-reduceで初期状態=最終状態に畳まれる(他社サイトに見られる、実績1件を重ねキャプチャで見せる構図を参考にブランド再構築・2026-08-08)。

追加日:
2026-08-08
依存:
なし
tags
#block #works #case-study #featured #stack #screens #hairline #hover-reveal #no-image

プレビュー

BLK-15 — Featured Work / 実績1件

一覧では伝わらない、一件の中身を。

何をつくったかより、何が変わったか。直近の一件を、課題・打ち手・結果の順で公開しています。

トップページ施工事例お問い合わせ
Nagi Koumuten架空の事例です

施工事例を軸に組み直した、地域工務店のコーポレートサイト

業種
建設・工務店
提供内容
サイト設計 / 制作 / CMS導入
期間
10週間
公開
2026
  1. 01Issue課題

    問い合わせの前に、比較で離脱していた

    施工の質は評価されているのに、サイトには会社概要と電話番号しかなく、相見積もりの段階で候補から外れていた。写真も社内に散在し、営業担当が個別にメールで送っていた。

  2. 02Approach打ち手

    事例を資産に変え、比較の土俵に乗せる

    施工事例を「工期・費用帯・課題・仕上がり」の型に揃えて再構成し、検索と絞り込みを用意。見積依頼フォームは3項目まで削り、事例ページの末尾からそのまま送れる導線に置き換えた。

  3. 03Result結果

    公開3か月で、問い合わせの質が変わった

    件数だけでなく「事例を見た前提の相談」が増え、初回訪問での提案精度が上がった。事例の追加は社内で完結し、更新のたびに制作会社へ依頼する必要もなくなった。

    問い合わせ件数
    2.4
    直帰率
    -18pt
    公開からの経過
    3か月
"use client";

import { useEffect, useRef, useState } from "react";
import { Montserrat } from "next/font/google";

const montserrat = Montserrat({
  weight: ["600", "700", "800"],
  subsets: ["latin"],
  display: "swap",
});

/**
 * 実績・事例(1件フィーチャー/重ねスタック) / Featured Work — Stacked Screens
 * 参考: 他社サイトに見られる「実績1件を画面キャプチャの重ねスタックで見せる」構図を
 * ブランド再構築(コピーではない)。テキスト・数値・社名はすべて架空のオリジナル。画像素材ゼロ。
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - 重ねスタック(Stacked screens): 同じ枠を3枚 absolute で完全に重ね、index 由来の
 *   translate / rotate / scale だけで奥行きを作る。レイアウトは一切動かないので、hover や
 *   reduced-motion での切り替えが transform の付け外しだけで完結する(リフローゼロ)。
 *   奥のプレートほど scale を落とし opacity も落とすと、影を濃くしなくても前後関係が出る。
 * - 扇状の展開: group-hover で各プレートの translate/rotate を「外向きに」増やすと、
 *   カードめくりのような気配が出る。z 順は DOM 順(後に書いたものが上)で決め、z-index を
 *   動的に触らない=再描画とフォーカス順の破綻を避ける。
 * - 画像ゼロの画面モック: ブラウザ・クロム(信号ドット+URLピル)+ワイヤーフレーム
 *   (bg-gray-100/200 のバーと amber のブロック)で「サイトの画面」を表現する。
 *   実キャプチャを貼らないので機密・権利の持ち込みが起きず、テンプレとして配布できる。
 * - 1件フィーチャーの読ませ方: グリッド一覧(BLK-06)の対として、課題 → 打ち手 → 結果 を
 *   縦のレジャー(hairline 罫線で仕切った定義リスト)で順に読ませる。カードで囲まない。
 * - リビール & a11y: IntersectionObserver(threshold 0.2)で画面内に入ったら開始し disconnect。
 *   モーションは transform/opacity のみ・すべて motion-reduce: で初期状態=最終状態に畳む。
 */

type Screen = {
  label: string;
  caption: string;
  /** 前面(0) → 奥(2) の重ね順 */
  depth: 0 | 1 | 2;
};

const screens: Screen[] = [
  { label: "CONTACT", caption: "お問い合わせ", depth: 2 },
  { label: "WORKS", caption: "施工事例", depth: 1 },
  { label: "TOP", caption: "トップページ", depth: 0 },
];

type Step = {
  no: string;
  en: string;
  ja: string;
  title: string;
  body: string;
};

const steps: Step[] = [
  {
    no: "01",
    en: "Issue",
    ja: "課題",
    title: "問い合わせの前に、比較で離脱していた",
    body: "施工の質は評価されているのに、サイトには会社概要と電話番号しかなく、相見積もりの段階で候補から外れていた。写真も社内に散在し、営業担当が個別にメールで送っていた。",
  },
  {
    no: "02",
    en: "Approach",
    ja: "打ち手",
    title: "事例を資産に変え、比較の土俵に乗せる",
    body: "施工事例を「工期・費用帯・課題・仕上がり」の型に揃えて再構成し、検索と絞り込みを用意。見積依頼フォームは3項目まで削り、事例ページの末尾からそのまま送れる導線に置き換えた。",
  },
  {
    no: "03",
    en: "Result",
    ja: "結果",
    title: "公開3か月で、問い合わせの質が変わった",
    body: "件数だけでなく「事例を見た前提の相談」が増え、初回訪問での提案精度が上がった。事例の追加は社内で完結し、更新のたびに制作会社へ依頼する必要もなくなった。",
  },
];

type Metric = {
  value: string;
  unit: string;
  label: string;
};

const metrics: Metric[] = [
  { value: "2.4", unit: "倍", label: "問い合わせ件数" },
  { value: "-18", unit: "pt", label: "直帰率" },
  { value: "3", unit: "か月", label: "公開からの経過" },
];

/** 画面キャプチャの代替(画像ゼロ・ワイヤーフレームのみ)。装飾なので aria-hidden。 */
function ScreenMock({ depth }: { depth: 0 | 1 | 2 }) {
  return (
    <div aria-hidden="true" className="flex h-full flex-col">
      {/* ブラウザ・クロム */}
      <div className="flex items-center gap-1.5 border-b border-gray-200 bg-gray-50 px-2.5 py-1.5">
        <span className="size-1.5 rounded-full bg-gray-300" />
        <span className="size-1.5 rounded-full bg-gray-300" />
        <span className="size-1.5 rounded-full bg-gray-300" />
        <span className="ml-1.5 h-2 w-1/2 rounded-full bg-gray-200" />
      </div>
      {/* 画面ごとに違うワイヤーフレーム */}
      {depth === 0 ? (
        <div className="flex flex-1 flex-col gap-2 p-3">
          <span className="h-2 w-1/4 rounded-full bg-amber-500/70" />
          <span className="h-3.5 w-4/5 rounded-sm bg-gray-800/85" />
          <span className="h-3.5 w-3/5 rounded-sm bg-gray-800/85" />
          <span className="mt-1 h-1.5 w-2/3 rounded-full bg-gray-200" />
          <span className="h-1.5 w-1/2 rounded-full bg-gray-200" />
          <span className="mt-auto flex items-center gap-2">
            <span className="h-4 w-16 rounded-sm bg-amber-500/85" />
            <span className="h-4 w-12 rounded-sm border border-gray-300" />
          </span>
        </div>
      ) : depth === 1 ? (
        <div className="flex flex-1 flex-col gap-2 p-3">
          <span className="h-2 w-1/3 rounded-full bg-gray-300" />
          <span className="grid flex-1 grid-cols-3 gap-1.5">
            {[0, 1, 2, 3, 4, 5].map((i) => (
              <span
                key={i}
                className={`rounded-sm ${i === 0 ? "bg-amber-500/20" : "bg-gray-100"}`}
              />
            ))}
          </span>
        </div>
      ) : (
        <div className="flex flex-1 flex-col gap-2 p-3">
          <span className="h-2 w-2/5 rounded-full bg-gray-300" />
          <span className="h-5 w-full rounded-sm bg-gray-100" />
          <span className="h-5 w-full rounded-sm bg-gray-100" />
          <span className="mt-auto h-4 w-20 rounded-sm bg-amber-500/70" />
        </div>
      )}
    </div>
  );
}

export default function WorkFeatureStack() {
  const rootRef = useRef<HTMLElement>(null);
  const [active, setActive] = useState(false);

  // 画面内に入ったら reveal 開始(IO 非対応環境は即開始)。reduced-motion は CSS 側の
  // motion-reduce: ユーティリティで初期状態=最終状態に畳むので JS 判定は不要。
  useEffect(() => {
    const el = rootRef.current;
    if (!el) return;
    if (typeof IntersectionObserver === "undefined") {
      const id = requestAnimationFrame(() => setActive(true));
      return () => cancelAnimationFrame(id);
    }
    const io = new IntersectionObserver(
      (entries) => {
        if (entries.some((e) => e.isIntersecting)) {
          setActive(true);
          io.disconnect();
        }
      },
      { threshold: 0.2 }
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const reveal = `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
    active
      ? "translate-y-0 opacity-100"
      : "translate-y-6 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
  }`;

  // 奥のプレートほど「上へ・小さく・薄く」。hover では外向きに開く(transform のみ)。
  const plate: Record<0 | 1 | 2, string> = {
    2: "-translate-x-4 -translate-y-8 scale-[0.88] -rotate-3 opacity-70 group-hover:-translate-x-8 group-hover:-translate-y-11 group-hover:-rotate-6",
    1: "translate-x-4 -translate-y-4 scale-[0.94] rotate-[1.5deg] opacity-90 group-hover:translate-x-8 group-hover:-translate-y-6 group-hover:rotate-3",
    0: "translate-y-0 scale-100 rotate-0 opacity-100 group-hover:translate-y-1",
  };

  return (
    <section
      ref={rootRef}
      aria-labelledby="work-feature-stack-heading"
      className="w-full max-w-5xl bg-white px-1"
    >
      {/* ヘッダー(宣言) */}
      <header className="max-w-2xl">
        <p
          className={`${montserrat.className} text-xs font-bold tracking-[0.25em] text-amber-600 uppercase ${reveal}`}
          style={{ transitionDelay: "0ms" }}
        >
          BLK-15 — Featured Work / 実績1件
        </p>
        <h2
          id="work-feature-stack-heading"
          className={`mt-4 text-[clamp(1.8rem,4vw,2.6rem)] leading-[1.25] font-bold tracking-tight text-gray-900 ${reveal}`}
          style={{ transitionDelay: "80ms" }}
        >
          一覧では伝わらない、
          <span className="relative whitespace-nowrap">
            一件の中身
            <span
              aria-hidden="true"
              className="absolute -bottom-1 left-0 h-[3px] w-full bg-amber-500/70"
            />
          </span>
          を。
        </h2>
        <p
          className={`mt-5 text-[15px] leading-[1.9] font-medium text-gray-600 ${reveal}`}
          style={{ transitionDelay: "160ms" }}
        >
          何をつくったかより、何が変わったか。直近の一件を、課題・打ち手・結果の順で公開しています。
        </p>
      </header>

      {/* 事例本体: 重ねスタック + 諸元 */}
      <article className="mt-10 grid grid-cols-1 items-start gap-10 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,1fr)] lg:gap-8">
        {/* 重ねスタック(画像ゼロの画面モック3枚) */}
        <figure
          className={`group m-0 ${reveal}`}
          style={{ transitionDelay: "240ms" }}
        >
          <div className="relative aspect-[16/11] w-full">
            <span
              aria-hidden="true"
              className="absolute inset-0 rounded-lg"
              style={{
                backgroundImage: "radial-gradient(#e5e7eb 1px, transparent 1px)",
                backgroundSize: "14px 14px",
              }}
            />
            <span
              aria-hidden="true"
              className="absolute -right-4 -bottom-6 size-24 rounded-full border-[7px] border-amber-500/20 transition-transform duration-700 ease-out group-hover:translate-x-2 group-hover:translate-y-1 motion-reduce:transition-none"
            />
            {screens.map((s) => (
              <div
                key={s.label}
                aria-hidden="true"
                className={`absolute inset-x-[6%] top-[10%] bottom-[10%] overflow-hidden rounded-md border border-gray-200 bg-white shadow-[0_6px_18px_-10px_rgba(15,23,42,0.35)] transition-transform duration-700 ease-out motion-reduce:transition-none ${plate[s.depth]}`}
              >
                <ScreenMock depth={s.depth} />
                <span
                  className={`${montserrat.className} absolute top-1.5 right-2 text-[9px] font-bold tracking-[0.18em] text-gray-300`}
                >
                  {s.label}
                </span>
              </div>
            ))}
          </div>
          <figcaption className="mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] font-medium text-gray-400">
            {screens
              .slice()
              .reverse()
              .map((s, i) => (
                <span key={s.label} className="inline-flex items-center gap-3">
                  {i > 0 ? (
                    <span aria-hidden="true" className="text-gray-300">
                      /
                    </span>
                  ) : null}
                  {s.caption}
                </span>
              ))}
          </figcaption>
        </figure>

        {/* 諸元(hairline 罫線の定義リスト・カードで囲まない) */}
        <div className={reveal} style={{ transitionDelay: "320ms" }}>
          <div className="flex items-center gap-2.5">
            <span
              className={`${montserrat.className} grid size-10 place-items-center rounded bg-gray-900 text-[14px] font-bold tracking-tight text-white`}
              aria-hidden="true"
            >
              NK
            </span>
            <span>
              <span
                className={`${montserrat.className} block text-[15px] font-bold tracking-wide text-gray-900 uppercase`}
              >
                Nagi Koumuten
              </span>
              <span className="mt-0.5 block text-[11px] font-medium text-gray-400">
                架空の事例です
              </span>
            </span>
          </div>

          <h3 className="mt-5 text-[19px] leading-[1.6] font-bold text-gray-900">
            施工事例を軸に組み直した、地域工務店のコーポレートサイト
          </h3>

          <dl className="mt-6 border-t border-gray-200 text-[13px]">
            {[
              { k: "業種", v: "建設・工務店" },
              { k: "提供内容", v: "サイト設計 / 制作 / CMS導入" },
              { k: "期間", v: "10週間" },
              { k: "公開", v: "2026" },
            ].map((row) => (
              <div
                key={row.k}
                className="flex items-baseline justify-between gap-4 border-b border-gray-200 py-2.5"
              >
                <dt className="shrink-0 font-semibold whitespace-nowrap text-gray-400">
                  {row.k}
                </dt>
                <dd
                  className={`m-0 text-right leading-[1.7] font-medium text-gray-800 ${
                    row.k === "公開" ? `${montserrat.className} tabular-nums` : ""
                  }`}
                >
                  {row.v}
                </dd>
              </div>
            ))}
          </dl>
        </div>
      </article>

      {/* 課題 → 打ち手 → 結果(縦レジャー) */}
      <ol className="mt-12 list-none border-t border-gray-200 pl-0">
        {steps.map((s, i) => (
          <li
            key={s.no}
            className={`grid grid-cols-1 gap-x-6 gap-y-2 border-b border-gray-200 py-6 sm:grid-cols-[7rem_minmax(0,1fr)] ${reveal}`}
            style={{ transitionDelay: `${400 + i * 90}ms` }}
          >
            <div className="flex items-center gap-2.5 sm:flex-col sm:items-start sm:gap-1">
              <span
                className={`${montserrat.className} text-[11px] font-bold tracking-[0.2em] text-amber-600 tabular-nums`}
              >
                {s.no}
              </span>
              <span
                className={`${montserrat.className} text-[11px] font-bold tracking-[0.18em] text-gray-300 uppercase`}
              >
                {s.en}
              </span>
              <span className="text-[13px] font-bold whitespace-nowrap text-gray-900 sm:mt-1">
                {s.ja}
              </span>
            </div>
            <div>
              <h4 className="text-[16px] leading-[1.6] font-bold text-gray-900">
                {s.title}
              </h4>
              <p className="mt-2 text-[14px] leading-[1.9] text-gray-600">
                {s.body}
              </p>

              {/* 結果の行だけ数値を添える */}
              {s.no === "03" ? (
                <dl className="mt-5 grid grid-cols-1 gap-px overflow-hidden rounded-lg border border-gray-200 bg-gray-200 sm:grid-cols-3">
                  {metrics.map((m) => (
                    // dt→dd の DOM 順を保ったまま flex-col-reverse で数値を上に出す
                    <div
                      key={m.label}
                      className="flex flex-col-reverse bg-white px-4 py-3.5"
                    >
                      <dt className="mt-2 text-[11px] font-semibold tracking-wide text-gray-500">
                        {m.label}
                      </dt>
                      <dd
                        className={`${montserrat.className} m-0 text-[26px] leading-none font-extrabold tracking-tight text-gray-900 tabular-nums`}
                      >
                        {m.value}
                        <span className="ml-1 text-[13px] font-bold text-amber-600">
                          {m.unit}
                        </span>
                      </dd>
                    </div>
                  ))}
                </dl>
              ) : null}
            </div>
          </li>
        ))}
      </ol>

      {/* テキストCTA(下線ワイプ・focus-visible) */}
      <div className={`mt-10 ${reveal}`} style={{ transitionDelay: "700ms" }}>
        <a
          href="#"
          className="group inline-flex items-center gap-2 text-sm font-semibold text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
        >
          <span className="relative">
            この事例の詳細を読む
            <span className="absolute -bottom-1 left-0 h-px w-full origin-left scale-x-100 bg-gray-900 transition-transform duration-300 ease-out group-hover:scale-x-0 motion-reduce:transition-none" />
            <span className="absolute -bottom-1 left-0 h-px w-full origin-right scale-x-0 bg-amber-600 transition-transform delay-150 duration-300 ease-out group-hover:origin-left group-hover:scale-x-100 motion-reduce:transition-none" />
          </span>
          <span
            aria-hidden="true"
            className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1 motion-reduce:transition-none"
          >
            →
          </span>
        </a>
      </div>
    </section>
  );
}

shadcn CLI でプロジェクトに追加

npx shadcn@latest add https://designs.first-ch.com/r/work-feature-stack.json