BLK-09Blocks
サイトフッター
白地にhairlineの区切りだけで組む3段フッター。1段目=サービス/会社案内/法務の多段リンク(SPは横罫の積み上げ・PCは縦罫の列にdivideの向きを入れ替える)、2段目=小さく再掲したロゴ+住所1行+電話・受付、3段目=コピーライトと「トップへ戻る」。背景の色面もカードも使わず、ページ末尾を重くしない。ロゴはワードマークだけに落としてヘッダーと役割を衝突させない。上端のアンバーtickはIntersectionObserverで1度だけ左からワイプし、リンクのホバーは文字の2px移動と下線のscale-xのみ。画像素材ゼロ(他社コーポレートサイトに見られる3段フッター構造を参考にブランド再構築・2026-08-03)。
- 追加日:
- 2026-08-03
- 依存:
- なし
- tags
- #footer #sitemap #corporate #hairline #no-card #back-to-top #a11y #no-image
プレビュー
"use client";
import { useEffect, useRef, useState } from "react";
import { Montserrat } from "next/font/google";
const montserrat = Montserrat({
weight: ["600", "700"],
subsets: ["latin"],
display: "swap",
});
/**
* サイトフッター / Site Footer
* 参考: 他社コーポレートサイトに見られるフッター(多段リンク・罫線だけの整理)。
* 構造だけ借りて First CH のブランド(白 + アンバー #d97706 / Montserrat + Noto Sans JP)で
* 再構築したもので、見た目のコピーではない。画像素材ゼロ。
*
* 汎用技法メモ(web-design-playbook 還流用):
* - フッターは「面」ではなく「罫線」で組む: 背景色ブロックもカードも使わず、白地に hairline
* (border-gray-200)だけで3段に切る。フッターに色面を敷くとページ末尾が急に重くなり、
* 本文側のCTAより目立ってしまう。
* - 列の区切りはブレークポイントで divide の向きを入れ替える:
* grid-cols-1 divide-y → sm:grid-cols-3 sm:divide-x sm:divide-y-0。
* SPでは横罫(積み上げの段組み)、PCでは縦罫(台帳の列)になり、DOMは1つのまま。
* - ロゴの再掲は最下段に置く: フッター左上に置くとヘッダーと同じロックアップが同じ位置に出て
* 「ヘッダーの複製」に見える。ロゴ+住所+電話を最下段のユーティリティ帯にまとめると、
* 上段のリンク列(=サイトマップ)と役割が衝突しない。モノグラムは外しワードマークだけ再掲する。
* - 見出しは 4px のアンバー角と和文の短いラベルだけ: 意味のない英語の小ラベル(SERVICE 等)を
* 足すと、どのサイトにも移植できる=役割のない装飾になる。
* - 入場モーションは「1本の罫線」に集約: 上端に w-16 のアンバーの tick を重ね、
* IntersectionObserver で1度だけ origin-left の scale-x 0→1 でワイプ。列は translate-y-2→0 と
* opacity のスタッガーのみ。動かすのは transform / opacity だけで、reduced-motion では即時表示。
* - リンクのホバーは「文字ごと 2px 右へ + アンバーの下線ワイプ」。下線を内側の relative span に
* 入れると、文字の translate に下線が追従して長さもズレない。
*
* 実案件への移植:
* <footer> をそのまま最下部に置くだけでよい(デモシェルは同梱していない)。
* 「トップへ戻る」は window.scrollTo で、prefers-reduced-motion: reduce のときは smooth を外す。
*/
type LinkItem = { label: string; href: string };
type Group = { heading: string; links: LinkItem[] };
const GROUPS: Group[] = [
{
heading: "サービス",
links: [
{ label: "コーポレートサイト制作", href: "#" },
{ label: "ランディングページ制作", href: "#" },
{ label: "保守・運用改善", href: "#" },
{ label: "AIコンサルティング", href: "#" },
],
},
{
heading: "会社案内",
links: [
{ label: "会社概要", href: "#" },
{ label: "制作実績", href: "#" },
{ label: "お知らせ", href: "#" },
{ label: "採用情報", href: "#" },
],
},
{
heading: "法務・ご利用について",
links: [
{ label: "プライバシーポリシー", href: "#" },
{ label: "特定商取引法に基づく表記", href: "#" },
{ label: "サイトのご利用について", href: "#" },
],
},
];
function FooterLink({ label, href }: LinkItem) {
return (
<a
href={href}
className="group inline-flex py-1.5 text-[12px] leading-relaxed text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-amber-600"
>
<span className="relative inline-block transition-transform duration-300 ease-out group-hover:translate-x-0.5 group-focus-visible:translate-x-0.5 motion-reduce:transition-none">
{label}
<span
aria-hidden="true"
className="absolute -bottom-0.5 left-0 h-px w-full origin-left scale-x-0 bg-amber-600 transition-transform duration-300 ease-out group-hover:scale-x-100 group-focus-visible:scale-x-100 motion-reduce:transition-none"
/>
</span>
</a>
);
}
export default function SiteFooter() {
const rootRef = useRef<HTMLElement>(null);
const [inView, setInView] = useState(false);
// 画面に入ったら1度だけリビール(reduced-motion 時は motion-reduce: クラスで即時表示)
useEffect(() => {
const el = rootRef.current;
if (!el) return;
const io = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setInView(true);
io.disconnect();
}
},
{ threshold: 0.15 },
);
io.observe(el);
return () => io.disconnect();
}, []);
const toTop = () => {
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
window.scrollTo({ top: 0, behavior: reduce ? "auto" : "smooth" });
};
return (
<footer
ref={rootRef}
className="relative w-full max-w-5xl overflow-hidden rounded-2xl border border-gray-200 bg-white"
>
{/* 上端の罫線に重ねるアンバーの tick(入場で左からワイプ) */}
<span
aria-hidden="true"
className={`absolute top-0 left-0 h-px w-16 origin-left bg-amber-600 transition-transform duration-700 ease-out motion-reduce:transition-none ${
inView ? "scale-x-100" : "scale-x-0 motion-reduce:scale-x-100"
}`}
/>
<div className="px-6 sm:px-8">
{/* 1段目: サイトマップ(SPは横罫の積み上げ / PCは縦罫の列) */}
<nav
aria-label="フッター"
className="grid grid-cols-1 divide-y divide-gray-200 sm:grid-cols-3 sm:divide-x sm:divide-y-0"
>
{GROUPS.map((group, i) => (
<div
key={group.heading}
className={`py-7 transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none sm:px-6 sm:py-9 sm:first:pl-0 sm:last:pr-0 ${
inView
? "translate-y-0 opacity-100"
: "translate-y-2 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
}`}
style={{ transitionDelay: inView ? `${120 + i * 90}ms` : "0ms" }}
>
<h2 className="flex items-center gap-2 text-[11px] font-bold tracking-[0.14em] text-gray-900">
<span
aria-hidden="true"
className="inline-block size-1 shrink-0 bg-amber-600"
/>
{group.heading}
</h2>
<ul className="mt-3">
{group.links.map((link) => (
<li key={link.label}>
<FooterLink {...link} />
</li>
))}
</ul>
</div>
))}
</nav>
{/* 2段目: ロゴ小・住所1行・連絡先(ヘッダーの複製にしないため最下段へ置く) */}
<div className="flex flex-wrap items-center gap-x-5 gap-y-3 border-t border-gray-200 py-6">
<a
href="#"
className="group inline-flex shrink-0 items-baseline rounded-sm whitespace-nowrap focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
>
<span
className={`${montserrat.className} text-[13px] font-bold tracking-[0.16em] text-gray-900`}
>
FIRST CH
</span>
<span
aria-hidden="true"
className="ml-1 inline-block size-1.5 bg-amber-600 transition-transform duration-300 ease-out group-hover:translate-y-[-2px] motion-reduce:transition-none"
/>
</a>
<span
aria-hidden="true"
className="hidden h-3 w-px shrink-0 bg-gray-200 sm:block"
/>
<address className="text-[11px] leading-relaxed font-normal text-gray-500 not-italic">
<span className={`${montserrat.className} tabular-nums`}>
〒150-0000
</span>{" "}
東京都渋谷区0-0-0 サンプルビル5F
</address>
<dl className="ml-auto flex flex-wrap items-baseline gap-x-5 gap-y-1 text-[11px] text-gray-500">
<div className="flex items-baseline gap-2">
<dt className="font-semibold text-gray-700">電話</dt>
<dd className={`${montserrat.className} tabular-nums`}>
03-0000-0000
</dd>
</div>
<div className="flex items-baseline gap-2">
<dt className="font-semibold text-gray-700">受付</dt>
<dd>平日 10:00–19:00</dd>
</div>
</dl>
</div>
{/* 3段目: コピーライト + トップへ戻る */}
<div className="flex flex-col gap-3 border-t border-gray-200 py-5 sm:flex-row sm:items-center sm:justify-between">
<p
className={`${montserrat.className} text-[11px] tracking-[0.08em] text-gray-400`}
>
© 2026 First CH LLC.
</p>
<button
type="button"
onClick={toTop}
className="group inline-flex items-center gap-2 self-start rounded-sm text-[11px] font-semibold text-gray-600 transition-colors hover:text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 sm:self-auto"
>
トップへ戻る
<span
aria-hidden="true"
className="inline-block transition-transform duration-300 ease-out group-hover:-translate-y-0.5 motion-reduce:transition-none"
>
↑
</span>
</button>
</div>
</div>
</footer>
);
}
shadcn CLI でプロジェクトに追加
npx shadcn@latest add https://designs.first-ch.com/r/site-footer.json