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本引く理由まで説明がつきます。
小さな会社なので、同時に関われる数には限りがあります。そのぶん、担当が最後まで変わらないこと、更新のたびに事情を説明し直さなくていいことを、お約束にしています。
十年後に読み返しても恥ずかしくない仕事を。それが、私たちがいまも動かしていない基準です。
代表取締役山田 太郎
※ 掲載の文面・氏名はサンプルです。
"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