First CH Designs
PGE-02Pages

エディトリアル大タイポヒーロー

白背景に明朝の巨大英文を主役に据えたエディトリアル型ヒーロー。階段状のぼかしモチーフ・縦書きScroll Downインジケータ・下線ワイプのテキストCTAで、画像素材ゼロのまま"間"と高級感を作る。smoothScrollプロパティでLenisの慣性スクロールをopt-in適用できる(他社コーポレートサイトに見られる構図を参考にブランド再構築・2026-07-25)。

追加日:
2026-07-25
依存:
lenis
tags
#hero #page #editorial #serif #motion #lenis #no-image

プレビュー

Web Production Studio

Be the standard.

つくって、終わらせない。中小企業のWebを、成果の出る標準へ。

"use client";

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

// preload: false は和文フォントでは必須。Zen Old Mincho は unicode-range で約37分割されており、
// preload(既定 true)だと2ウェイト分=74ファイルを <link rel="preload"> でギャラリー全ページに
// 先読みさせる(この標本はトップのカードプレビューに載るため、全ページに効いてしまう)。
// 実測(2026-08-08)でトップのLCPが 19.7s。preload を切って unicode-range の遅延読み込みに任せる。
const zenOldMincho = Zen_Old_Mincho({
  weight: ["400", "700"],
  subsets: ["latin"],
  display: "swap",
  preload: false,
});

/**
 * エディトリアル大タイポヒーロー
 * 参考: 白背景×巨大セリフ英文×階段状のぼかしモチーフ(構図の重心=左上、余白の性格=静的な"間")。
 * smoothScroll を true にするとページ全体に Lenis の慣性スクロールを適用する
 * (ギャラリープレビューでは false。実案件ではルート近くで1回だけ有効化する)。
 */
export default function HeroEditorialTypo({
  smoothScroll = false,
  headingTag: Heading = "h1",
}: {
  smoothScroll?: boolean;
  /**
   * 見出しのタグ。実案件ではそのまま(h1)。ギャラリーのプレビューは1ページに複数の標本が
   * 並ぶため "p" を渡してh1の重複を避ける(見た目はclassNameで決まるので変わらない)。
   */
  headingTag?: "h1" | "p";
}) {
  const [inView, setInView] = useState(false);
  const rootRef = useRef<HTMLDivElement>(null);

  // マウント後にリビール開始(reduced-motion 時は motion-reduce: クラスで即時表示)
  useEffect(() => {
    const id = requestAnimationFrame(() => setInView(true));
    return () => cancelAnimationFrame(id);
  }, []);

  // Lenis 慣性スクロール(opt-in・アンマウントで確実に破棄)
  useEffect(() => {
    if (!smoothScroll) return;
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let lenis: { raf: (t: number) => void; destroy: () => void } | null = null;
    let rafId = 0;
    let cancelled = false;
    import("lenis").then(({ default: Lenis }) => {
      if (cancelled) return;
      lenis = new Lenis({ lerp: 0.12 });
      const raf = (time: number) => {
        lenis?.raf(time);
        rafId = requestAnimationFrame(raf);
      };
      rafId = requestAnimationFrame(raf);
    });
    return () => {
      cancelled = true;
      cancelAnimationFrame(rafId);
      lenis?.destroy();
    };
  }, [smoothScroll]);

  // 階段状のぼかしモチーフ(右上→中央へ降りる。装飾なので aria-hidden)
  const steps = [
    { top: 4, right: 0, w: 150 },
    { top: 9, right: 6, w: 120 },
    { top: 14, right: 2, w: 170 },
    { top: 20, right: 12, w: 110 },
    { top: 26, right: 5, w: 190 },
    { top: 33, right: 16, w: 130 },
    { top: 40, right: 9, w: 160 },
    { top: 48, right: 22, w: 120 },
    { top: 56, right: 14, w: 180 },
    { top: 64, right: 28, w: 110 },
    { top: 72, right: 19, w: 150 },
    { top: 81, right: 34, w: 100 },
  ];

  const reveal = (delayMs: number) =>
    `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
      inView
        ? "translate-y-0 opacity-100"
        : "translate-y-5 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
    }`.concat(` [transition-delay:${delayMs}ms]`);

  return (
    <div
      ref={rootRef}
      className="relative w-full max-w-4xl overflow-hidden rounded-2xl border border-gray-200 bg-white"
    >
      {/* 階段モチーフ */}
      <div aria-hidden="true" className="pointer-events-none absolute inset-0">
        {steps.map((s, i) => (
          <span
            key={i}
            className="absolute h-3 rounded-full bg-gray-300/50 blur-[5px] sm:h-3.5"
            style={{
              top: `${s.top}%`,
              right: `${s.right}%`,
              width: `${s.w}px`,
              transitionProperty: "opacity, transform",
              transitionDuration: "900ms",
              transitionTimingFunction: "cubic-bezier(0.16, 1, 0.3, 1)",
              transitionDelay: `${200 + i * 60}ms`,
              opacity: inView ? 1 : 0,
              transform: inView ? "translateX(0)" : "translateX(24px)",
            }}
          />
        ))}
      </div>

      <div className="relative grid min-h-[420px] grid-cols-[auto_1fr] sm:min-h-[520px]">
        {/* 左端: 縦書きスクロールインジケータ */}
        <div className="flex w-12 flex-col items-center justify-end pb-6 sm:w-16 sm:pb-8">
          <span
            className="text-[10px] font-medium tracking-[0.25em] text-gray-400 uppercase [writing-mode:vertical-rl]"
            aria-hidden="true"
          >
            Scroll Down
          </span>
          <span className="relative mt-3 block h-14 w-px overflow-hidden bg-gray-200 sm:h-20">
            <span className="absolute top-0 left-0 h-5 w-px animate-[hero-et-drop_2.2s_ease-in-out_infinite] bg-amber-600 motion-reduce:animate-none" />
          </span>
        </div>

        {/* 本体 */}
        <div className="flex flex-col justify-center py-14 pr-8 pl-2 sm:py-20 sm:pr-12">
          <p
            className={`text-xs font-bold tracking-[0.25em] text-amber-600 uppercase ${reveal(0)}`}
          >
            Web Production Studio
          </p>
          <Heading
            className={`${zenOldMincho.className} mt-5 text-[clamp(2.6rem,7.5vw,4.75rem)] leading-[1.08] tracking-tight text-gray-900 ${reveal(120)}`}
          >
            Be the standard.
          </Heading>
          <p
            className={`mt-6 max-w-xl text-[17px] leading-[1.8] font-medium tracking-[0.04em] text-gray-800 ${reveal(260)}`}
          >
            つくって、終わらせない。
            <span className="mx-1 inline-block h-px w-6 translate-y-[-0.25em] bg-amber-600 align-middle" />
            中小企業のWebを、成果の出る標準へ。
          </p>
          <div className={`mt-10 ${reveal(400)}`}>
            <a
              href="#"
              className="group inline-flex items-center gap-3 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 className="absolute -bottom-1 left-0 h-px w-full origin-left scale-x-100 bg-gray-900 transition-transform duration-300 ease-out group-hover:scale-x-0" />
                <span 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" />
              </span>
              <span
                aria-hidden="true"
                className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1"
              >
                →
              </span>
            </a>
          </div>
        </div>
      </div>

      {/* インジケータ用 keyframes(このブロック内で完結させる) */}
      <style>{`
        @keyframes hero-et-drop {
          0% { transform: translateY(-100%); }
          55% { transform: translateY(400%); }
          100% { transform: translateY(400%); }
        }
      `}</style>
    </div>
  );
}

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

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