Component library
Back to catalog

Closing statement footer

A dark footer that closes the page: a large statement beside the ways to reach the business (action, number, email), a quiet layer with the brand, address (only if supplied), pages and services, a legal line, and the business name as a wide sign-off sized to fit. Without a statement the top is one slim contact row.

footerctaclosingdarkwordmarkcontactlocal service

Open preview in a new tab

Use this component

Three ways to get Closing statement footer into one of your own projects. All pre-filled with this component's name and library page.

Or, for the design alone

Placement prompt

What to do, where it goes and what must not change. Paste it into Claude Code inside the destination project.

Read it first
Add Closing statement footer from my Astro Component Library to this Astro project.

Library page, for reference only:
https://astro.baysixmedia.com/components/closing-statement-footer/

Place it in:
[ENTER PAGE, FILE, OR ROUTE]

Position:
[ENTER WHERE IT SHOULD APPEAR]

First read the component source, README, documented props, dependencies, and required assets from the library page. Copy the component and every required companion asset locally into this project. Do not import files from the deployed library at runtime.

Adapt its demo content, links, colors, typography, and images to this project through documented props where possible. Preserve its responsive behavior, accessibility, interaction logic, reduced-motion behavior, and support for multiple instances. Avoid changing unrelated project code.

Test it at desktop, tablet, and mobile widths. Run the project's available check and build commands. Report the files changed and any differences from the library version. Do not commit or deploy.

--- VERIFIED IN THE COMPONENT LIBRARY ---
Production-ready in the library.
No verification run has been recorded.
Passed: integration route, keyboard, no javascript, reduced motion, two instances, no overflow.
This record pre-dates source fingerprinting, so it is not tied to a specific revision.

--- NOT PROVEN ---
- ClientRouter: not applicable — this component ships no script.

--- WHAT THIS DOES NOT CERTIFY ---
Verification was performed on the unmodified component, in an isolated fixture, in the
library. It says nothing about this project's CSS, routes, layout or data, and nothing
about the component once you adapt it. A verified component is a good starting point,
not a guarantee about the installation you are about to build.

--- TEST IN THIS PROJECT AFTER INSTALLING OR CUSTOMISING ---
- Horizontal overflow at 1440, 1280, 1024, 834, 390 and 320px.
- Keyboard operation end to end, including visible focus.
- The no-JavaScript render.
- prefers-reduced-motion: reduce.
- Two instances on one page, if this project renders more than one.
- If this project uses <ClientRouter />: teardown and reinitialisation across a real navigation.

Component reference

A fragment for a request you write yourself, e.g. “I want to add [paste] to Section 3 of my homepage.” It names the component, links its page, and fixes what must not change — the design build and its effects — while leaving branding and content open. It does not carry the code.

Read it first
“Closing statement footer” from my Astro Component Library:
https://astro.baysixmedia.com/components/closing-statement-footer/

Use the existing library component as the implementation source. Preserve its design structure, layout, proportions, spacing system, responsive behavior, visual effects, animations, transitions, hover states, interactions, and accessibility.

The copy, colors, typography, images, and other content may be adapted to match the destination website. Do not redesign, simplify, or reinterpret the component’s underlying design build or effects.

src/components/library/ClosingStatementFooter.astro

Installing this one is more than pasting a section

  • installOne required file, plus supporting files worth taking:
    • src/components/ClosingStatementFooter.astrocomponentrequired
    • src/components/ClosingStatementFooter.mdreadmesupporting
    Use Copy header kit above to get them all, each one labelled with the path it belongs at.
---
/**
 * ClosingStatementFooter (candidate, shell campaign — original, role `footer`)
 *
 * The page's close and the site's footer as one composition on a deep ground. At the top, the closing:
 * a large statement on the left and, on the right, a contact index — the ways to reach the business as
 * equal ruled rows in order of weight: the one action (its label set large, an arrow in the accent),
 * the number, the email. A hairline, then the reference layer: the brand with a supplied description,
 * address and hours (each only when supplied), the page links and the service links as quiet columns.
 * Then the legal line. At the very foot the business name is set as a sign-off fitted exactly to the
 * grid width (SVG textLength, spacing only), one tone off the ground — the last thing on the page.
 *
 * Without a closing statement (a page that already ended on a call to action) the top becomes a slim
 * contact row, so the same button is never asked for twice in a row.
 *
 * Truth rules. Every fact, link and action renders only when passed; the address only when supplied;
 * social links only when supplied (by name, never icons standing in for proof). tel:/mailto: are built
 * from supplied values; the copyright is "© {year} {name}" unless overridden. Empty columns are never
 * drawn.
 *
 * Reads, never declares: --csf-bg, --csf-text, --csf-text-muted, --csf-brand, --csf-brand-text,
 * --csf-border, --csf-family, --csf-display, --csf-grid-max, --csf-grid-gutter, --csf-heading-size.
 */
export interface ClosingStatementFooterLink {
	label: string;
	href: string;
}

interface Props {
	businessName?: string;
	logo?: { src: string; alt?: string; width?: number; height?: number } | null;
	homeHref?: string;
	/** The closing statement. Omitted: the top is a slim contact row instead. */
	closingHeading?: string;
	closingIntro?: string;
	headingLevel?: "h2" | "h3";
	cta?: ClosingStatementFooterLink | null;
	phone?: { number: string; label?: string } | null;
	email?: string;
	/** One or two sentences under the brand, from verified facts. */
	description?: string;
	address?: string | readonly string[];
	addressHref?: string;
	hours?: string | readonly string[];
	primaryLinks?: readonly ClosingStatementFooterLink[];
	serviceLinks?: readonly ClosingStatementFooterLink[];
	legalLinks?: readonly ClosingStatementFooterLink[];
	social?: readonly ClosingStatementFooterLink[];
	labels?: { pages?: string; services?: string; call?: string; email?: string };
	copyright?: string;
	/** The wide name at the foot; on by default. */
	signOff?: boolean;
	fontStylesheet?: string;
	class?: string;
	id?: string;
}

const {
	businessName = "",
	logo = null,
	homeHref = "/",
	closingHeading = "",
	closingIntro = "",
	headingLevel = "h2",
	cta = null,
	phone = null,
	email = "",
	description = "",
	address,
	addressHref,
	hours,
	primaryLinks = [],
	serviceLinks = [],
	legalLinks = [],
	social = [],
	labels = {},
	copyright,
	signOff = true,
	fontStylesheet = "https://fonts.googleapis.com/css2?family=Geist:wght@400;500;600;700&display=swap",
	class: className,
	id,
} = Astro.props;

const safeHref = (href: string | undefined) => {
	const h = (href ?? "").trim();
	return /^(https?:\/\/|mailto:|tel:|#|\/(?!\/))/i.test(h) ? h : null;
};
const clean = (list: readonly ClosingStatementFooterLink[]) => list.map((l) => ({ label: (l?.label ?? "").trim(), href: safeHref(l?.href) })).filter((l): l is { label: string; href: string } => Boolean(l.label && l.href));
const pages = clean(primaryLinks);
const services = clean(serviceLinks);
const legal = clean(legalLinks);
const socials = clean(social).filter((l) => /^https?:\/\//i.test(l.href));
const action = cta && cta.label?.trim() && safeHref(cta.href) ? { label: cta.label.trim(), href: safeHref(cta.href)! } : null;
const digits = (phone?.number ?? "").replace(/[^\d+]/g, "");
const call = phone && phone.number?.trim() && digits.replace(/\D/g, "").length >= 7 ? { number: phone.number.trim(), label: (phone.label ?? labels.call ?? "Call").trim(), href: `tel:${digits}` } : null;
const mail = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim()) ? email.trim() : "";
const lines = (v: string | readonly string[] | undefined) => (Array.isArray(v) ? v : v ? [v] : []).map((x) => String(x).trim()).filter(Boolean);
const addressLines = lines(address);
const hourLines = lines(hours);
const about = description.trim();
const mapHref = addressHref && /^https:\/\//i.test(addressHref.trim()) ? addressHref.trim() : null;
const name = businessName.trim();
const statement = closingHeading.trim();
const hasContact = Boolean(action || call || mail);
const year = new Date().getFullYear();
const rights = copyright === undefined ? (name ? `© ${year} ${name}` : "") : copyright;
const Heading = headingLevel;
/*
 * The sign-off is SVG text fitted exactly to the grid width (textLength, spacing only — glyphs are never
 * stretched). Its size is estimated from the name's length so the spacing adjustment stays small.
 */
const SIGN_W = 1000;
const signFont = name ? Math.round(Math.min(260, SIGN_W / (Math.max(name.length, 4) * 0.5))) : 0;
/* Tall enough for descenders: the baseline sits at 80% of the box. */
const signH = Math.round(signFont * 1.02);
const home = safeHref(homeHref);
const brandFacts = Boolean(about || addressLines.length || hourLines.length);
---

{(name || hasContact || pages.length > 0) && (
	<footer class:list={["csf", className]} id={id} data-closing={statement ? "statement" : "row"} data-brand={brandFacts ? "full" : "name"}>
		{fontStylesheet && <link rel="stylesheet" href={fontStylesheet} />}
		<div class="csf-inner">
			{(statement || hasContact) && (
				<div class="csf-close">
					{statement && (
						<div class="csf-statement">
							<Heading class="csf-heading">{statement}</Heading>
							{closingIntro.trim() && <p class="csf-intro">{closingIntro.trim()}</p>}
						</div>
					)}
					{hasContact && (
						<ul class="csf-reach" role="list">
							{action && (
								<li>
									<a class="csf-row csf-row--action" href={action.href}>
										<span class="csf-row-value">{action.label}</span>
										<svg class="csf-arrow" aria-hidden="true" viewBox="0 0 24 24" width="24" height="24"><path d="M4 12h15M13 6l6 6-6 6" fill="none" stroke="currentColor" stroke-width="1.8" /></svg>
									</a>
								</li>
							)}
							{call && (
								<li>
									<a class="csf-row csf-row--call" href={call.href}>
										<span class="csf-row-key">{call.label}</span>
										<span class="csf-row-value">{call.number}</span>
									</a>
								</li>
							)}
							{mail && (
								<li>
									<a class="csf-row" href={`mailto:${mail}`}>
										<span class="csf-row-key">{labels.email ?? "Email"}</span>
										<span class="csf-row-value">{mail}</span>
									</a>
								</li>
							)}
						</ul>
					)}
				</div>
			)}

			<div class="csf-reference">
				{(name || logo?.src || brandFacts) && (
					<div class="csf-brand">
						{(name || logo?.src) && (home ? (
							<a class="csf-brand-link" href={home}>
								{logo?.src ? <img class="csf-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="csf-wordmark">{name}</span>}
							</a>
						) : (
							<span class="csf-brand-link">{logo?.src ? <img class="csf-logo" src={logo.src} alt={logo.alt || name} width={logo.width} height={logo.height} /> : <span class="csf-wordmark">{name}</span>}</span>
						))}
						{about && <p class="csf-about">{about}</p>}
						{(addressLines.length > 0 || hourLines.length > 0) && (
							<div class="csf-facts">
								{addressLines.length > 0 && (
									<address class="csf-address">
										{mapHref ? <a href={mapHref}>{addressLines.map((l) => <span>{l}</span>)}</a> : addressLines.map((l) => <span>{l}</span>)}
									</address>
								)}
								{hourLines.length > 0 && <p class="csf-hours">{hourLines.map((l) => <span>{l}</span>)}</p>}
							</div>
						)}
					</div>
				)}
				{pages.length > 0 && (
					<nav class="csf-group" aria-label={labels.pages ?? "Pages"}>
						<p class="csf-group-title" aria-hidden="true">{labels.pages ?? "Pages"}</p>
						<ul role="list">
							{pages.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
						</ul>
					</nav>
				)}
				{services.length > 0 && (
					<nav class="csf-group" aria-label={labels.services ?? "Services"}>
						<p class="csf-group-title" aria-hidden="true">{labels.services ?? "Services"}</p>
						<ul role="list">
							{services.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
						</ul>
					</nav>
				)}
			</div>

			{(rights || legal.length > 0 || socials.length > 0) && (
				<div class="csf-legal">
					{rights && <p class="csf-rights">{rights}</p>}
					{(legal.length > 0 || socials.length > 0) && (
						<ul class="csf-legal-links" role="list">
							{legal.map((l) => <li><a href={l.href}>{l.label}</a></li>)}
							{socials.map((l) => <li><a href={l.href} rel="me noopener">{l.label}</a></li>)}
						</ul>
					)}
				</div>
			)}

			{signOff && name && (
				<svg class="csf-sign" aria-hidden="true" focusable="false" viewBox={`0 0 ${SIGN_W} ${signH}`} preserveAspectRatio="xMinYMax meet">
					<text x="0" y={Math.round(signFont * 0.8)} textLength={SIGN_W} lengthAdjust="spacing" font-size={signFont}>{name}</text>
				</svg>
			)}
		</div>
	</footer>
)}

<style>
	.csf {
		--csf-ground: var(--csf-bg, #111416);
		--csf-ink: var(--csf-text, #f3f1ec);
		--csf-soft: var(--csf-text-muted, rgb(243 241 236 / 0.66));
		--csf-accent: var(--csf-brand, #d9c7a3);
		--csf-accent-ink: var(--csf-brand-text, #111416);
		--csf-line: var(--csf-border, rgb(243 241 236 / 0.16));
		--csf-font: var(--csf-family, "Geist", ui-sans-serif, system-ui, sans-serif);
		--csf-font-display: var(--csf-display, "Geist", ui-sans-serif, system-ui, sans-serif);
		--csf-pad: 20px;
		container-type: inline-size;
		overflow: hidden;
		background: var(--csf-ground);
		color: var(--csf-ink);
		font-family: var(--csf-font);
	}
	.csf-inner {
		box-sizing: border-box;
		max-width: var(--csf-grid-max, 1232px);
		margin: 0 auto;
		padding: 56px var(--csf-pad) 20px;
	}
	.csf a {
		color: inherit;
	}
	.csf a:focus-visible {
		outline: 2px solid var(--csf-accent);
		outline-offset: 3px;
		border-radius: 2px;
	}

	/* The close: statement | contact index */
	.csf-close {
		display: grid;
		gap: 32px;
		padding-bottom: 44px;
		border-bottom: 1px solid var(--csf-line);
	}
	.csf[data-closing="row"] .csf-close {
		padding-bottom: 28px;
	}
	.csf[data-closing="statement"] .csf-close {
		padding-bottom: 24px;
	}
	.csf-heading {
		margin: 0;
		max-width: 18ch;
		font-family: var(--csf-font-display);
		font-size: var(--csf-heading-size, clamp(34px, 7.4cqi, 60px));
		font-weight: 600;
		line-height: 1.04;
		letter-spacing: -0.035em;
		text-wrap: balance;
	}
	.csf-intro {
		margin: 18px 0 0;
		max-width: 44ch;
		font-size: 17px;
		line-height: 1.55;
		color: var(--csf-soft);
	}
	.csf-reach {
		display: grid;
		align-content: end;
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.csf-row {
		display: grid;
		grid-template-columns: 5em minmax(0, 1fr);
		align-items: baseline;
		gap: 12px;
		min-height: 56px;
		padding-block: 14px;
		border-top: 1px solid var(--csf-line);
		text-decoration: none;
	}
	.csf-row-key {
		font-size: 13px;
		color: var(--csf-soft);
	}
	.csf-row-value {
		font-size: 18px;
		font-weight: 500;
		overflow-wrap: anywhere;
	}
	/* A number never breaks across lines. */
	.csf-row--call .csf-row-value {
		white-space: nowrap;
		overflow-wrap: normal;
		font-family: var(--csf-font-display);
		font-size: 26px;
		font-weight: 600;
		letter-spacing: -0.015em;
		font-variant-numeric: tabular-nums;
	}
	/* The action is the index's first row: its label large, an arrow in the accent. */
	.csf-row--action {
		grid-template-columns: minmax(0, 1fr) auto;
		align-items: center;
		border-top: 2px solid var(--csf-ink);
	}
	.csf-row--action .csf-row-value {
		font-family: var(--csf-font-display);
		font-size: clamp(22px, 3.2cqi, 30px);
		font-weight: 600;
		letter-spacing: -0.02em;
	}
	.csf-arrow {
		color: var(--csf-accent);
		transition: transform 180ms ease;
	}
	.csf-row--action:hover .csf-arrow {
		transform: translateX(4px);
	}
	.csf-row:hover .csf-row-value {
		text-decoration: underline;
		text-underline-offset: 5px;
		text-decoration-thickness: 1px;
	}
	.csf-row--action:hover .csf-row-value {
		text-decoration-color: var(--csf-accent);
	}

	/* The reference layer */
	.csf-reference {
		display: grid;
		grid-template-columns: repeat(2, minmax(0, 1fr));
		gap: 32px 24px;
		padding-block: 40px 44px;
	}
	.csf-brand {
		grid-column: 1 / -1;
		display: grid;
		gap: 14px;
		align-content: start;
	}
	.csf-brand-link {
		display: inline-flex;
		align-items: center;
		min-height: 44px;
		justify-self: start;
		text-decoration: none;
	}
	.csf-wordmark {
		font-family: var(--csf-font-display);
		font-size: 20px;
		font-weight: 650;
		letter-spacing: -0.02em;
		overflow-wrap: anywhere;
	}
	.csf-logo {
		display: block;
		height: 36px;
		width: auto;
		max-width: 220px;
		object-fit: contain;
	}
	.csf-about {
		margin: 0;
		max-width: 38ch;
		font-size: 15px;
		line-height: 1.55;
		color: var(--csf-soft);
	}
	.csf-facts {
		display: grid;
		grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
		gap: 16px 24px;
		font-size: 14px;
		line-height: 1.55;
		color: var(--csf-soft);
	}
	.csf-address,
	.csf-hours {
		display: grid;
		margin: 0;
		font-style: normal;
	}
	.csf-address a {
		display: grid;
		text-decoration: none;
	}
	.csf-address a:hover {
		color: var(--csf-ink);
	}
	.csf-group-title {
		margin: 0 0 12px;
		font-size: 13px;
		color: var(--csf-soft);
	}
	.csf-group ul {
		display: grid;
		gap: 2px;
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.csf-group a {
		display: inline-flex;
		align-items: center;
		min-height: 36px;
		font-size: 15px;
		text-decoration: none;
	}
	.csf-group a:hover {
		text-decoration: underline;
		text-underline-offset: 4px;
	}

	.csf-legal {
		display: flex;
		flex-wrap: wrap;
		align-items: center;
		justify-content: space-between;
		gap: 8px 24px;
		padding-top: 20px;
		border-top: 1px solid var(--csf-line);
		font-size: 13px;
		color: var(--csf-soft);
	}
	.csf-rights {
		margin: 0;
	}
	.csf-legal-links {
		display: flex;
		flex-wrap: wrap;
		gap: 0 20px;
		margin: 0;
		padding: 0;
		list-style: none;
	}
	.csf-legal-links a {
		display: inline-flex;
		align-items: center;
		min-height: 36px;
		text-decoration: none;
	}
	.csf-legal-links a:hover {
		color: var(--csf-ink);
		text-decoration: underline;
		text-underline-offset: 4px;
	}

	/* The sign-off: the name fitted to the grid width, one tone off the ground, sitting on the footer's foot. */
	/* One solid tone a step off the ground (mixed, not transparent): quiet, but crisp. */
	.csf-sign {
		display: block;
		width: 100%;
		height: auto;
		margin-top: 28px;
		color: color-mix(in srgb, var(--csf-ink) 17%, var(--csf-ground));
		user-select: none;
	}
	.csf-sign text {
		font-family: var(--csf-font-display);
		font-weight: 700;
		letter-spacing: -0.02em;
		fill: currentColor;
	}

	@container (min-width: 760px) {
		.csf-close {
			grid-template-columns: minmax(0, 7fr) minmax(0, 5fr);
			column-gap: 56px;
			align-items: end;
		}
		.csf[data-closing="statement"] .csf-close {
			padding-bottom: 44px;
		}
		/* Beside the statement the index closes on its own rule; stacked, the section's rule closes it. */
		.csf[data-closing="statement"] .csf-reach {
			border-bottom: 1px solid var(--csf-line);
		}
		.csf-row--call .csf-row-value {
			font-size: 22px;
		}
		.csf[data-closing="row"] .csf-close {
			grid-template-columns: 1fr;
		}
		.csf[data-closing="row"] .csf-reach {
			grid-template-columns: repeat(auto-fit, minmax(0, 1fr));
			column-gap: 32px;
			border-bottom: 0;
		}
		.csf-reference {
			grid-template-columns: minmax(0, 5fr) repeat(2, minmax(0, 3fr));
		}
		.csf[data-brand="name"] .csf-reference {
			grid-template-columns: minmax(0, 4fr) repeat(2, minmax(0, 4fr));
		}
		.csf-brand {
			grid-column: auto;
		}
	}
	@container (min-width: 960px) {
		/* A container query cannot style its own container: the gutter is set on its children. */
		.csf > * {
			--csf-pad: var(--csf-grid-gutter, 48px);
		}
		.csf-inner {
			padding-top: 88px;
		}
		.csf-close,
		.csf[data-closing="statement"] .csf-close {
			padding-bottom: 64px;
		}
		.csf-reference {
			padding-block: 48px 56px;
		}
		.csf-sign {
			margin-top: 40px;
		}
		.csf-row--call .csf-row-value {
			font-size: 26px;
		}
	}

	@media (prefers-reduced-motion: reduce) {
		.csf-arrow {
			transition: none;
		}
		.csf-row--action:hover .csf-arrow {
			transform: none;
		}
	}
	@media (forced-colors: active) {
		.csf-row--action {
			border-top-color: CanvasText;
		}
		.csf-sign {
			color: GrayText;
		}
	}
</style>