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
プレビュー
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