First CH Designs
PGE-07Pages

Split Band Hero

A hero split into three vertical columns: a text panel, a thin divider rail, and a visual panel. The visual panel breaks into six vertical bands, each holding one colour block of differing position and length that rises from below, staggered by 90ms so the eye is pulled from the visual across to the headline. Zero image assets and zero JS: animation-fill-mode backwards means the finished layout is what renders on SSR, with JS disabled, and under reduced motion. Summary facts are grouped by hairline rules and whitespace instead of cards.

Added:
2026-08-21
Dependencies:
None
tags
#hero #page #split #editorial #stagger #css-only-motion #no-image #no-js

Preview

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>
  );
}

Add to your project via shadcn CLI

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