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