First CH Designs
BLK-72Blocks

サービス案内の帯構成(節の連なりで密度を作る)

サービスを1つの節の中で並べ替えるのではなく、節の連なりそのものを組むブロック。1枚の12列方眼を全帯に貫通させたうえで、帯ごとにその方眼の割り方と高さを変え、1列で厚い帯→12列で薄い帯→12列割りの行→2列→1列と密度の起伏でページを進める。各帯の見出しは右端に「何列・何項目か」を出すので、構成そのものが読める情報になる。帯は独立しておらず、領域の帯で語を選ぶとサービスの帯の該当行だけが残る(節をまたぐ絞り込み・hoverは試し読み、clickで固定)。カードで囲まず、セルの境界はgapで作るため背面の方眼と線が一致する。英字キッカー・末尾の矢印リンク・角丸・スクロール出現アニメは使っていない。画像素材ゼロ・依存ゼロ。

追加日:
2026-09-21
依存:
なし
tags
#block #service #page-composition #bands #density #column-grid #cross-section-filter #container-query #hairline #cardless #a11y #no-image #no-deps

プレビュー

00

サービスの考え方

1列・全幅

作って終わりにせず、公開してからの数年を一緒に見るために、範囲と手順を先に決めます

引き受ける範囲
設計・デザイン・実装・公開後の運用
進め方
2週ごとに動くものを見ながら決める
引き継ぎ
操作手順と更新できる箇所の一覧を添える
01

取り扱う領域

12列・12項目
02

提供サービス

4行・12列割り
  • 01

    会社案内サイトの制作

    載せる順番を決めるところから引き受けます。公開後にご自身で直せる範囲を先に合意してから作ります。

    規模の目安
    8〜16ページ
    期間
    8〜12週
  • 02

    申込ページの制作

    1枚で読み切れる構成にし、申込までの導線と計測をまとめて用意します。文章の草案もこちらで書きます。

    規模の目安
    1ページ
    期間
    4〜6週
  • 03

    公開後の更新・改善

    数字を見ながら毎月すこしずつ直します。対応する時間の上限を決めてから始めるので、費用が膨らみません。

    規模の目安
    月4〜8時間
    期間
    継続
  • 04

    業務用の画面開発

    手作業で回している集計や台帳を、社内だけで使う小さな画面に置き換えます。既存の道具は残したまま繋ぎます。

    規模の目安
    2〜6画面
    期間
    6〜10週
03

はじめに決めること

2列・3項目

見積りの幅は、作業量よりも決まっていない事柄の数で広がります。最初の打ち合わせでこの3つの見当がつけば、金額も日程も、幅ではなく1つの数で出せます

公開したい時期
逆算して、決めていただく期限をこちらから出します
更新する人
誰が何を直すかで、作り方も引き継ぐ資料も変わります
用意できる素材
写真と文章の当てがない箇所は、代わりの手を先に決めます
04

相談の入口

1列・終端

まだ何も決まっていない段階でかまいません。決め方のほうからお話しします

相談してみる
"use client";

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

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

/**
 * サービス案内の帯構成(1節ではなく節の連なりを組む)
 * 既存のサービス系は service-zigzag / service-photo-stack / strength-bento-grid / six-faces-cube の
 * ように「1つの節の中で N 件をどう並べるか」を解いたもので、節と節のあいだの設計を持つものは無い。
 * 本ブロックは扱う単位を節の連なりへ上げ、1枚の12列方眼を全帯に貫通させたうえで、帯ごとに
 * その方眼の割り方(1列・12列・12列割りの行・2列・1列の順)と高さを変え、密度の起伏を作る。
 * 各帯の見出しは右端に「その帯が何列・何項目か」を出し、構成そのものを読める情報にしている。
 * 帯は独立していない——領域の帯で語を選ぶと、サービスの帯の該当行だけが残る(節をまたぐ絞り込み)。
 * 飽和idiomは英字キッカーと末尾の矢印リンクを外し、角丸・スクロール出現アニメもゼロ。
 * 掲載文はすべてオリジナル。画像素材ゼロ・依存ゼロ。
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - **方眼は1枚だけ置き、帯は割り方だけを変える**: 罫線を帯ごとに引くと列がズレて「同じページ」に
 *   見えない。`linear-gradient(to right, line 0 1px, transparent 1px)` を root に absolute で
 *   1枚敷き、`background-repeat: repeat-x` + `background-size: calc(100%/12) 100%` で列幅を
 *   決める。狭幅は `[background-size:25%_100%]` で4列に落とす(列数の変更が1箇所で済む)。
 *   ここを `repeating-linear-gradient` で書くと**周期が自分の最終色停止位置=1pxになり**、
 *   background-size が効かずに面全体が薄いグレーで塗り潰される(線が1本も出ない)。
 *   タイルの幅を background-size で決めるなら、勾配は必ず非repeating側で書く。
 * - **セルの境界は gap で作る**: `grid gap-px bg-gray-200` + セルを `bg-white` にすると、
 *   セル数がいくつでも罫線が二重にならず、背面の方眼と同じ位置に線が落ちる(border では
 *   隣接セルで2px になり方眼とズレる)。
 * - **密度は高さで語る**: 低密度の帯は列を使わず縦padding を厚く、高密度の帯は列いっぱいに
 *   割って高さを薄くする。同じ幅の中で「1列で厚い / 12列で薄い」を交互に置くと、
 *   カードを使わずにページのリズムが立つ。
 * - **節をまたぐ絞り込みは preview と pinned の2状態で持つ**: hover/focus は preview、click は
 *   pinned(`aria-pressed`)。実効値は `preview ?? pinned` なので、ポインタでの試し読みと
 *   キーボードでの固定が衝突しない。減衰は opacity のみ・帯の見出しの読み値は aria-live で伝える。
 */

type Domain = { label: string; services: number[] };

/** 帯01: 取り扱い領域(12列の方眼を1列ずつ使い切る薄い帯) */
const domains: Domain[] = [
  { label: "会社案内", services: [0] },
  { label: "採用情報", services: [0] },
  { label: "商品紹介", services: [0, 1] },
  { label: "予約・申込", services: [1] },
  { label: "資料請求", services: [1] },
  { label: "多言語", services: [0] },
  { label: "記事更新", services: [2] },
  { label: "写真撮影", services: [0, 1] },
  { label: "計測設定", services: [1, 2] },
  { label: "表示速度", services: [2] },
  { label: "問い合わせ", services: [1, 2] },
  { label: "社内画面", services: [3] },
];

/** 帯02: 提供サービス(方眼を 1 / 4 / 4 / 3 に割った全幅の行) */
const services = [
  {
    no: "01",
    name: "会社案内サイトの制作",
    body: "載せる順番を決めるところから引き受けます。公開後にご自身で直せる範囲を先に合意してから作ります。",
    scale: "8〜16ページ",
    span: "8〜12週",
  },
  {
    no: "02",
    name: "申込ページの制作",
    body: "1枚で読み切れる構成にし、申込までの導線と計測をまとめて用意します。文章の草案もこちらで書きます。",
    scale: "1ページ",
    span: "4〜6週",
  },
  {
    no: "03",
    name: "公開後の更新・改善",
    body: "数字を見ながら毎月すこしずつ直します。対応する時間の上限を決めてから始めるので、費用が膨らみません。",
    scale: "月4〜8時間",
    span: "継続",
  },
  {
    no: "04",
    name: "業務用の画面開発",
    body: "手作業で回している集計や台帳を、社内だけで使う小さな画面に置き換えます。既存の道具は残したまま繋ぎます。",
    scale: "2〜6画面",
    span: "6〜10週",
  },
];

/** 帯03: はじめに決めること(2列の帯) */
const decisions = [
  { term: "公開したい時期", note: "逆算して、決めていただく期限をこちらから出します" },
  { term: "更新する人", note: "誰が何を直すかで、作り方も引き継ぐ資料も変わります" },
  { term: "用意できる素材", note: "写真と文章の当てがない箇所は、代わりの手を先に決めます" },
];

/** 帯の見出し(左=通し番号 / 中=和文見出し / 右=その帯の割り方) */
function BandHead({
  n,
  title,
  readout,
  live,
  headingTag: Tag = "h3",
  id,
}: {
  n: string;
  title: string;
  readout: string;
  live?: boolean;
  headingTag?: "h2" | "h3";
  id?: string;
}) {
  return (
    <div className="flex items-end justify-between gap-4 border-b border-gray-200 px-5 pb-3 @2xl:px-8">
      <div className="flex items-end gap-3 @2xl:gap-5">
        <span
          className={`${montserrat.className} shrink-0 pb-[3px] text-[11px] leading-none font-semibold tracking-widest text-amber-600 tabular-nums @2xl:text-[13px]`}
        >
          {n}
        </span>
        <Tag
          id={id}
          className="text-[1.0625rem] leading-[1.35] font-bold tracking-tight text-gray-900 @2xl:text-[1.375rem]"
        >
          {title}
        </Tag>
      </div>
      <span
        aria-live={live ? "polite" : undefined}
        className="shrink-0 pb-[3px] text-right text-[10px] leading-[1.5] text-gray-500 tabular-nums @2xl:text-[11px]"
      >
        {readout}
      </span>
    </div>
  );
}

export default function ServiceBandComposition() {
  // hover/focus は preview、click は pinned。実効値は preview ?? pinned。
  const [pinned, setPinned] = useState<number | null>(null);
  const [preview, setPreview] = useState<number | null>(null);
  const active = preview ?? pinned;
  const activeDomain = active === null ? null : domains[active];

  return (
    <section
      aria-labelledby="svc-band-heading"
      className={`@container relative w-full max-w-5xl border-y border-gray-200 bg-white`}
    >
      {/* 全帯を貫く12列の方眼(狭幅は4列)。帯はこの割り方を変えるだけで、線そのものは動かさない */}
      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-0 [background-image:linear-gradient(to_right,rgba(34,30,25,0.1)_0_1px,transparent_1px)] [background-repeat:repeat-x] [background-size:25%_100%] @2xl:[background-size:calc(100%/12)_100%]"
      />

      {/* 帯00: 1列・余白を最大に取る導入 */}
      <div className="relative pt-8 @2xl:pt-12">
        <BandHead
          n="00"
          title="サービスの考え方"
          readout="1列・全幅"
          headingTag="h2"
          id="svc-band-heading"
        />
        <div className="px-5 py-10 @2xl:px-8 @2xl:py-16">
          <p className="max-w-[24ch] text-[1.25rem] leading-[1.75] font-bold tracking-tight text-gray-900 text-balance @2xl:text-[1.75rem] @2xl:leading-[1.7]">
            作って終わりにせず、公開してからの数年を一緒に見るために、範囲と手順を先に決めます
          </p>
          <dl className="mt-8 grid max-w-2xl grid-cols-1 gap-px bg-gray-200 @md:grid-cols-3 @2xl:mt-12">
            {[
              { t: "引き受ける範囲", d: "設計・デザイン・実装・公開後の運用" },
              { t: "進め方", d: "2週ごとに動くものを見ながら決める" },
              { t: "引き継ぎ", d: "操作手順と更新できる箇所の一覧を添える" },
            ].map((s) => (
              <div key={s.t} className="bg-white py-4 pr-4 @md:px-4 @md:first:pl-0">
                <dt className="text-[11px] font-bold tracking-wide text-amber-700">{s.t}</dt>
                <dd className="mt-1.5 text-[13px] leading-[1.7] text-gray-600">{s.d}</dd>
              </div>
            ))}
          </dl>
        </div>
      </div>

      {/* 帯01: 12列を1列ずつ使い切る薄い帯。押すとサービスの帯が絞り込まれる */}
      <div className="relative border-t border-gray-200 pt-6 @2xl:pt-8">
        <BandHead n="01" title="取り扱う領域" readout="12列・12項目" />
        <ul className="grid grid-cols-4 gap-px bg-gray-200 @2xl:grid-cols-12">
          {domains.map((d, i) => {
            const on = active === i;
            return (
              <li key={d.label} className="bg-white">
                <button
                  type="button"
                  aria-pressed={pinned === i}
                  onMouseEnter={() => setPreview(i)}
                  onMouseLeave={() => setPreview(null)}
                  onFocus={() => setPreview(i)}
                  onBlur={() => setPreview(null)}
                  onClick={() => setPinned((p) => (p === i ? null : i))}
                  className={`relative flex h-full w-full items-start px-2.5 pt-4 pb-5 text-left text-[11px] leading-[1.5] font-medium transition-colors duration-200 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none ${
                    on ? "bg-amber-50 text-amber-800" : "text-gray-700 hover:bg-gray-50"
                  }`}
                >
                  <span
                    aria-hidden="true"
                    className={`absolute inset-x-0 top-0 h-[2px] origin-left bg-amber-600 transition-transform duration-200 ease-out motion-reduce:transition-none ${
                      on ? "scale-x-100" : "scale-x-0"
                    }`}
                  />
                  {d.label}
                </button>
              </li>
            );
          })}
        </ul>
      </div>

      {/* 帯02: 方眼を 1/4/5/2 に割った全幅の行。帯01の選択でここだけが絞り込まれる */}
      <div className="relative border-t border-gray-200 pt-6 @2xl:pt-8">
        <BandHead
          n="02"
          title="提供サービス"
          live
          readout={
            activeDomain
              ? `${activeDomain.label}に関わる ${activeDomain.services.length}行`
              : "4行・12列割り"
          }
        />
        <ul>
          {services.map((s, i) => {
            const dimmed = activeDomain !== null && !activeDomain.services.includes(i);
            const marked = activeDomain !== null && activeDomain.services.includes(i);
            return (
              <li
                key={s.no}
                className={`relative border-b border-gray-200 transition-opacity duration-300 last:border-b-0 motion-reduce:transition-none ${
                  dimmed ? "opacity-40" : "opacity-100"
                }`}
              >
                <span
                  aria-hidden="true"
                  className={`absolute top-0 bottom-0 left-0 w-[3px] origin-top bg-amber-600 transition-transform duration-300 ease-out motion-reduce:transition-none ${
                    marked ? "scale-y-100" : "scale-y-0"
                  }`}
                />
                <div className="grid grid-cols-1 gap-x-5 gap-y-2 px-5 py-6 @2xl:grid-cols-12 @2xl:px-8 @2xl:py-7">
                  <span
                    className={`${montserrat.className} text-[11px] leading-none font-semibold tracking-widest text-gray-400 tabular-nums @2xl:col-span-1 @2xl:pt-[5px]`}
                  >
                    {s.no}
                  </span>
                  <h4 className="text-[1rem] leading-[1.45] font-bold tracking-tight text-gray-900 @2xl:col-span-4 @2xl:text-[1.0625rem]">
                    {s.name}
                  </h4>
                  <p className="text-[13px] leading-[1.8] text-gray-600 @2xl:col-span-4 @2xl:text-[13.5px]">
                    {s.body}
                  </p>
                  <dl className="flex gap-5 text-[11px] leading-[1.6] @2xl:col-span-3 @2xl:flex-col @2xl:gap-2 @2xl:text-right">
                    <div>
                      <dt className="text-gray-400">規模の目安</dt>
                      <dd className="font-medium text-gray-700 tabular-nums">{s.scale}</dd>
                    </div>
                    <div>
                      <dt className="text-gray-400">期間</dt>
                      <dd className="font-medium text-gray-700 tabular-nums">{s.span}</dd>
                    </div>
                  </dl>
                </div>
              </li>
            );
          })}
        </ul>
      </div>

      {/* 帯03: 2列。本文と項目を左右に分けて密度を落とす */}
      <div className="relative border-t border-gray-200 pt-6 @2xl:pt-8">
        <BandHead n="03" title="はじめに決めること" readout="2列・3項目" />
        <div className="grid grid-cols-1 gap-8 px-5 py-8 @2xl:grid-cols-12 @2xl:gap-10 @2xl:px-8 @2xl:py-10">
          <p className="max-w-[34ch] text-[13.5px] leading-[1.95] text-gray-700 @2xl:col-span-5">
            見積りの幅は、作業量よりも決まっていない事柄の数で広がります。最初の打ち合わせでこの3つの見当がつけば、金額も日程も、幅ではなく1つの数で出せます
          </p>
          <dl className="@2xl:col-span-7">
            {decisions.map((d) => (
              <div
                key={d.term}
                className="grid grid-cols-1 gap-1 border-t border-gray-200 py-3.5 first:border-t-0 first:pt-0 @md:grid-cols-[10rem_1fr] @md:gap-5"
              >
                <dt className="text-[13px] leading-[1.6] font-bold text-gray-900">{d.term}</dt>
                <dd className="text-[12.5px] leading-[1.75] text-gray-600">{d.note}</dd>
              </div>
            ))}
          </dl>
        </div>
      </div>

      {/* 帯04: 1列に戻して終端。角丸ゼロ・矢印なしのテキストボタン */}
      <div className="relative border-t border-gray-200 pt-6 @2xl:pt-8">
        <BandHead n="04" title="相談の入口" readout="1列・終端" />
        <div className="flex flex-col gap-6 px-5 py-10 @2xl:flex-row @2xl:items-end @2xl:justify-between @2xl:px-8 @2xl:py-14">
          <p className="max-w-[28ch] text-[1rem] leading-[1.8] font-bold tracking-tight text-gray-900 text-balance @2xl:text-[1.1875rem]">
            まだ何も決まっていない段階でかまいません。決め方のほうからお話しします
          </p>
          <a
            href="#contact"
            className="inline-flex shrink-0 items-center justify-center border-2 border-gray-900 px-7 py-3.5 text-[13px] font-bold tracking-wide text-gray-900 transition-colors duration-200 hover:bg-gray-900 hover:text-white focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
          >
            相談してみる
          </a>
        </div>
      </div>
    </section>
  );
}

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

npx shadcn@latest add https://designs.first-ch.com/r/service-band-composition.json