施工事例を軸に組み直した、地域工務店のコーポレートサイト
- 業種
- 建設・工務店
- 提供内容
- サイト設計 / 制作 / CMS導入
- 期間
- 10週間
- 公開
- 2026
A works block that drills into a single recent project instead of listing many. On the left, three screen captures stack with slight offsets (zero image assets — browser chrome plus wireframes drawn in markup) and fan open on hover. On the right, a client lockup and a hairline spec list. Below, a vertical ledger reads issue → approach → result, with three metrics attached only to the result row. No category filter: this is the counterpart to the grid index (BLK-06 work-grid). Depth comes purely from translate/rotate/scale so nothing reflows, and every motion — including the IntersectionObserver staggered reveal — collapses to its final state under prefers-reduced-motion.
BLK-15 — Featured Work / 実績1件
何をつくったかより、何が変わったか。直近の一件を、課題・打ち手・結果の順で公開しています。
施工の質は評価されているのに、サイトには会社概要と電話番号しかなく、相見積もりの段階で候補から外れていた。写真も社内に散在し、営業担当が個別にメールで送っていた。
施工事例を「工期・費用帯・課題・仕上がり」の型に揃えて再構成し、検索と絞り込みを用意。見積依頼フォームは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>
);
}
npx shadcn@latest add https://designs.first-ch.com/r/work-feature-stack.json