First CH Designs
BLK-58Blocks

Mission Story with a Pinned Backdrop

A three-chapter story section for a company's principles. A pinned 100svh statement stays completely still while an opaque white sheet slides up and covers it, advancing the narrative. Depth comes from occlusion and a receding amber accent instead of shadows: the pinning is pure CSS sticky plus paint order, and JS only writes the occlusion ratio into one custom property. Inside short containers and under reduced motion it degrades to a plain stacked reading piece. No images, no dependencies.

Added:
2026-09-12
Dependencies:
None
tags
#block #mission #story #about #philosophy #sticky #layered #occlusion #scroll-linked #hairline #no-image #no-dependency

Preview

会社の考え方

実績の数より、
決める順番の話をします

制作会社を選ぶときに本当に知りたいのは、作った数ではなく進め方だと思っています。私たちが守っている順番を、三つに分けて置きました。

01はじめの一時間

事業の言葉を借りる

最初の打ち合わせで、サイトの話はほとんどしません。

01はじめの一時間

伺うのはデザインの好みではなく事業の話です。何を売っていて、誰に届けたいのか。そこで出てきた言い回しを要約せずに書き留め、そのまま見出しと導線の骨に使います。

借りた言葉は最後まで捨てません。罫線を1本引く理由も、色を1つ減らす理由も、事業の言葉で説明できる状態を保ちます。説明がつかない装飾は、この段階で落ちます。

聞いた言葉のまま書かれた見出しは、社内で誰も直しません

決めていること
聞いた言い回しを、そのままの粒度で残す
やらないこと
先に見た目の方向性を決めてしまう
02つくる順番

速さより、順番

早く公開することと、早く成果が出ることは別の話です。

02つくる順番

順番を決めないまま手を動かすと、あとから前提が変わって作り直しになります。文字と並び順を先に固め、合意してから絵にするほうが、結局はいちばん速く着きます。

そのぶん、序盤は地味です。画面が出てくるまでに時間がかかるように見えますが、戻りが起きない進め方を選んでいるだけで、工程を飛ばしているわけではありません。

戻りが起きない進み方が、結果としていちばん短い道になります

決めていること
文字と並び順の合意を、意匠より前に取る
やらないこと
決まる前に画面を作り始める
03公開したあと

公開してからが本編

公開日は締め切りではなく、測りはじめる日です。

03公開したあと

数字を見て文言を直し、季節で内容を入れ替える。そこまで続けてはじめて、サイトは費用ではなく設備になります。公開の瞬間がいちばん価値の低い状態です。

そのために、引き渡す時点で自分たちで直せる範囲を必ず残します。どこを触ってよくて、どこに触ると崩れるのか。その線引きを書面にして渡すところまでが制作です。

自分たちで直せないサイトは、公開した日がいちばん新しい

決めていること
更新できる範囲を、書面で線引きして渡す
やらないこと
直せる人がこちらにしかいない形で納める

三つに共通しているのは、決める順番を守ることだけです。特別な手法は使っていません。

会社概要を見る
"use client";

import { useEffect, useRef } from "react";
import { Montserrat } from "next/font/google";

// ラテン系フォントは unicode-range の分割が少ないので preload 既定のままでよい。
const montserrat = Montserrat({
  weight: ["600"],
  subsets: ["latin"],
  display: "swap",
});

/**
 * ミッション・ストーリー(背面レイヤー×前景シートの層構成)
 *
 * 差別化: 既存のスクロール連動ブロックはいずれも「1枚の面の中で要素が動く」型(ピン留めした舞台を
 * 横へ送る/見出しを脇に残して工程を進める/読み進み量で文字を出現させる)だが、本ブロックは面を
 * 2枚に分け、奥の一文は一切動かさない。手前の白いシートがその上を滑って覆っていくことだけで
 * 章が進む。奥行きは影を使わず、遮蔽と彩度の後退だけで作る。
 * 飽和していた英字キッカー・末尾の矢印リンク・角丸・要素ごとの出現アニメは意図的に外した。
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - **「覆う」は積み順だけで作れる**: 章の <div> に `sticky top-0 h-[100svh]` の奥レイヤーと、
 *   その後ろへ続く不透明な白いシートを置く。親(章)はシートのぶんだけ背が高いので、奥レイヤーは
 *   貼り付いたまま、あとから流れてくるシートに上から塗り潰されていく。JS は固定にも遮蔽にも
 *   関与しない(スクロール量で position を切り替えると必ずガタつく)。
 * - **奥行きの手掛かりは影ではなく2つ**: ①遮蔽(手前の不透明な面が奥の面を欠けさせる)
 *   ②彩度の後退。アンバーの罫だけを速い係数 `1 - 1.8p` で抜き、墨の文字は緩い係数 `1 - 0.55p` で
 *   残すと「色が先に遠ざかり、輪郭が後から沈む」という空気遠近と同じ順序になる。ぼかしは使わない
 *   (合成が重いうえ、hairline の1pxが最初に溶けて構造そのものが失われる)。
 * - **進捗は奥ではなく「覆う側の上端」で測る**: `p = (vh - sheet.top) / vh`。遮蔽の量そのものを
 *   測っているので、章の高さやシートの長さを変えても係数を調整し直さなくてよい。
 * - **貼り付いた天井の扱いは配置で解く**: 奥の一文を上端揃えではなく上下中央に置くと、固定ヘッダーの
 *   あるサイトへ落としても主役の一文が潜らない。ヘッダー高を実測して top を補正する必要が無くなる。
 * - **短い箱に埋め込まれたら固定をやめる**: 直近の overflow を持つ祖先が1画面ぶんの高さを持たない
 *   ときは position:static へ降格し、素の縦積みにする(`transform: scale()` した中で描かれると、
 *   sticky は縮小後の座標を基準に貼り付いてレイアウトが崩れるため)。動きを控える設定でも同じ形へ
 *   倒す。覆う演出が一度も走らなくても、章と本文の読み物として完結する。
 */

type Chapter = {
  no: string;
  label: string;
  /** 奥に貼り付く一文。句点で止めない */
  message: string;
  /** 一文に添える短い補足 */
  aside: string;
  lead: string;
  body: string;
  /** 面の中ほどに置く一文。本文の要約ではなく、言い切りを1つ */
  pull: string;
  keeps: string;
  avoids: string;
};

const chapters: Chapter[] = [
  {
    no: "01",
    label: "はじめの一時間",
    message: "事業の言葉を借りる",
    aside: "最初の打ち合わせで、サイトの話はほとんどしません。",
    lead: "伺うのはデザインの好みではなく事業の話です。何を売っていて、誰に届けたいのか。そこで出てきた言い回しを要約せずに書き留め、そのまま見出しと導線の骨に使います。",
    body: "借りた言葉は最後まで捨てません。罫線を1本引く理由も、色を1つ減らす理由も、事業の言葉で説明できる状態を保ちます。説明がつかない装飾は、この段階で落ちます。",
    pull: "聞いた言葉のまま書かれた見出しは、社内で誰も直しません",
    keeps: "聞いた言い回しを、そのままの粒度で残す",
    avoids: "先に見た目の方向性を決めてしまう",
  },
  {
    no: "02",
    label: "つくる順番",
    message: "速さより、順番",
    aside: "早く公開することと、早く成果が出ることは別の話です。",
    lead: "順番を決めないまま手を動かすと、あとから前提が変わって作り直しになります。文字と並び順を先に固め、合意してから絵にするほうが、結局はいちばん速く着きます。",
    body: "そのぶん、序盤は地味です。画面が出てくるまでに時間がかかるように見えますが、戻りが起きない進め方を選んでいるだけで、工程を飛ばしているわけではありません。",
    pull: "戻りが起きない進み方が、結果としていちばん短い道になります",
    keeps: "文字と並び順の合意を、意匠より前に取る",
    avoids: "決まる前に画面を作り始める",
  },
  {
    no: "03",
    label: "公開したあと",
    message: "公開してからが本編",
    aside: "公開日は締め切りではなく、測りはじめる日です。",
    lead: "数字を見て文言を直し、季節で内容を入れ替える。そこまで続けてはじめて、サイトは費用ではなく設備になります。公開の瞬間がいちばん価値の低い状態です。",
    body: "そのために、引き渡す時点で自分たちで直せる範囲を必ず残します。どこを触ってよくて、どこに触ると崩れるのか。その線引きを書面にして渡すところまでが制作です。",
    pull: "自分たちで直せないサイトは、公開した日がいちばん新しい",
    keeps: "更新できる範囲を、書面で線引きして渡す",
    avoids: "直せる人がこちらにしかいない形で納める",
  },
];

const clamp01 = (v: number) => (v < 0 ? 0 : v > 1 ? 1 : v);

/** 直近の「切り取られる箱」を探す(ページそのものは対象外) */
function findClipper(el: HTMLElement): HTMLElement | null {
  let node = el.parentElement;
  while (node && node !== document.body && node !== document.documentElement) {
    const oy = getComputedStyle(node).overflowY;
    if (oy === "auto" || oy === "scroll" || oy === "hidden") return node;
    node = node.parentElement;
  }
  return null;
}

export default function StickyBackdropStory() {
  const sectionRef = useRef<HTMLElement | null>(null);
  const rafRef = useRef<number | null>(null);

  useEffect(() => {
    const section = sectionRef.current;
    if (!section) return;
    // 動きを控える設定では CSS 側が平置きへ倒すので、JS は何もしない
    if (
      typeof window.matchMedia === "function" &&
      window.matchMedia("(prefers-reduced-motion: reduce)").matches
    ) {
      return;
    }

    const chapterEls = Array.from(section.querySelectorAll<HTMLElement>(".sb-chapter"));
    const first = chapterEls[0];
    if (!first) return;

    // 固定する余地の無い箱(一覧カード等)へ埋め込まれているときは平置きへ降格する
    const back = first.querySelector<HTMLElement>(".sb-back");
    const clipper = findClipper(section);
    if (
      back &&
      clipper &&
      clipper.clientHeight > 0 &&
      clipper.clientHeight < back.offsetHeight * 0.8
    ) {
      section.classList.add("sb-flat");
      return;
    }

    const sheets = chapterEls.map((c) => c.querySelector<HTMLElement>(".sb-sheet"));

    const measure = () => {
      rafRef.current = null;
      const vh = window.innerHeight || 1;
      const rect = section.getBoundingClientRect();
      // 画面外では書かない(端に到達した値がそのまま残るので、戻ってきても破綻しない)
      if (rect.bottom < 0 || rect.top > vh) return;
      chapterEls.forEach((chapter, i) => {
        const sheet = sheets[i];
        if (!sheet) return;
        // 覆う側の上端が画面下端にあるとき 0、画面上端まで来たとき 1
        const p = clamp01((vh - sheet.getBoundingClientRect().top) / vh);
        chapter.style.setProperty("--sb-p", p.toFixed(3));
      });
    };

    const request = () => {
      if (rafRef.current === null) rafRef.current = requestAnimationFrame(measure);
    };

    measure();
    window.addEventListener("scroll", request, { passive: true });
    window.addEventListener("resize", request);
    return () => {
      window.removeEventListener("scroll", request);
      window.removeEventListener("resize", request);
      if (rafRef.current !== null) cancelAnimationFrame(rafRef.current);
      rafRef.current = null;
    };
  }, []);

  return (
    <section
      ref={sectionRef}
      aria-labelledby="sb-title"
      className="sb w-full bg-white text-gray-900"
    >
      <style>{`
        .sb-chapter { --sb-p: 0; }
        /* 出力は transform と opacity だけ。奥の面は動かさず、沈むだけにする */
        .sb-recede {
          transform: translateY(calc(var(--sb-p) * 18px)) scale(calc(1 - 0.055 * var(--sb-p)));
          transform-origin: 50% 40%;
          will-change: transform;
        }
        .sb-ink { opacity: calc(1 - 0.55 * var(--sb-p)); }
        /* 色は輪郭より先に遠ざかる(彩度の後退) */
        .sb-accent { opacity: calc(1 - 1.8 * var(--sb-p)); }

        /* 平置きへの降格。マークアップは同じまま、固定と遮蔽だけを外す */
        .sb-flat .sb-back {
          position: static;
          height: auto;
          padding-top: 4.5rem;
          padding-bottom: 3rem;
        }
        .sb-flat .sb-sheet { min-height: 0; }
        .sb-flat .sb-recede { transform: none; }
        .sb-flat .sb-ink, .sb-flat .sb-accent { opacity: 1; }

        @media (prefers-reduced-motion: reduce) {
          .sb .sb-back {
            position: static;
            height: auto;
            padding-top: 4.5rem;
            padding-bottom: 3rem;
          }
          .sb .sb-sheet { min-height: 0; }
          .sb .sb-recede { transform: none; }
          .sb .sb-ink, .sb .sb-accent { opacity: 1; }
        }
      `}</style>

      {/* 導入(層の外側。読み終えたら普通に流れて消える) */}
      <div className="mx-auto max-w-3xl px-5 pt-2 pb-12 sm:px-8">
        <p className="flex items-center gap-3 text-[12px] font-bold tracking-[0.1em] text-gray-500">
          <span aria-hidden="true" className="inline-block size-1 shrink-0 bg-amber-600" />
          会社の考え方
        </p>
        <h2
          id="sb-title"
          className="mt-5 text-[clamp(1.55rem,3.4vw,2.2rem)] leading-[1.5] font-bold tracking-tight"
        >
          実績の数より、
          <br />
          決める順番の話をします
        </h2>
        <p className="mt-5 max-w-xl text-[14px] leading-[2] text-gray-600">
          制作会社を選ぶときに本当に知りたいのは、作った数ではなく進め方だと思っています。私たちが守っている順番を、三つに分けて置きました。
        </p>
      </div>

      {chapters.map((chapter) => (
        <div key={chapter.no} className="sb-chapter relative isolate">
          {/* 奥の面: 貼り付いたまま動かない。中央寄せなので固定ヘッダーに潜らない */}
          <div className="sb-back sticky top-0 z-0 flex h-[100svh] items-center">
            <div className="sb-recede mx-auto w-full max-w-3xl px-5 sm:px-8">
              <div className="flex items-center gap-4">
                <span aria-hidden="true" className="sb-accent h-px w-12 shrink-0 bg-amber-600" />
                <span
                  className={`${montserrat.className} sb-accent text-[11px] font-semibold tracking-[0.18em] text-amber-700 tabular-nums`}
                >
                  {chapter.no}
                </span>
                <span className="sb-ink text-[11px] font-bold tracking-[0.12em] text-gray-500">
                  {chapter.label}
                </span>
              </div>
              {/* 折り返しの質を2つで担保する: text-balance で行長を揃え、auto-phrase で
                  文節の途中(「言|葉」等)に改行が落ちるのを防ぐ(非対応ブラウザは素の折り返し) */}
              <h3 className="sb-ink mt-7 text-[clamp(1.75rem,5.2vw,3rem)] leading-[1.4] font-bold tracking-tight text-balance [word-break:auto-phrase]">
                {chapter.message}
              </h3>
              <p className="sb-ink mt-7 max-w-md text-[13px] leading-[2] text-gray-500">
                {chapter.aside}
              </p>
            </div>
          </div>

          {/* 手前の面: 不透明な白。これが奥の面を上から欠けさせる(影は置かない) */}
          <div className="sb-sheet relative z-10 flex min-h-[88svh] flex-col justify-between gap-14 border-t border-gray-200 bg-white">
            <span
              aria-hidden="true"
              className="absolute top-0 left-0 h-px w-16 bg-amber-600 sm:w-24"
            />
            {/* 上: 覆う境界のすぐ下から本文が始まる */}
            <div className="@container mx-auto w-full max-w-3xl px-5 pt-12 sm:px-8">
              <div className="grid gap-6 @2xl:grid-cols-[8rem_minmax(0,1fr)] @2xl:gap-12">
                <div className="flex items-baseline gap-3 @2xl:block">
                  <span
                    className={`${montserrat.className} text-[11px] font-semibold tracking-[0.18em] text-gray-400 tabular-nums`}
                  >
                    {chapter.no}
                  </span>
                  <span className="text-[12px] font-bold tracking-[0.08em] whitespace-nowrap text-gray-500 @2xl:mt-2.5 @2xl:block">
                    {chapter.label}
                  </span>
                </div>

                <div className="min-w-0">
                  <p className="text-[14px] leading-[2] font-medium text-gray-800">{chapter.lead}</p>
                  <p className="mt-5 text-[13.5px] leading-[2] text-gray-600">{chapter.body}</p>
                </div>
              </div>
            </div>

            {/* 中: 言い切りを1つ。面の真ん中に重さを置き、余白を「間」として成立させる */}
            <div className="mx-auto w-full max-w-3xl px-5 sm:px-8">
              <p className="max-w-2xl border-l-2 border-amber-600 pl-6 text-[clamp(1.05rem,2.1vw,1.35rem)] leading-[1.9] font-bold tracking-tight text-gray-900">
                {chapter.pull}
              </p>
            </div>

            {/* 下: 章の結論を面の下端に置き、余白を上下の内容で挟む */}
            <div className="@container mx-auto w-full max-w-3xl px-5 pb-14 sm:px-8">
              <dl className="grid gap-x-12 gap-y-5 border-t border-gray-200 pt-5 @2xl:grid-cols-2">
                <div className="@2xl:border-r @2xl:border-gray-200 @2xl:pr-12">
                  <dt className="text-[11px] font-bold tracking-[0.08em] text-amber-700">
                    決めていること
                  </dt>
                  <dd className="mt-2 text-[13px] leading-[1.9] text-gray-700">{chapter.keeps}</dd>
                </div>
                <div>
                  <dt className="text-[11px] font-bold tracking-[0.08em] text-gray-400">
                    やらないこと
                  </dt>
                  <dd className="mt-2 text-[13px] leading-[1.9] text-gray-700">{chapter.avoids}</dd>
                </div>
              </dl>
            </div>
          </div>
        </div>
      ))}

      {/* 結び */}
      <div className="mx-auto max-w-3xl px-5 pt-12 pb-2 sm:px-8">
        <p className="max-w-xl text-[14px] leading-[2] text-gray-600">
          三つに共通しているのは、決める順番を守ることだけです。特別な手法は使っていません。
        </p>
        <a
          href="#"
          className="mt-6 inline-block text-[13px] font-semibold text-gray-900 underline decoration-gray-300 decoration-1 underline-offset-[7px] transition-colors duration-200 hover:decoration-amber-600 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 motion-reduce:transition-none"
        >
          会社概要を見る
        </a>
      </div>
    </section>
  );
}

Add to your project via shadcn CLI

npx shadcn@latest add https://designs.first-ch.com/r/sticky-backdrop-story.json