First CH Designs
PGE-07Pages

分割ヒーロー(左テキスト×右ビジュアルの帯構成)

ヒーローを「テキスト面 / 細い分割レール / ビジュアル面」の縦3カラムに割った分割構成。右のビジュアル面は6本の縦帯に分解され、帯ごとに位置と長さの違う色片が下からせり上がる。その入りを90msずつずらすことで、視線が右のビジュアル→左の見出しへ流れる時間差を作る。画像素材ゼロ・JSゼロで、animation-fill-mode: backwards によりSSRの初期HTML・JS無効・reduced-motion では組み上がった完成状態で表示される。概要はカードにせず hairline罫線+余白でグルーピング(2026-08-21)。

追加日:
2026-08-21
依存:
なし
tags
#hero #page #split #editorial #stagger #css-only-motion #no-image #no-js

プレビュー

Web Production

伝わる面を、
まっすぐ組む。

情報を整理し、余白で語る。中小企業のWebを、成果につながる標準へ。

納期
最短3週間
体制
設計〜運用
支援
公開後も伴走
import { Montserrat } from "next/font/google";

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

/**
 * 分割ヒーロー(左テキスト × 右ビジュアルの帯構成)
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - 縦帯スプリット: ヒーローを「テキスト面 / 細いレール / ビジュアル面」の3カラムに割り、
 *   ビジュアル面はさらに6本の縦帯へ分解し、帯ごとに位置と長さの違う色片を1つ置く。
 *   写真がなくても、面の分割と色片のリズムだけで主役側の重心が作れる。
 * - 入りの時間差(stagger): 各帯を overflow-hidden にし、中のカーテン(absolute inset-0)を
 *   translateY(100%)→0 でせり上げる。カーテンは帯と同寸なので 100% ずらせば完全に隠れる
 *   (色片そのものを動かすと帯からはみ出し切らず、隠しきれない)。animationDelay を帯ごとに +90ms。
 *   文字側は帯が揃い始めた頃(560ms〜)に translateY で立ち上げると、視線が右→左へ流れる。
 * - JSゼロで組む: animation-fill-mode: backwards(both)を使うと要素の素の状態=完成形になるため、
 *   SSRの初期HTML・JS無効・reduced-motion のいずれでも「組み上がった状態」で表示される。
 *   animation は必ずクラス側に持たせ、inline style には animationDelay だけを渡す
 *   (inline style に animation を書くと prefers-reduced-motion のガードが上書きできない)。
 * - グルーピングはカードではなく hairline 罫線(border-t / sm:border-l)+余白で行う。
 */

// 右ビジュアルの縦帯(帯ごとに位置と長さの違う色片を持たせ、階段状のリズムを作る。装飾なので aria-hidden)
const bands = [
  { tone: "bg-[#faf8f4]", top: 12, h: 30, seg: "bg-amber-100" },
  { tone: "bg-[#f4f0e9]", top: 30, h: 46, seg: "bg-amber-500" },
  { tone: "bg-[#faf8f4]", top: 8, h: 62, seg: "bg-[#1f1b16]" },
  { tone: "bg-[#f4f0e9]", top: 44, h: 40, seg: "bg-amber-600" },
  { tone: "bg-[#faf8f4]", top: 22, h: 34, seg: "bg-[#e3ddd2]" },
  { tone: "bg-[#f4f0e9]", top: 56, h: 30, seg: "bg-amber-200" },
];

const facts = [
  { label: "納期", value: "最短3週間" },
  { label: "体制", value: "設計〜運用" },
  { label: "支援", value: "公開後も伴走" },
];

export default function SplitBandHero({
  headingTag: Heading = "h1",
}: {
  /**
   * 見出しのタグ。実案件ではそのまま(h1)。ギャラリーのプレビューは1ページに複数の標本が
   * 並ぶため "p" を渡してh1の重複を避ける(見た目はclassNameで決まるので変わらない)。
   */
  headingTag?: "h1" | "p";
}) {
  return (
    <div className="relative w-full max-w-5xl overflow-hidden rounded-2xl border border-gray-200 bg-white">
      <div className="grid grid-cols-1 md:grid-cols-[1fr_auto_0.62fr]">
        {/* 左: テキスト面 */}
        <div className="flex flex-col justify-center px-7 py-12 sm:px-10 sm:py-16 md:py-20">
          <p
            className={`${montserrat.className} esh-rise flex items-center gap-3 text-xs font-bold tracking-[0.25em] text-amber-600 uppercase`}
            style={{ animationDelay: "560ms" }}
          >
            <span aria-hidden="true" className="h-px w-8 bg-amber-600" />
            Web Production
          </p>

          <Heading
            className="esh-rise mt-5 max-w-md text-[clamp(1.6rem,3.2vw,2.4rem)] leading-[1.4] font-bold tracking-tight text-gray-900"
            style={{ animationDelay: "680ms" }}
          >
            伝わる面を、
            <br />
            まっすぐ組む。
          </Heading>

          <p
            className="esh-rise mt-6 max-w-md text-[15px] leading-[2] text-gray-600"
            style={{ animationDelay: "820ms" }}
          >
            情報を整理し、余白で語る。中小企業のWebを、成果につながる標準へ。
          </p>

          <div
            className="esh-rise mt-9 flex flex-wrap items-center gap-x-7 gap-y-4"
            style={{ animationDelay: "940ms" }}
          >
            <a
              href="#"
              className="rounded-lg bg-amber-600 px-6 py-3 text-sm font-semibold text-white shadow-sm transition-colors duration-200 hover:bg-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
            >
              制作のご相談
            </a>
            <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
                  aria-hidden="true"
                  className="absolute -bottom-1 left-0 h-px w-full origin-left scale-x-100 bg-gray-900/40 transition-transform duration-300 ease-out group-hover:scale-x-0 motion-reduce:transition-none"
                />
                <span
                  aria-hidden="true"
                  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>

          {/* 概要(カードにせず hairline 罫線と余白でグルーピング) */}
          <dl
            className="esh-rise mt-10 flex flex-col border-t border-gray-900/10 pt-6 sm:flex-row"
            style={{ animationDelay: "1060ms" }}
          >
            {facts.map((f, i) => (
              <div
                key={f.label}
                className={
                  i === 0
                    ? "py-2 sm:pr-5"
                    : "border-t border-gray-900/10 py-2 sm:border-t-0 sm:border-l sm:border-gray-900/10 sm:px-5"
                }
              >
                <dt className="text-[11px] font-bold tracking-[0.14em] whitespace-nowrap text-gray-400">
                  {f.label}
                </dt>
                <dd className="mt-1 text-sm font-semibold whitespace-nowrap text-gray-900">
                  {f.value}
                </dd>
              </div>
            ))}
          </dl>
        </div>

        {/* 中央: 分割レール(縦書きラベル + アンバーの伸長線) */}
        <div
          aria-hidden="true"
          className="hidden w-12 flex-col items-center justify-center border-x border-gray-200 md:flex"
        >
          <span className="esh-rail block h-16 w-px origin-top bg-gray-200" />
          <span
            className={`${montserrat.className} my-4 text-[10px] font-semibold tracking-[0.3em] text-gray-400 uppercase [writing-mode:vertical-rl]`}
          >
            Split Composition
          </span>
          <span
            className="esh-rail block h-16 w-px origin-top bg-amber-500"
            style={{ animationDelay: "300ms" }}
          />
        </div>

        {/* 右: ビジュアル面(縦帯のスカイライン) */}
        <div
          role="img"
          aria-label="位置と長さの異なる色片が並ぶ、白とアンバーの縦帯の抽象ビジュアル"
          className="relative order-first h-44 overflow-hidden bg-[#faf8f4] sm:h-56 md:order-none md:h-auto md:min-h-[480px]"
        >
          <div aria-hidden="true" className="absolute inset-0 flex gap-px">
            {bands.map((b, i) => (
              <div
                key={i}
                className={`relative h-full flex-1 overflow-hidden ${b.tone}`}
              >
                {/* 帯ごとのカーテン(帯の高さぶん translateY すれば overflow-hidden で完全に隠れる) */}
                <div
                  className="esh-band absolute inset-0"
                  style={{ animationDelay: `${i * 90}ms` }}
                >
                  <span
                    className={`absolute inset-x-0 block ${b.seg}`}
                    style={{ top: `${b.top}%`, height: `${b.h}%` }}
                  />
                </div>
              </div>
            ))}
          </div>

          {/* 帯の上に重ねる標本的な作図(輪郭の円と水平罫線) */}
          <span
            aria-hidden="true"
            className="esh-fade absolute top-1/2 left-1/2 block aspect-square w-[46%] max-w-[220px] -translate-x-1/2 -translate-y-1/2 rounded-full border border-white/70"
            style={{ animationDelay: "880ms" }}
          />
          <span
            aria-hidden="true"
            className="esh-fade absolute inset-x-0 top-[38%] block h-px bg-white/50"
            style={{ animationDelay: "980ms" }}
          />
          <span
            aria-hidden="true"
            className="esh-fade absolute inset-x-0 bottom-[22%] block h-px bg-white/40"
            style={{ animationDelay: "1040ms" }}
          />
        </div>
      </div>

      {/* モーション(transform / opacity のみ・このブロック内で完結させる) */}
      <style>{`
        .esh-band {
          animation: esh-band-in 900ms cubic-bezier(0.16, 1, 0.3, 1) both;
        }
        .esh-rise {
          animation: esh-rise-in 800ms cubic-bezier(0.16, 1, 0.3, 1) both;
        }
        .esh-rail {
          animation: esh-rail-in 700ms cubic-bezier(0.16, 1, 0.3, 1) 120ms both;
        }
        .esh-fade {
          animation: esh-fade-in 700ms ease-out both;
        }
        @keyframes esh-band-in {
          from { transform: translateY(101%); }
          to { transform: translateY(0); }
        }
        @keyframes esh-rise-in {
          from { opacity: 0; transform: translateY(14px); }
          to { opacity: 1; transform: translateY(0); }
        }
        @keyframes esh-rail-in {
          from { transform: scaleY(0); }
          to { transform: scaleY(1); }
        }
        @keyframes esh-fade-in {
          from { opacity: 0; }
          to { opacity: 1; }
        }
        @media (prefers-reduced-motion: reduce) {
          .esh-band, .esh-rise, .esh-rail, .esh-fade {
            animation: none;
          }
        }
      `}</style>
    </div>
  );
}

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

npx shadcn@latest add https://designs.first-ch.com/r/editorial-split-hero.json