Facts-Ledger CTA (Hours & Coverage Inline)
A call-to-action whose main surface is the trust ledger itself. Eight facts — reception hours, closing days, service area, on-site visits, registration numbers, years in business, first-consultation fee and reply time — are set in a two-column ledger keyed by a single lowercase Latin word, and the request button forms the final row as a full-width amber bar. It is neither a split band of copy versus buttons nor a selection or input flow: the place where reading ends is the place where the click happens. Only the long coverage list expands, in place, to all fourteen municipalities. Wrapping is governed by container queries (@2xl) rather than the viewport, so it holds up inside narrow frames. No rounded corners, no uppercase Latin kicker, no trailing arrows — hairline rules and spacing do the binding, and small print stays on white rather than on the amber bar for contrast. All facts are fictional display samples. Zero image assets, zero dependencies.
- Added:
- 2026-09-10
- Dependencies:
- None
- tags
- #cta #section #block #facts #ledger #trust #hours #coverage #container-query #hairline #tabular-nums #brand #motion #no-image
Preview
相談先を決める材料を、
この一枚にまとめました
受付時間もお伺いできる範囲も下のとおりです。目を通したうえでご相談ください。
- 受付時間
- 平日 9:00–18:00 / 土曜 9:00–13:00
- 定休日
- 日曜・祝日(メールは終日受付)
- 対応エリア
- けやき市・さくら台市・なぎさ市 ほか全14市町
- 出張相談
- エリア内は無料(エリア外は実費)
- 登録番号
- ◯◯県知事登録 第00-000000号
- 開業・実績
- 2011年開業 / ご相談 累計 3,200件
- 初回相談
- 無料・60分(お見積りも無料)
- ご返信
- 1営業日以内(土日祝を除く)
"use client";
import { useEffect, useRef, useState } from "react";
import { Montserrat } from "next/font/google";
const montserrat = Montserrat({
weight: ["500", "600"],
subsets: ["latin"],
display: "swap",
preload: false,
});
/**
* 事実表一体型CTA / Facts-Ledger CTA
* 既存のCTAは「文+ボタン」を面や左右分割で置く型と、選択・入力を主役にする型が出揃っているため、
* これは *事実表そのものを面の主役に据え、行動をその台帳の下端に一体化させた* 型にした。
* 面積の大半は8項目の事実台帳(受付時間・定休日・対応エリア・出張・登録番号・実績・相談料・返信)で、
* ボタンは台帳の最終行として全幅の帯になる。既存CTAが持つ事実は最大でも3行なので情報密度でも別物。
* 角丸・英字キッカー・末尾の「→」は使わない。事実はすべて架空の表示サンプル。
*
* 汎用技法メモ(web-design-playbook 還流用):
* - 信頼情報を別セクションへ切らずCTAの面へ入れる: 「営業時間はどこ?」「自分の地域は対象?」は
* クリックを止める最後の疑問なので、答えを導線の直前ではなく *同じ面* に置く。構図としては
* 「左に文・右にボタン」ではなく「台帳が本体・行動が下端の縁」にすると、読み終わりと押す場所が一致する。
* - 台帳の鍵は小文字の英字1語で固定幅の列にする: dt を w-14 の小文字英字(uppercase + 広い字間の
* キッカーにはしない)にすると、値の和文が主になり、2カラムでも視線が鍵の列で揃う。
* 英字は読み上げから外し(aria-hidden)、和文の項目名を sr-only で dt に持たせる。
* - 長い項目だけ行内で開く: 対応エリアのように列挙が長い項目は、要約+行内のトグルで全件を出す。
* 開閉は高さの変化そのものなので transform/opacity では表現できない。トランジションを付けず
* 即時に差し替え、aria-expanded / aria-controls で状態を伝える。
* - 折り返しはビューポートではなくコンテナで決める: 親に @container を付け @2xl:(=672px)で
* 1→2カラムに切り替える。埋め込み先が狭いときに sm:/lg: で起きる「広い画面なのに箱は狭い」ズレが出ない。
* - 全幅のアンバー帯に小さな注記を載せない: 白文字は #d97706 上で 3.6:1 しかなく小さな字がAA未満になる。
* 注記は帯の外(白地・text-gray-500)へ出し、帯には大きめの太字ラベルだけを置く。
* 帯のフォーカスリングは白(focus-visible:-outline-offset-4 で内側に引く)にする。
*/
/** 数値は Montserrat + tabular-nums。時刻や番号の途中で折れないよう nowrap も持たせる */
function Num({ children }: { children: React.ReactNode }) {
return (
<span className={`${montserrat.className} whitespace-nowrap tabular-nums`}>
{children}
</span>
);
}
type Fact = {
/** 台帳の鍵。小文字英字1語で固定幅の列に置く(読み上げは term 側) */
key: string;
/** 読み上げ用の和文項目名 */
term: string;
value: React.ReactNode;
};
const FACTS: Fact[] = [
{
key: "hours",
term: "受付時間",
value: (
<>
平日 <Num>9:00–18:00</Num> / 土曜 <Num>9:00–13:00</Num>
</>
),
},
{
key: "closed",
term: "定休日",
value: <>日曜・祝日(メールは終日受付)</>,
},
// area は行内トグルを持つため、値をここでは持たせず本体で組み立てる
{ key: "area", term: "対応エリア", value: null },
{
key: "visit",
term: "出張相談",
value: <>エリア内は無料(エリア外は実費)</>,
},
{
key: "license",
term: "登録番号",
value: (
<>
◯◯県知事登録 第<Num>00-000000</Num>号
</>
),
},
{
key: "since",
term: "開業・実績",
value: (
<>
<Num>2011</Num>年開業 / ご相談 累計 <Num>3,200</Num>件
</>
),
},
{
key: "fee",
term: "初回相談",
value: (
<>
無料・<Num>60</Num>分(お見積りも無料)
</>
),
},
{
key: "reply",
term: "ご返信",
value: <>1営業日以内(土日祝を除く)</>,
},
];
/** 対応エリアの全件(地名は表示サンプル) */
const AREAS = [
"けやき市",
"さくら台市",
"なぎさ市",
"ひばりが丘市",
"南野市",
"東かえで市",
"若葉市",
"小山田市",
"桜川町",
"白鷺町",
"海原町",
"峰岸町",
"沢渡村",
"三日月村",
];
export default function FactsIntegratedCta() {
const rootRef = useRef<HTMLElement>(null);
const [inView, setInView] = useState(false);
const [areaOpen, setAreaOpen] = useState(false);
useEffect(() => {
const el = rootRef.current;
if (!el) return;
// 縦に長い節でも上端が入れば発火するよう threshold ではなく rootMargin で見る
const io = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setInView(true);
io.disconnect();
}
},
{ threshold: 0, rootMargin: "0px 0px -10% 0px" },
);
io.observe(el);
return () => io.disconnect();
}, []);
const rise = (delay: number) => ({
className: `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
inView
? "translate-y-0 opacity-100"
: "translate-y-2 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
}`,
style: { transitionDelay: inView ? `${delay}ms` : "0ms" },
});
const heading = rise(0);
const lead = rise(80);
const footnote = rise(470);
const action = rise(540);
return (
<section
ref={rootRef}
aria-labelledby="facts-integrated-cta-heading"
className="@container w-full max-w-4xl border border-gray-200 bg-white"
>
{/* 導入は小さく。面の主役は下の台帳なので、ここで色も面も使わない */}
<div className="px-5 py-6 @2xl:px-8 @2xl:py-7">
<h2
id="facts-integrated-cta-heading"
className={`text-lg leading-[1.6] font-bold tracking-tight text-gray-900 @2xl:text-xl ${heading.className}`}
style={heading.style}
>
相談先を決める材料を、
<br />
この一枚にまとめました
</h2>
<p
className={`mt-3 max-w-lg text-[13px] leading-[1.9] text-gray-600 ${lead.className}`}
style={lead.style}
>
受付時間もお伺いできる範囲も下のとおりです。目を通したうえでご相談ください。
</p>
</div>
{/* 事実台帳。面積の大半をここが占める=これが本体 */}
<dl className="grid border-t border-gray-200 @2xl:grid-cols-2">
{FACTS.map((fact, i) => {
const row = rise(160 + i * 35);
const isArea = fact.key === "area";
return (
<div
key={fact.key}
className={`flex flex-col gap-0.5 border-b border-gray-100 px-5 py-3 @md:flex-row @md:items-baseline @md:gap-3 @2xl:px-8 @2xl:py-3.5 @2xl:odd:border-r @2xl:odd:border-r-gray-100 ${row.className}`}
style={row.style}
>
{/* 器が狭いときは鍵を値の上に積む(横並びのまま固定幅の鍵を置くと値が3行に折れる) */}
<dt className="text-[10.5px] leading-[2] font-medium text-gray-400 @md:w-14 @md:shrink-0">
{/* 見せるのは英字1語、読み上げるのは和文 */}
<span aria-hidden="true">{fact.key}</span>
<span className="sr-only">{fact.term}</span>
</dt>
<dd className="text-[13px] leading-[1.9] text-gray-800 @md:flex-1">
{isArea ? (
<>
けやき市・さくら台市・なぎさ市{" "}
<span className="whitespace-nowrap">
ほか全<Num>14</Num>市町
</span>
<button
type="button"
aria-expanded={areaOpen}
aria-controls="facts-integrated-cta-areas"
onClick={() => setAreaOpen((v) => !v)}
className="ml-2 align-baseline text-[11px] font-medium text-gray-700 underline underline-offset-4 transition-colors hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
>
{areaOpen ? "閉じる" : "全域を見る"}
</button>
{areaOpen && (
// 開閉は高さの変化そのものなのでアニメーションさせない
<ul
id="facts-integrated-cta-areas"
className="mt-2 flex flex-wrap gap-x-3 gap-y-1 text-[12px] leading-[1.8] text-gray-600"
>
{AREAS.map((area) => (
<li key={area}>{area}</li>
))}
</ul>
)}
</>
) : (
fact.value
)}
</dd>
</div>
);
})}
</dl>
{/* 注記と副導線。小さい字はアンバー帯に載せず白地に置く(コントラスト) */}
<div
className={`flex flex-col gap-2 px-5 py-4 text-[11px] leading-[1.9] text-gray-500 @2xl:flex-row @2xl:items-center @2xl:justify-between @2xl:px-8 ${footnote.className}`}
style={footnote.style}
>
<p>記載の内容は表示サンプルです(最終更新 2026年9月1日)</p>
<p className="flex flex-wrap items-center gap-x-4 gap-y-1">
<a
href="#"
className="font-medium text-gray-700 underline underline-offset-4 transition-colors hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
>
料金の目安を見る
</a>
<a
href="#"
className="font-medium text-gray-700 underline underline-offset-4 transition-colors hover:text-amber-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
>
よくあるご質問
</a>
</p>
</div>
{/* 行動は台帳の最終行として下端に一体化させる(別の箱にしない・角は丸めない) */}
<div className={action.className} style={action.style}>
<a
href="#"
className="group relative flex items-center justify-center overflow-hidden bg-amber-600 px-5 py-4 text-center text-white focus-visible:outline-2 focus-visible:-outline-offset-4 focus-visible:outline-white"
>
<span
aria-hidden="true"
className="absolute inset-0 origin-left scale-x-0 bg-amber-700 transition-transform duration-500 ease-out group-hover:scale-x-100 group-focus-visible:scale-x-100 motion-reduce:transition-none"
/>
<span className="relative text-[15px] font-bold tracking-tight @2xl:text-base">
無料相談を申し込む
</span>
</a>
</div>
</section>
);
}
Add to your project via shadcn CLI
npx shadcn@latest add https://designs.first-ch.com/r/facts-integrated-cta.json