First CH Designs
BLK-06Blocks

実績・事例(グリッド)

制作実績・導入事例を情報密度高く一覧するブロック。影付きカードの羅列ではなく、gap-pxのhairline罫線で仕切った「レジャー(台帳)」で並べ、各事例に疑似ロゴ・ロックアップ(モノグラム+欧文社名/画像ゼロ)・業種タグ・巨大ゴースト番号を与えて一覧の律動を作る。ホバー(+キーボードfocus)で抜粋がフェードイン/矢印がスライドし、抜粋の高さは常に予約してリフローを起こさない。業種フィルタはhiddenトグルでリフローアニメを付けず即時に絞り込み、件数はaria-liveで通知。IntersectionObserverでスクロールインするとスタッガーでリビール、prefers-reduced-motion時は即最終状態(他社サイトの事例紹介に見られる情報密度と律動を参考にブランド再構築・2026-07-29)。

追加日:
2026-07-29
依存:
なし
tags
#block #works #case-study #portfolio #grid #ledger #hairline #filter #hover-reveal #no-image #ready-crew

プレビュー

BLK-06 — Works / 制作実績

つくった先の、変化で選ばれたい。

業種も規模もさまざまでも、共通しているのは「公開して終わりにしない」こと。 近年の制作事例から抜粋してご紹介します。

すべての事例6件表示中
"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>
  );
}

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

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