Works — Case-study Grid
A works / case-study index block with high information density. Instead of a row of shadowed cards, entries sit in a hairline 'ledger' (gap-px on a gray fill), each with a faux logo lockup (monogram + wordmark, zero images), an industry tag, and a giant ghost index numeral for rhythm. On hover (and keyboard focus) the excerpt fades in and the arrow slides; the excerpt's height is always reserved so nothing reflows. An industry filter toggles cells via hidden (no reflow animation) and announces the count via aria-live. Staggered reveal on scroll-in via IntersectionObserver, instant final state under prefers-reduced-motion.
- Added:
- 2026-07-29
- Dependencies:
- None
- tags
- #block #works #case-study #portfolio #grid #ledger #hairline #filter #hover-reveal #no-image #ready-crew
Preview
BLK-06 — Works / 制作実績
つくった先の、変化で選ばれたい。
業種も規模もさまざまでも、共通しているのは「公開して終わりにしない」こと。 近年の制作事例から抜粋してご紹介します。
- 01医療Midori Clinic
初診の不安を減らす、予約特化のクリニックサイト
症状別FAQと医師紹介を前面に。予約導線はLINEへ一本化し、電話に頼らず迷わせない設計に。
コーポレート / 予約2026制作事例 - 02飲食Kanade Coffee
焙煎の物語で選ばれる、自家焙煎コーヒー店
豆ごとのストーリーとオンライン購入を接続。写真に頼らず、余白と罫線で香りの世界観を立てる。
ブランドサイト / EC2025制作事例 - 03士業Aoi Law Office
相談のハードルを下げた、法律事務所のリニューアル
分野別の入口と料金の透明化で、初回相談までの距離を短く。派手さより静かな信頼を設計。
コーポレート2026制作事例 - 04コーポレートTsubame Works
技術力を語り直す、町工場のリブランディング
加工事例を数字と工程で見せ、BtoBの引き合いに応える骨太な情報設計。採用ページも同時整備。
コーポレート / 採用2025制作事例 - 05飲食Umi to Tsuki
予約が主役の、海辺のレストラン
コースと空席状況を軸に構成。世界観は罫線と間で表現し、料理写真は要所のみに絞る。
ブランドサイト / 予約2026制作事例 - 06コーポレートLumo Studio
採用に効く、美容ブランドの発信基地
サービスと採用を一本の物語に束ねる。スタッフの言葉を軸に、応募までを滑らかにつなぐ。
コーポレート / 採用2026制作事例
"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",
});
/**
* 実績・事例(グリッド) / Works — Case-study Grid
* 参考: 他社サイトの事例紹介に見られる「事例カードの情報密度(ロゴ・業種タグ・抜粋)と一覧の律動」を
* ブランド再構築(コピーではない)。画像素材ゼロ。
*
* 汎用技法メモ(web-design-playbook 還流用):
* - 脱カード羅列: 影付きの浮いたカードを並べるのではなく、grid の gap-px を bg-gray-200 の上に敷いて
* 「1px 罫線で仕切られたレジャー(台帳)」を作る(cell は bg-white)。外周は rounded-xl + border で囲む。
* これで箱の反復ではなく hairline 罫線+余白のグルーピングになり、標本シートの律動が出る。
* - 画像なしのロゴ表現: モノグラム(頭文字を入れた四角マーク)+欧文社名の「ロゴ・ロックアップ」で
* 実ロゴ画像の代わりを務める。背景に巨大なゴースト番号(text-gray-100)を敷いて一覧のリズムを作る。
* - ホバーで抜粋(リフローさせない): 抜粋は常に DOM に置き高さを予約したまま、desktop 初期は opacity-0/-translate-y、
* group-hover / group-focus-visible で opacity-100/translate-y-0 にフェードイン。height/max-height は動かさない
* (transform/opacity のみ=reduced-motion で確実に無効化できる)。モバイルは hover が無いので常時表示。
* - フィルタはリフローアニメを付けない: 業種で絞り込むとき非該当 cell に hidden を付けるだけ(表示/非表示の
* トグルであってアニメーションではない)。色/背景の変化はトランジションを付けず即時に切り替える。
* - リビール & a11y: IntersectionObserver(threshold 0.15)で画面内に入ったら reveal 開始、以後 disconnect。
* prefers-reduced-motion 時は初期状態から即最終状態。フィルタボタンは aria-pressed、件数は aria-live で通知。
*/
type Work = {
no: string;
client: string;
monogram: string;
category: string;
title: string;
excerpt: string;
services: string;
year: string;
};
const works: Work[] = [
{
no: "01",
client: "Midori Clinic",
monogram: "MC",
category: "医療",
title: "初診の不安を減らす、予約特化のクリニックサイト",
excerpt:
"症状別FAQと医師紹介を前面に。予約導線はLINEへ一本化し、電話に頼らず迷わせない設計に。",
services: "コーポレート / 予約",
year: "2026",
},
{
no: "02",
client: "Kanade Coffee",
monogram: "KC",
category: "飲食",
title: "焙煎の物語で選ばれる、自家焙煎コーヒー店",
excerpt:
"豆ごとのストーリーとオンライン購入を接続。写真に頼らず、余白と罫線で香りの世界観を立てる。",
services: "ブランドサイト / EC",
year: "2025",
},
{
no: "03",
client: "Aoi Law Office",
monogram: "AL",
category: "士業",
title: "相談のハードルを下げた、法律事務所のリニューアル",
excerpt:
"分野別の入口と料金の透明化で、初回相談までの距離を短く。派手さより静かな信頼を設計。",
services: "コーポレート",
year: "2026",
},
{
no: "04",
client: "Tsubame Works",
monogram: "TW",
category: "コーポレート",
title: "技術力を語り直す、町工場のリブランディング",
excerpt:
"加工事例を数字と工程で見せ、BtoBの引き合いに応える骨太な情報設計。採用ページも同時整備。",
services: "コーポレート / 採用",
year: "2025",
},
{
no: "05",
client: "Umi to Tsuki",
monogram: "UT",
category: "飲食",
title: "予約が主役の、海辺のレストラン",
excerpt:
"コースと空席状況を軸に構成。世界観は罫線と間で表現し、料理写真は要所のみに絞る。",
services: "ブランドサイト / 予約",
year: "2026",
},
{
no: "06",
client: "Lumo Studio",
monogram: "LS",
category: "コーポレート",
title: "採用に効く、美容ブランドの発信基地",
excerpt:
"サービスと採用を一本の物語に束ねる。スタッフの言葉を軸に、応募までを滑らかにつなぐ。",
services: "コーポレート / 採用",
year: "2026",
},
];
const ALL = "すべて";
const categories = [ALL, ...Array.from(new Set(works.map((w) => w.category)))];
/** index ごとに変わるアンバーの装飾図形(plate 内・装飾なので aria-hidden) */
function PlateAccent({ index }: { index: number }) {
const variant = index % 3;
if (variant === 0) {
return (
<span
aria-hidden="true"
className="absolute -top-6 -left-6 size-24 rounded-full border-[7px] border-amber-500/25 transition-transform duration-500 ease-out group-hover:translate-x-2 group-hover:translate-y-1 motion-reduce:transition-none"
/>
);
}
if (variant === 1) {
return (
<span
aria-hidden="true"
className="absolute -right-5 -bottom-5 size-20 rotate-12 bg-amber-500/15 transition-transform duration-500 ease-out group-hover:-translate-x-2 group-hover:-rotate-3 motion-reduce:transition-none"
/>
);
}
return (
<span
aria-hidden="true"
className="absolute top-1/2 -left-3 h-16 w-3 -translate-y-1/2 bg-amber-500/25 transition-transform duration-500 ease-out group-hover:translate-x-2 motion-reduce:transition-none"
/>
);
}
export default function WorkGrid() {
const rootRef = useRef<HTMLElement>(null);
const [active, setActive] = useState(false);
const [filter, setFilter] = useState(ALL);
// 画面内に入ったら 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.15 }
);
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"
}`;
const visibleCount =
filter === ALL
? works.length
: works.filter((w) => w.category === filter).length;
return (
<section
ref={rootRef}
aria-labelledby="work-grid-heading"
className="w-full max-w-6xl 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-06 — Works / 制作実績
</p>
<h2
id="work-grid-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>
{/* 業種フィルタ */}
<div
className={`mt-8 flex flex-wrap items-center gap-2 ${reveal}`}
style={{ transitionDelay: "240ms" }}
>
<span className="sr-only" role="status" aria-live="polite">
{filter === ALL ? "すべての事例" : `業種:${filter}`}を{visibleCount}件表示中
</span>
{categories.map((c) => {
const on = filter === c;
return (
<button
key={c}
type="button"
aria-pressed={on}
onClick={() => setFilter(c)}
className={`rounded-full border px-4 py-1.5 text-[13px] font-semibold focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 ${
on
? "border-gray-900 bg-gray-900 text-white"
: "border-gray-200 bg-white text-gray-600 hover:border-gray-400 hover:text-gray-900"
}`}
>
{c}
</button>
);
})}
</div>
{/* 事例レジャー(gap-px の hairline グリッド/カード羅列を回避) */}
<ul
className={`mt-6 grid list-none grid-cols-1 gap-px overflow-hidden rounded-xl border border-gray-200 bg-gray-200 sm:grid-cols-2 lg:grid-cols-3 ${reveal}`}
style={{ transitionDelay: "320ms" }}
>
{works.map((w, i) => {
const hidden = filter !== ALL && w.category !== filter;
return (
<li key={w.no} className={hidden ? "hidden" : ""}>
<a
href="#"
className="group flex h-full flex-col bg-white p-5 hover:bg-gray-50 focus-visible:relative focus-visible:z-10 focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-amber-600 sm:p-6"
>
{/* メタ行: 通し番号 + 業種タグ */}
<div className="flex items-center justify-between">
<span
className={`${montserrat.className} text-[11px] font-bold tracking-[0.2em] text-gray-400 tabular-nums`}
>
{w.no}
</span>
<span className="rounded-full border border-amber-200 bg-amber-50 px-2.5 py-0.5 text-[11px] font-semibold text-amber-700">
{w.category}
</span>
</div>
{/* plate: 疑似ロゴ・ロックアップ + ゴースト番号 + アンバー装飾 */}
<div className="relative mt-4 aspect-[16/10] overflow-hidden rounded-md border border-gray-200 bg-white">
<span
aria-hidden="true"
className="absolute inset-0"
style={{
backgroundImage:
"radial-gradient(#e5e7eb 1px, transparent 1px)",
backgroundSize: "14px 14px",
}}
/>
<PlateAccent index={i} />
<span
aria-hidden="true"
className={`${montserrat.className} pointer-events-none absolute -right-1 -bottom-7 text-[6.5rem] leading-none font-extrabold text-gray-100 tabular-nums select-none`}
>
{w.no}
</span>
<div className="absolute inset-0 flex items-center justify-center">
<div className="flex items-center gap-2.5 transition-transform duration-500 ease-out group-hover:scale-[1.04] motion-reduce:transition-none">
<span
className={`${montserrat.className} grid size-9 place-items-center rounded bg-gray-900 text-[13px] font-bold tracking-tight text-white`}
aria-hidden="true"
>
{w.monogram}
</span>
<span
className={`${montserrat.className} text-sm font-bold tracking-wide text-gray-900 uppercase`}
>
{w.client}
</span>
</div>
</div>
</div>
{/* タイトル */}
<h3 className="mt-4 text-[15px] leading-[1.6] font-bold text-gray-900 group-hover:text-amber-700">
{w.title}
</h3>
{/* 抜粋: 高さは予約したまま hover でフェードイン(リフローなし) */}
<p className="mt-2 line-clamp-2 text-[13px] leading-[1.75] text-gray-500 transition-[opacity,transform] duration-300 ease-out translate-y-0 opacity-100 motion-reduce:transition-none sm:-translate-y-1 sm:opacity-0 sm:group-hover:translate-y-0 sm:group-hover:opacity-100 sm:group-focus-visible:translate-y-0 sm:group-focus-visible:opacity-100">
{w.excerpt}
</p>
{/* フッター行: 提供内容・年 + View 矢印 */}
<div className="mt-auto flex items-center justify-between gap-3 pt-4">
<span className="text-[11px] font-medium tracking-wide text-gray-400">
{w.services}
<span aria-hidden="true" className="mx-1.5 text-gray-300">
/
</span>
<span className={`${montserrat.className} tabular-nums`}>
{w.year}
</span>
</span>
<span className="inline-flex items-center gap-1 text-[12px] font-semibold text-amber-700">
制作事例
<span
aria-hidden="true"
className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1 motion-reduce:transition-none"
>
→
</span>
</span>
</div>
</a>
</li>
);
})}
</ul>
{/* テキストCTA(下線ワイプ・focus-visible) */}
<div className={`mt-10 ${reveal}`} style={{ transitionDelay: "400ms" }}>
<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>
);
}
Add to your project via shadcn CLI
npx shadcn@latest add https://designs.first-ch.com/r/work-grid.json