First CH Designs
BLK-41Blocks

Message from the President — Vertical Accent Editorial

A president's-message block for company pages. No portrait, no badges: the composition rests on the contrast between a single vertical-writing headline and a two-column horizontal body. Line breaks in the vertical headline are fixed by rendering one line per element in a flex-row-reverse row rather than letting the browser wrap on height, which also allows per-line indents. Emphasis comes from Japanese sesame dots (text-emphasis) in amber instead of colour or bold, and half-width digits stand upright via text-combine-upright. The body flows through CSS multi-column with a hairline column rule and collapses to one column in narrow containers. Motion is limited to a scroll-like vertical mask reveal and rule scaling — transform and opacity only. No rounded corners, no uppercase kicker, no trailing arrow link; it closes on a title and signature.

Added:
2026-08-26
Dependencies:
None
tags
#block #message #greeting #company #vertical-writing #editorial #multi-column #mincho #hairline #signature #no-image #no-dependency

Preview

代表挨拶

つくって終わる仕事を、私たちは選びません

創業から16年、同じ姿勢で。

数あるウェブ制作会社のなかから、このページを開いてくださってありがとうございます。

私たちは、公開日を締め切りだとは思っていません。サイトの価値が決まるのは、公開したあとの何年かです。だから、つくって渡して終わりにできる仕事の受け方を、はじめから選びませんでした。

打ち合わせでいちばん時間を使うのは、デザインの好みではなく事業の言葉です。誰に、何を、どの順番で伝えたいのか。そこさえ決まっていれば、罫線を1本引く理由まで説明がつきます。

小さな会社なので、同時に関われる数には限りがあります。そのぶん、担当が最後まで変わらないこと、更新のたびに事情を説明し直さなくていいことを、お約束にしています。

十年後に読み返しても恥ずかしくない仕事を。それが、私たちがいまも動かしていない基準です。

代表取締役山田 太郎

BLK-41 / Message

※ 掲載の文面・氏名はサンプルです。

"use client";

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

// preload: false は和文フォント必須(unicode-range で約37分割されるため、preload すると
// 全ページに数十本の <link rel="preload"> がぶら下がる)。
const zenOldMincho = Zen_Old_Mincho({
  weight: ["400", "700"],
  subsets: ["latin"],
  display: "swap",
  preload: false,
});

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

/**
 * 代表メッセージ(縦書きアクセント×エディトリアル)
 *
 * 差別化: ギャラリー既存のタイポ系ブロックはスクロール量やタブ操作で「文字が動くこと」自体が
 * 主役だが、本ブロックは動かない読み物として、縦組みの本見出し1本と横組み2段の長文本文という
 * 組み方向の対比だけで間を作る(既存の縦組みはいずれも10px級のミニラベルで、本文見出しではない)。
 * 飽和パターンの角丸・英字キッカー・末尾の「→」リンクは意図的に外し、締めは署名にした。
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 * - **縦組み見出しの折り返しはブラウザに任せない**: [writing-mode:vertical-rl] の要素を高さで
 *   折り返させると、フォント読み込みや枠の高さで改行位置が毎回変わる。行を配列で持ち、1行=1要素を
 *   flex-row-reverse で並べると「右から左へ」の行順を保ったまま改行位置を固定できる(行ごとの
 *   字下げも pt- で自由に効く)。
 * - **傍点(圏点)で和文にアクセントを置く**: [text-emphasis:filled_sesame_#d97706] は縦組みでは
 *   自動的に行の右側に付く。色や太字で強調するより文章の格を落とさず、未対応環境では単に消えるので
 *   フォールバックも安全。
 * - **縦中横**: 半角数字は縦組みだと寝てしまう。数字だけを span で囲って
 *   [text-combine-upright:all] を当てると2桁までが1文字幅に立つ。
 * - **横組み2段は multi-column で**: grid で2つに割ると原稿を手で分割することになる。columns-2 +
 *   [column-rule:1px_solid_#e5e7eb] なら流し込みのまま、段間の hairline も1プロパティで済む。
 *   段抜けを防ぐため各段落に [break-inside:avoid] を付け、狭幅ではコンテナクエリで1段に戻す。
 * - マスクリビールは縦組みでは「上から降ろす」: overflow-hidden の親+子の -translate-y-full。
 *   縦書きの読み進む向きと一致するので、掛け軸を下ろすように見える。
 */

/** 見出し1行=セグメントの配列(dot: true で傍点) */
type Segment = { t: string; dot?: boolean };
type Line = { segments: Segment[]; indentEm?: number };

const headlineLines: Line[] = [
  { segments: [{ t: "つくって終わる仕事を、" }] },
  {
    segments: [{ t: "私たちは" }, { t: "選びません", dot: true }, { t: "。" }],
    indentEm: 1.6,
  },
];

const bodyParagraphs = [
  "私たちは、公開日を締め切りだとは思っていません。サイトの価値が決まるのは、公開したあとの何年かです。だから、つくって渡して終わりにできる仕事の受け方を、はじめから選びませんでした。",
  "打ち合わせでいちばん時間を使うのは、デザインの好みではなく事業の言葉です。誰に、何を、どの順番で伝えたいのか。そこさえ決まっていれば、罫線を1本引く理由まで説明がつきます。",
  "小さな会社なので、同時に関われる数には限りがあります。そのぶん、担当が最後まで変わらないこと、更新のたびに事情を説明し直さなくていいことを、お約束にしています。",
  "十年後に読み返しても恥ずかしくない仕事を。それが、私たちがいまも動かしていない基準です。",
];

export default function CeoMessageEditorial() {
  const rootRef = useRef<HTMLElement>(null);
  const [active, setActive] = useState(false);

  // 画面内に入ったら一度だけリビール(非対応環境では即完成状態)
  useEffect(() => {
    const el = rootRef.current;
    if (!el || typeof IntersectionObserver === "undefined") {
      setActive(true);
      return;
    }
    const io = new IntersectionObserver(
      (entries) => {
        if (entries.some((e) => e.isIntersecting)) {
          setActive(true);
          io.disconnect();
        }
      },
      { threshold: 0.15 },
    );
    io.observe(el);
    return () => io.disconnect();
  }, []);

  const fade =
    `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
      active
        ? "translate-y-0 opacity-100"
        : "translate-y-4 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
    }`;

  return (
    <section
      ref={rootRef}
      aria-labelledby="ceo-message-editorial-heading"
      className="@container w-full max-w-5xl bg-white px-1"
    >
      {/* 広い枠では縦組みの見出し柱を「右」に置く(縦書きは右から始まるため row-reverse) */}
      <div className="flex flex-col gap-10 @3xl:flex-row-reverse @3xl:gap-14">
        {/* 縦組みの見出し柱 */}
        <div className="flex shrink-0 flex-row-reverse items-stretch gap-4 @3xl:gap-5">
          {/* 和文ミニラベル(英字キッカーは使わない) */}
          <span
            className={`shrink-0 pt-1 text-[11px] leading-none font-medium tracking-[0.35em] text-gray-400 [writing-mode:vertical-rl] ${fade}`}
            style={{ transitionDelay: "0ms" }}
          >
            代表挨拶
          </span>

          {/* アンバーの縦罫(上から引かれる) */}
          <span
            aria-hidden="true"
            className={`w-px shrink-0 origin-top bg-amber-600 transition-transform duration-[900ms] ease-out motion-reduce:transition-none ${
              active ? "scale-y-100" : "scale-y-0 motion-reduce:scale-y-100"
            }`}
            style={{ transitionDelay: "120ms" }}
          />

          <h2
            id="ceo-message-editorial-heading"
            className={`${zenOldMincho.className} flex flex-row-reverse items-start gap-2 text-[clamp(1.55rem,5.2cqw,2.1rem)] leading-[1.5] font-bold tracking-[0.08em] text-gray-900 @3xl:gap-3`}
          >
            {headlineLines.map((line, i) => (
              <span
                key={i}
                className="block overflow-hidden [writing-mode:vertical-rl]"
                style={{ paddingTop: line.indentEm ? `${line.indentEm}em` : undefined }}
              >
                <span
                  className={`block transition-transform duration-[900ms] ease-out motion-reduce:transition-none ${
                    active
                      ? "translate-y-0"
                      : "-translate-y-full motion-reduce:translate-y-0"
                  }`}
                  style={{ transitionDelay: `${200 + i * 160}ms` }}
                >
                  {line.segments.map((s, j) =>
                    s.dot ? (
                      <span
                        key={j}
                        className="[text-emphasis:filled_sesame_#d97706] [-webkit-text-emphasis:filled_sesame_#d97706]"
                      >
                        {s.t}
                      </span>
                    ) : (
                      <span key={j}>{s.t}</span>
                    ),
                  )}
                </span>
              </span>
            ))}
          </h2>

          {/* 添え書き(縦中横つき・見出しの読み上げには含めない) */}
          <span
            className={`self-end pb-1 text-[13px] leading-[2] tracking-[0.1em] text-gray-500 [writing-mode:vertical-rl] ${fade}`}
            style={{ transitionDelay: "560ms" }}
          >
            創業から
            <span className="[text-combine-upright:all]">16</span>
            年、同じ姿勢で。
          </span>
        </div>

        {/* 横組みの本文 */}
        <div className="min-w-0 flex-1">
          <p
            className={`text-[16px] leading-[2] font-medium tracking-[0.03em] text-gray-900 @3xl:text-[17px] ${fade}`}
            style={{ transitionDelay: "320ms" }}
          >
            数あるウェブ制作会社のなかから、このページを開いてくださって
            <br className="hidden @3xl:inline" />
            ありがとうございます。
          </p>

          <span
            aria-hidden="true"
            className={`mt-7 block h-px w-full origin-left bg-gray-200 transition-transform duration-[900ms] ease-out motion-reduce:transition-none ${
              active ? "scale-x-100" : "scale-x-0 motion-reduce:scale-x-100"
            }`}
            style={{ transitionDelay: "400ms" }}
          />

          <div
            className={`mt-7 @2xl:columns-2 @2xl:gap-10 @2xl:[column-rule:1px_solid_#e5e7eb] ${fade}`}
            style={{ transitionDelay: "480ms" }}
          >
            {bodyParagraphs.map((text, i) => (
              <p
                key={i}
                className="mb-5 text-[14.5px] leading-[2.1] tracking-[0.02em] text-gray-600 [break-inside:avoid] last:mb-0 @2xl:text-[15px]"
              >
                {text}
              </p>
            ))}
          </div>

          {/* 署名(末尾の「→」リンクは置かない) */}
          <p
            className={`mt-10 text-right ${fade}`}
            style={{ transitionDelay: "600ms" }}
          >
            <span className="block text-[12px] leading-none tracking-[0.2em] text-gray-500">
              代表取締役
            </span>
            <span
              className={`${zenOldMincho.className} mt-3 flex items-center justify-end gap-3 text-[21px] leading-none tracking-[0.22em] whitespace-nowrap text-gray-900 @3xl:text-[26px]`}
            >
              山田 太郎
              <span aria-hidden="true" className="h-3 w-3 shrink-0 bg-amber-600" />
            </span>
          </p>

          <div className="mt-8 flex flex-col gap-2 @xl:flex-row @xl:items-center @xl:justify-between @xl:gap-6">
            <span
              className={`${montserrat.className} text-[10px] leading-none font-semibold tracking-[0.18em] whitespace-nowrap text-gray-300`}
            >
              BLK-41 / Message
            </span>
            <p className="text-[12px] leading-[1.8] text-gray-400">
              ※ 掲載の文面・氏名はサンプルです。
            </p>
          </div>
        </div>
      </div>
    </section>
  );
}

Add to your project via shadcn CLI

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