Component librarylocal catalog
Back to catalog

Social proof closer

A dark closing section stacking a pull quote beside review artwork, a row of headline figures, and a call to action over a soft glow.

sectiontestimonialstatsctadark

Open preview in a new tab

Use this component

Three ways to get Social proof closer into one of your own projects, all pre-filled with this component's name and library page.

Start here

Component reference

Paste it into 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 the branding and content open.

Read it first
“Social proof closer” from my Astro Component Library:
https://astro.baysixmedia.com/components/social-proof-closer/

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.

ChatGPT brief

Ask ChatGPT to plan a more detailed customization. Use it when you are still deciding which project the component belongs in, where on the page it sits, and what needs changing. It answers with a Claude Code prompt to run.

Read it first
Help me add this component to one of my Astro websites:

Component: Social proof closer
Library page: https://astro.baysixmedia.com/components/social-proof-closer

I want it added to:
[ENTER PROJECT, PAGE, OR ROUTE]

Place it:
[ENTER LOCATION OR SELECTOR]

Customize it for:
[ENTER BUSINESS, CONTENT, COLORS, IMAGES, OR OTHER CHANGES]

Create a complete Claude Code implementation prompt. Tell Claude to read the component's source, README, documented props, dependencies, and required assets from the library page; copy everything locally into the target project; preserve its responsive behavior, accessibility, interactions, reduced-motion behavior, and multiple-instance safety; test it at desktop, tablet, and mobile widths; run the target project's available check and build commands; report all changed files and differences from the library version; and avoid committing or deploying.

Placement prompt

Send the implementation request straight to Claude Code. Use it when you already know which project, which page and where on that page it goes. Paste it into Claude Code inside the destination project and it does the work.

Read it first
Add Social proof closer from my Astro Component Library to this Astro project.

Library page:
https://astro.baysixmedia.com/components/social-proof-closer

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.

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

Position:
[ENTER WHERE IT SHOULD APPEAR]

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.

src/components/library/SocialProofCloser.astro

This component is not one file on its own

  • assetsCopy these across too — they are referenced by URL, so nothing in the source above points at them: public/social-proof-closer/review-cards-1200.webp. Leave them behind and those URLs resolve to nothing on the other end. What each component does without them is in its README.
---
/**
 * Social proof closer
 *
 * A dark full-bleed section that stacks three proof devices and then closes:
 * a pull quote beside review artwork, a row of headline figures, and a tinted
 * band carrying the final heading and call to action over a soft glow.
 *
 * No JavaScript. The reference section this was measured from has no buttons,
 * tabs, accordions, carousels, scroll triggers or entrance animations — its
 * only transition is a 0.15s colour change on the button — so none of that is
 * invented here either.
 *
 * Self-contained: no imports, no packages, no global stylesheet. Every colour
 * and measurement is a `--pc-*` custom property on the root.
 */

export interface ProofStat {
	/** The figure itself, e.g. "40k+". */
	value: string;
	/** The line under it. */
	label: string;
}

interface Props {
	/** Small masthead above the quote, e.g. a publication name. `""` hides it. */
	source?: string;
	/** The pull quote. Rendered inside a `<blockquote>`. */
	quote?: string;
	/** Attribution under the quote. `""` hides it. */
	attribution?: string;
	/** Link under the quote. Needs `quoteHref` to become a link. */
	quoteLinkLabel?: string;
	/** Destination for the quote link. Without one the label is plain text. */
	quoteHref?: string;
	/** Artwork beside the quote. Omit for a drawn placeholder. */
	image?: string;
	/** Alternative text for the artwork. */
	imageAlt?: string;
	/** Aspect ratio of the artwork frame. */
	imageRatio?: string;
	/** The headline figures. */
	stats?: ProofStat[];
	/** Small print under the figures. `""` hides it. */
	disclaimer?: string;
	/** Heading in the closing band. */
	closingTitle?: string;
	/** Call to action label. `""` hides the button. */
	ctaLabel?: string;
	/** Destination. Without one the label renders as text, not a link. */
	ctaHref?: string;
	/** For pages where `h2` is the wrong level for the closing heading. */
	headingLevel?: "h1" | "h2" | "h3";
	/** Ground behind the whole section. */
	background?: string;
	/** Tint over the closing band. */
	closingTint?: string;
	/** The glow behind the closing band. `transparent` removes it. */
	glow?: string;
	/** Primary text colour. */
	ink?: string;
	/** Secondary text: attribution, labels, small print. */
	muted?: string;
	/** Button fill and link colour. */
	accent?: string;
	/** Text on the button. */
	accentInk?: string;
	/** Height of the closing band once there is room. */
	closingHeight?: string;
	/** Caps the figures row and centres it. */
	statsMaxWidth?: string;
	/** Caps the whole content column. */
	maxWidth?: string;
	/** Added to the root `<section>`. */
	class?: string;
	/** Root element id. */
	id?: string;
}

const {
	source = "The Ledger Review",
	quote =
		"Northbeam turns a weekend of spreadsheet wrangling into something you finish before the kettle boils.",
	attribution =
		"From a fictional publication, used here as sample copy. No real review, rating or endorsement is represented.",
	quoteLinkLabel = "Read the write-up",
	quoteHref = "/press",
	image = undefined,
	imageAlt = "",
	imageRatio = "15 / 8",
	stats = [
		{ value: "40k+", label: "Sample teams onboarded" },
		{ value: "120k", label: "Demo records processed" },
		{ value: "9 yrs", label: "Of made-up history" },
		{ value: "NBM", label: "Placeholder ticker" },
	],
	disclaimer =
		"Every figure, quote and name in this component is fictional demonstration content. Nothing here describes a real product, company or result.",
	closingTitle = "Good tools, quietly done.",
	ctaLabel = "Open an account",
	ctaHref = "/signup",
	headingLevel = "h2",
	background = "#3b2b7d",
	closingTint = "rgb(35 11 89 / 60%)",
	glow = "rgb(150 130 255 / 45%)",
	ink = "#f0f0f2",
	muted = "rgb(255 255 255 / 50%)",
	accent = "#4840bb",
	accentInk = "#ffffff",
	closingHeight = "300px",
	statsMaxWidth = "1000px",
	maxWidth = "1233px",
	class: className,
	id,
} = Astro.props;

const Heading = headingLevel;

const style = [
	`--pc-bg:${background}`,
	`--pc-tint:${closingTint}`,
	`--pc-glow:${glow}`,
	`--pc-ink:${ink}`,
	`--pc-muted:${muted}`,
	`--pc-accent:${accent}`,
	`--pc-accent-ink:${accentInk}`,
	`--pc-closing-h:${closingHeight}`,
	`--pc-stats-max:${statsMaxWidth}`,
	`--pc-max:${maxWidth}`,
	`--pc-ratio:${imageRatio}`,
].join(";");
---

<section class:list={["proof", className]} id={id} style={style}>
	<div class="top">
		<div class="inner">
			{/* Quote left, artwork right — one column until there is genuinely
			    room for two, which the reference puts at 1280. */}
			<div class="split">
				<div class="quote-col">
					{source && <p class="source">{source}</p>}
					<blockquote class="quote">
						<span class="mark" aria-hidden="true">&ldquo;</span>
						<p class="quote-text">{quote}</p>
					</blockquote>
					{
						quoteLinkLabel &&
							(quoteHref ? (
								<a class="quote-link" href={quoteHref}>
									{quoteLinkLabel}
								</a>
							) : (
								/* No destination, so no link: a dead anchor that looks
								   clickable is worse than plain text. */
								<span class="quote-link is-static">{quoteLinkLabel}</span>
							))
					}
					{attribution && <p class="note">{attribution}</p>}
				</div>

				<div class="media-col">
					<figure class="shot">
						{
							image ? (
								<img src={image} alt={imageAlt} loading="lazy" decoding="async" />
							) : (
								/* Drawn, not loaded: with no artwork yet the block still has
								   to look deliberate rather than broken. */
								<span class="placeholder" aria-hidden="true">
									<span class="ph-card"></span>
									<span class="ph-card"></span>
									<span class="ph-card"></span>
								</span>
							)
						}
					</figure>
				</div>
			</div>

			{
				(stats.length > 0 || disclaimer) && (
					<div class="stats-wrap">
						{stats.length > 0 && (
							<ul class="stats">
								{stats.map((stat) => (
									<li class="stat">
										<span class="stat-value">{stat.value}</span>
										<span class="stat-label">{stat.label}</span>
									</li>
								))}
							</ul>
						)}
						{disclaimer && <p class="note disclaimer">{disclaimer}</p>}
					</div>
				)
			}
		</div>
	</div>

	<div class="closing">
		{/* The reference bleeds an oversized artwork off the corner behind this
		    band and clips it. A radial gradient does the same job with no file
		    to ship and no resolution to pick. */}
		<span class="glow" aria-hidden="true"></span>
		<div class="closing-inner">
			<Heading class="closing-title">{closingTitle}</Heading>
			{
				ctaLabel &&
					(ctaHref ? (
						<a class="cta" href={ctaHref}>
							{ctaLabel}
						</a>
					) : (
						<span class="cta is-static">{ctaLabel}</span>
					))
			}
		</div>
	</div>
</section>

<style>
	.proof {
		box-sizing: border-box;
		background: var(--pc-bg);
		color: var(--pc-ink);
		font-family:
			system-ui,
			-apple-system,
			"Segoe UI",
			Roboto,
			sans-serif;
		font-size: 16px;
		line-height: 1.5;
		text-align: start;
	}

	/* Set rather than inherited, so a destination site's global rules cannot
	   change the geometry this section is built on. */
	.proof *,
	.proof *::before,
	.proof *::after {
		box-sizing: border-box;
	}

	.proof p,
	.proof ul,
	.proof li,
	.proof figure,
	.proof blockquote,
	.proof h1,
	.proof h2,
	.proof h3 {
		margin: 0;
		padding: 0;
	}

	.proof ul {
		list-style: none;
	}

	.proof img {
		display: block;
		max-width: 100%;
	}

	/* --- upper block ------------------------------------------------------- */

	.top {
		padding: 64px 16px;
	}

	.inner {
		display: flex;
		flex-direction: column;
		gap: 48px;
		max-width: var(--pc-max);
		margin-inline: auto;
	}

	.split {
		display: grid;
		grid-template-columns: 1fr;
		gap: 32px;
		align-items: center;
	}

	.quote-col {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 16px;
		min-width: 0;
		text-align: center;
	}

	.source {
		color: var(--pc-ink);
		font-size: 0.8125rem;
		font-weight: 700;
		letter-spacing: 0.16em;
		text-transform: uppercase;
		opacity: 0.85;
	}

	.quote {
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 4px;
	}

	.mark {
		display: block;
		margin-bottom: -0.15em;
		color: var(--pc-ink);
		font-family: Georgia, "Times New Roman", serif;
		font-size: 3.5rem;
		font-weight: 700;
		line-height: 0.7;
		opacity: 0.6;
	}

	.quote-text {
		max-width: 44ch;
		font-size: 1.125rem;
		line-height: 1.2;
	}

	.quote-link {
		color: var(--pc-muted);
		font-size: 0.875rem;
		text-decoration: underline;
		text-underline-offset: 3px;
		transition: color 150ms cubic-bezier(0.4, 0, 0.2, 1);
	}

	.quote-link:hover {
		color: var(--pc-ink);
	}

	.quote-link.is-static {
		text-decoration: none;
		cursor: default;
	}

	.note {
		max-width: 62ch;
		color: var(--pc-muted);
		font-size: 0.875rem;
		line-height: 1.15;
	}

	/* --- artwork ----------------------------------------------------------- */

	.media-col {
		min-width: 0;
	}

	.shot {
		overflow: hidden;
		aspect-ratio: var(--pc-ratio);
		border-radius: 14px;
		background: rgb(255 255 255 / 6%);
	}

	.shot img {
		width: 100%;
		height: 100%;
		object-fit: cover;
	}

	.placeholder {
		display: flex;
		flex-direction: column;
		justify-content: center;
		gap: 10px;
		width: 100%;
		height: 100%;
		padding: 8%;
		background: linear-gradient(
			160deg,
			rgb(255 255 255 / 10%),
			rgb(255 255 255 / 3%)
		);
	}

	.ph-card {
		display: block;
		height: 18%;
		border-radius: 8px;
		background: rgb(255 255 255 / 16%);
	}

	.ph-card:first-child {
		width: 82%;
	}

	.ph-card:nth-child(2) {
		width: 96%;
		background: rgb(255 255 255 / 24%);
	}

	.ph-card:last-child {
		width: 68%;
	}

	/* --- figures ------------------------------------------------------------ */

	.stats-wrap {
		display: flex;
		flex-direction: column;
		gap: 12px;
		align-items: center;
		width: 100%;
		max-width: var(--pc-stats-max);
		margin-inline: auto;
	}

	/*
	 * One line, always — the row never wraps. Below the wide breakpoint it
	 * carries three figures and the rest are hidden outright, which is the
	 * reference's behaviour and the only way a single row stays legible on a
	 * phone.
	 */
	.stats {
		display: flex;
		flex-wrap: nowrap;
		justify-content: center;
		gap: 12px;
		width: 100%;
	}

	/*
	 * `1 1 0` rather than a pixel basis: every figure takes an equal share of
	 * whatever width there is and shrinks with it. A basis in pixels is what
	 * makes a `nowrap` row overflow, and what made the wrapping version break
	 * into two rows at 390px and four at 320px.
	 */
	.stat {
		display: flex;
		flex: 1 1 0;
		flex-direction: column;
		align-items: center;
		gap: 6px;
		min-width: 0;
		text-align: center;
	}

	.stat-value,
	.stat-label {
		overflow-wrap: break-word;
	}

	/*
	 * Below the breakpoint only the first three are shown. `display: none`, so
	 * the figure leaves the layout and the accessibility tree together and a
	 * screen-reader user is told exactly what a sighted user is shown — rather
	 * than being clipped to nothing while still being announced.
	 *
	 * Pass more than three figures and the extras only appear on wide screens.
	 */
	@media (max-width: 1139.98px) {
		.stat:nth-child(n + 4) {
			display: none;
		}
	}

	.stat-value {
		font-size: 1.5rem;
		font-weight: 700;
		line-height: 0.9;
		letter-spacing: -0.03em;
	}

	.stat-label {
		color: var(--pc-muted);
		font-size: 0.875rem;
		line-height: 1.15;
	}

	.disclaimer {
		text-align: center;
	}

	/* --- closing band -------------------------------------------------------- */

	.closing {
		position: relative;
		display: flex;
		align-items: center;
		justify-content: center;
		overflow: hidden;
		min-height: var(--pc-closing-h);
		padding: 48px 16px;
		background: var(--pc-tint);
	}

	.glow {
		position: absolute;
		/* Oversized and pushed off the corner, then clipped — the same trick the
		   reference uses with a bitmap, done with a gradient. */
		top: -70%;
		right: -35%;
		width: 110%;
		aspect-ratio: 2 / 1;
		border-radius: 50%;
		background: radial-gradient(circle, var(--pc-glow), transparent 70%);
		pointer-events: none;
	}

	.closing-inner {
		position: relative;
		display: flex;
		flex-direction: column;
		align-items: center;
		gap: 24px;
		width: 100%;
		max-width: var(--pc-max);
		text-align: center;
	}

	.closing-title {
		font-size: clamp(2rem, 1.2rem + 3.6vw, 3.875rem);
		font-weight: 700;
		line-height: 0.9;
		letter-spacing: -0.03em;
	}

	.cta {
		display: inline-flex;
		align-items: center;
		justify-content: center;
		padding: 12px 20px;
		border-radius: 6px;
		background: var(--pc-accent);
		color: var(--pc-accent-ink);
		font-size: 1.125rem;
		font-weight: 500;
		text-decoration: none;
		transition: filter 150ms cubic-bezier(0.4, 0, 0.2, 1);
	}

	.cta:hover {
		filter: brightness(1.12);
	}

	.cta.is-static {
		background: rgb(255 255 255 / 12%);
		color: var(--pc-ink);
		cursor: default;
	}

	/* One ring for every control. The reference leaves the browser default. */
	.quote-link:focus-visible,
	.cta:focus-visible {
		outline: 3px solid var(--pc-ink);
		outline-offset: 3px;
		border-radius: 3px;
	}

	/* --- wider ---------------------------------------------------------------- */

	@media (min-width: 768px) {
		.stats-wrap {
			padding-inline: 40px;
		}
	}

	/*
	 * 1140px, measured: the reference shows three figures at 1139 and four at
	 * 1140. It is a custom breakpoint, not the 1280 the four test widths might
	 * suggest — bisected across 1100–1200 to find it.
	 */
	@media (min-width: 1140px) {
		.stat-value {
			font-size: 2rem;
		}
	}

	@media (min-width: 1280px) {
		.top {
			padding-block: 64px;
		}

		.split {
			grid-template-columns: 1fr 1fr;
			gap: 64px;
		}
	}

	/* --- reduced motion --------------------------------------------------------- */

	/*
	 * Only colour transitions exist here, and they go. The reference keeps its
	 * 0.15s button transition running regardless of the preference.
	 */
	@media (prefers-reduced-motion: reduce) {
		.quote-link,
		.cta {
			transition: none;
		}
	}
</style>