Custom Framer Component · Free, unlicensed
Proof Stats
Four translucent proof/USP boxes for the top of a hero, with a built-in drafting aid that flags unconfirmed figures until you're ready to publish.
statsproofherosocial-proof
This is a standalone Framer Code File — not installed with the CLI, not gated behind a subscription. Copy the whole file and paste it into Framer via Assets → Code → Create Code File.
Property controls
These are Framer's own addPropertyControls fields,
which show up as the right-hand properties panel once this is pasted into Framer. They are
not React props and have nothing to do with this site's licensed component API.
| Panel label | Control key | Control type |
|---|---|---|
| Proof points | boxes | Array |
| Footnote | footnote | String |
| Show flags | showFlags | Boolean |
| Ink | ink | Color |
| Accent | accent | Color |
| Tint | tint | Number |
Component source
The complete, unmodified file — copy the whole thing.
import { addPropertyControls, ControlType } from "framer"
import { useId } from "react"
export const META = {
slug: "proof-stats",
title: "Proof Stats",
description: "Four translucent proof/USP boxes for the top of a hero, with a built-in drafting aid that flags unconfirmed figures until you're ready to publish.",
tags: ["stats", "proof", "hero", "social-proof"],
category: "custom-framer",
}
/**
* Four proof / USP boxes, built to sit at the very top of a page — above-the-fold real estate
* that a lot of readers never scroll past.
*
* The `status` field is a drafting aid, not a decoration: while building out a page you can mark
* a figure `check` (contradicted somewhere else in your content) or `placeholder` (waiting on a
* real number), turn `Show flags` on, and an unconfirmed figure gets labelled right on the
* canvas so it cannot quietly ship. Turn `Show flags` off before publish — every shipped default
* here is `confirmed`.
*
* WIDTH IS LOCKED TO `fixed` — do not change it. Per Framer's docs `fixed` means the component
* "can fill 100% of its size", while `auto` means it "will dictate its own size based on its
* content". This is built on `width: 100%`, which only resolves under `fixed`. In `auto` the
* container shrink-wraps, 100% computes to 0, and the row collapses to a thin strip.
*
* BUILT FOR A DARK BACKGROUND. The boxes are translucent white over whatever sits behind them,
* so they inherit the hero rather than cutting a panel out of it. On a light background set
* `Ink` to a dark colour and raise `Tint`.
*
* @framerSupportedLayoutWidth fixed
* @framerSupportedLayoutHeight any-prefer-fixed
* @framerIntrinsicWidth 1200
* @framerIntrinsicHeight 220
* @framerDisableUnlink
*/
export default function ProofStats(props) {
const {
boxes = DEFAULT_BOXES,
footnote = DEFAULT_FOOTNOTE,
showFlags = false,
ink = "#FFFFFF",
accent = "#F97316",
tint = 6,
style,
} = props
const scope = "fc-" + useId().replace(/[^a-zA-Z0-9]/g, "")
const list = Array.isArray(boxes) ? boxes : []
if (list.length === 0) return <Empty style={style} />
return (
<div className={scope} style={style}>
<style>{css(scope, { ink, accent, tint })}</style>
<section
className="fc-proofs"
aria-label="Why people choose us"
>
<div className="fc-proof-grid">
{list.map((b, i) => (
<article
className={
"fc-proof" + (b.status === "placeholder" ? " is-pending" : "")
}
key={i}
>
<div className="fc-proof-art" aria-hidden="true">
<Art name={b.art} />
</div>
<p className="fc-proof-value">
{b.value}
{b.footnote ? (
<span className="fc-proof-mark">{b.footnote}</span>
) : null}
</p>
<p className="fc-proof-label">{b.label}</p>
{showFlags && b.status && b.status !== "confirmed" ? (
<p className={`fc-proof-flag fc-flag--${b.status}`}>
<span>
{b.status === "placeholder"
? "Needs a figure"
: "Check the number"}
</span>
{b.note}
</p>
) : null}
</article>
))}
</div>
{footnote ? <p className="fc-proof-foot">{footnote}</p> : null}
</section>
</div>
)
}
const DEFAULT_BOXES = [
{
value: "10,000+",
label: "Projects delivered",
status: "confirmed",
note: "",
art: "peaks",
},
{
value: "4.9",
label: "Average rating",
footnote: "★",
status: "confirmed",
note: "",
art: "stars",
},
{
value: "500+",
label: "5-star reviews",
status: "confirmed",
note: "",
art: "stars",
},
{
value: "15+",
label: "Years in business",
status: "confirmed",
note: "",
art: "bar",
},
]
const DEFAULT_FOOTNOTE = "*Terms and conditions apply."
/* The art is one family on purpose — a quantity filling up. A bar filling to its cap, five
marks appearing, five stars appearing. It reads as "this many" without a second glance,
which is the only job a 104px graphic on a proof box has. */
function Art({ name }) {
if (name === "peaks" || name === "stars") {
const peak = name === "peaks"
return (
<svg viewBox="0 0 120 40" fill="none" aria-hidden="true">
<g
className={peak ? "p-peaks" : "p-stars"}
{...(peak
? {
stroke: "currentColor",
strokeWidth: 1.4,
strokeLinejoin: "round",
strokeLinecap: "round",
}
: { fill: "currentColor" })}
>
{[0, 1, 2, 3, 4].map((n) =>
peak ? (
<path key={n} d={`M${6 + n * 24} 26 ${16 + n * 24} 16 ${26 + n * 24} 26`} />
) : (
<path
key={n}
d={`m${12 + n * 22} 12 2.4 4.9 5.4.8-3.9 3.8.9 5.4-4.8-2.6-4.8 2.6.9-5.4-3.9-3.8 5.4-.8z`}
/>
)
)}
</g>
</svg>
)
}
if (name === "pending") {
return (
<svg viewBox="0 0 120 40" fill="none" aria-hidden="true">
<rect
x="4"
y="12"
width="112"
height="16"
rx="8"
stroke="currentColor"
strokeWidth="1.2"
strokeOpacity=".35"
strokeDasharray="4 5"
/>
</svg>
)
}
return (
<svg viewBox="0 0 120 40" fill="none" aria-hidden="true">
<rect x="4" y="17" width="112" height="6" rx="3" stroke="currentColor" strokeWidth="1" strokeOpacity=".3" />
<rect className="p-fill" x="4" y="17" width="112" height="6" rx="3" fill="currentColor" fillOpacity=".8" />
</svg>
)
}
function Empty({ style }) {
return (
<div
style={{
...style,
display: "grid",
placeItems: "center",
minHeight: 120,
background: "#F4F5F6",
color: "#2F3A48",
font: "500 13px/1.4 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif",
}}
>
Add at least one proof point in the properties panel.
</div>
)
}
function css(scope, { ink, accent, tint }) {
const s = `.${scope}`
return `
/* A shrink-to-fit parent makes \`width: 100%\` compute to 0, which collapses every container
query to its narrowest step. The min-width is the floor; Framer overrides it on canvas. */
${s} { container: fc-proofs-shell / inline-size; width: 100%; min-width: 280px; }
${s} .fc-proofs {
--fc-content-max: 1356px;
--fc-gutter: 30px;
--fc-edge: max(var(--fc-gutter), calc((100% - var(--fc-content-max)) / 2 + var(--fc-gutter)));
container: fc-proofs / inline-size;
box-sizing: border-box;
width: 100%;
padding-inline: var(--fc-edge);
color: ${ink};
font-stretch: 100%;
-webkit-font-smoothing: antialiased;
}
${s} .fc-proofs *, ${s} .fc-proofs *::before, ${s} .fc-proofs *::after { box-sizing: border-box; }
${s} .fc-proof-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: clamp(9px, 1.1cqi, 14px);
}
/* Translucent rather than a solid panel, so the boxes sit ON the hero instead of cutting a
rectangle out of it. That is what lets them go at the very top of the page without the hero
photo losing its top third. */
${s} .fc-proof {
position: relative;
display: flex;
flex-direction: column;
padding: clamp(16px, 2cqi, 24px);
border: 1px solid color-mix(in srgb, ${ink} 16%, transparent);
border-radius: 14px;
background: linear-gradient(160deg,
color-mix(in srgb, ${ink} ${tint}%, transparent),
color-mix(in srgb, ${ink} ${Math.max(1, Math.round(tint * 0.42))}%, transparent));
overflow: hidden;
}
/* The empty fourth box reads as deliberately unfinished, not as a rendering failure. */
${s} .fc-proof.is-pending { border-style: dashed; background: none; }
${s} .fc-proof-art { color: ${accent}; opacity: .55; margin-bottom: clamp(10px, 1.4cqi, 16px); }
${s} .fc-proof-art svg { display: block; width: clamp(72px, 9cqi, 104px); height: auto; }
/* The number is the point of the box, so it carries the weight the old heading wasted. */
${s} .fc-proof-value {
margin: 0;
font-size: clamp(28px, 3.6cqi, 44px);
font-weight: 700;
line-height: 1;
letter-spacing: -.045em;
}
${s} .fc-proof-mark { color: ${accent}; font-size: .5em; vertical-align: super; font-weight: 500; }
${s} .fc-proof-label {
margin: clamp(7px, .9cqi, 11px) 0 0;
color: color-mix(in srgb, ${ink} 76%, transparent);
font-size: clamp(12px, 1.25cqi, 15px);
line-height: 1.35;
}
${s} .fc-proof.is-pending .fc-proof-value,
${s} .fc-proof.is-pending .fc-proof-label { opacity: .5; }
/* WORKING AID, NOT A CUSTOMER-FACING ELEMENT — turn \`Show flags\` off before publish. It is
here so an unconfirmed figure cannot ship quietly while you are still drafting content. */
${s} .fc-proof-flag {
margin: 12px 0 0;
padding-top: 11px;
border-top: 1px dashed color-mix(in srgb, #ffbf00 45%, transparent);
color: color-mix(in srgb, ${ink} 72%, #ffbf00);
font-size: 11px;
line-height: 1.5;
}
${s} .fc-proof-flag span {
display: block;
margin-bottom: 4px;
color: #ffbf00;
font-weight: 600;
letter-spacing: .04em;
text-transform: uppercase;
font-size: 9.5px;
}
${s} .fc-flag--placeholder span { color: #f59f00; }
${s} .fc-proof-foot {
margin: clamp(12px, 1.4cqi, 18px) 0 0;
color: color-mix(in srgb, ${ink} 55%, transparent);
font-size: clamp(10px, 1cqi, 12px);
line-height: 1.5;
}
/* The art animates in once, on load. It is a quantity filling up, so it has to build. */
${s} .fc-proof-art svg .p-fill {
transform-origin: 4px 20px;
transform: scaleX(0);
animation: ${scope}-fill 1100ms cubic-bezier(.22, 1, .36, 1) 200ms forwards;
}
${s} .fc-proof-art svg .p-peaks path,
${s} .fc-proof-art svg .p-stars path { opacity: 0; animation: ${scope}-in 420ms ease forwards; }
${s} .fc-proof-art svg .p-peaks path:nth-child(1),
${s} .fc-proof-art svg .p-stars path:nth-child(1) { animation-delay: 200ms; }
${s} .fc-proof-art svg .p-peaks path:nth-child(2),
${s} .fc-proof-art svg .p-stars path:nth-child(2) { animation-delay: 300ms; }
${s} .fc-proof-art svg .p-peaks path:nth-child(3),
${s} .fc-proof-art svg .p-stars path:nth-child(3) { animation-delay: 400ms; }
${s} .fc-proof-art svg .p-peaks path:nth-child(4),
${s} .fc-proof-art svg .p-stars path:nth-child(4) { animation-delay: 500ms; }
${s} .fc-proof-art svg .p-peaks path:nth-child(5),
${s} .fc-proof-art svg .p-stars path:nth-child(5) { animation-delay: 600ms; }
@keyframes ${scope}-fill { to { transform: scaleX(1); } }
@keyframes ${scope}-in { to { opacity: 1; } }
@container fc-proofs-shell (max-width: 767px) {
${s} .fc-proofs { --fc-gutter: 15px; }
}
@container fc-proofs (max-width: 720px) {
${s} .fc-proof-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@container fc-proofs (max-width: 340px) {
${s} .fc-proof-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (prefers-reduced-motion: reduce) {
${s} .fc-proof-art svg * { animation: none !important; opacity: 1 !important; transform: none !important; }
}
`
}
addPropertyControls(ProofStats, {
boxes: {
type: ControlType.Array,
title: "Proof points",
control: {
type: ControlType.Object,
controls: {
value: { type: ControlType.String, title: "Figure", defaultValue: "" },
label: { type: ControlType.String, title: "Label", defaultValue: "" },
footnote: {
type: ControlType.String,
title: "Marker",
defaultValue: "",
description: "Usually just *. It points at the footnote — do not use one without the other.",
},
art: {
type: ControlType.Enum,
title: "Art",
options: ["bar", "peaks", "stars", "pending"],
optionTitles: ["Bar", "Icons (set)", "Stars", "Pending"],
defaultValue: "bar",
},
status: {
type: ControlType.Enum,
title: "Status",
options: ["confirmed", "check", "placeholder"],
optionTitles: ["Confirmed", "Check the number", "Placeholder"],
defaultValue: "confirmed",
description: "Anything other than Confirmed is flagged on canvas while Show flags is on — a drafting aid for figures you haven't locked in yet.",
},
note: {
type: ControlType.String,
title: "Why",
displayTextArea: true,
defaultValue: "",
description: "What's unconfirmed about this figure. Only ever shown with Show flags on.",
},
},
},
defaultValue: DEFAULT_BOXES,
},
footnote: {
type: ControlType.String,
title: "Footnote",
displayTextArea: true,
defaultValue: DEFAULT_FOOTNOTE,
},
showFlags: {
type: ControlType.Boolean,
title: "Show flags",
defaultValue: false,
description:
"Working aid — labels every unconfirmed figure on the canvas while you draft. Turn off before publish.",
},
ink: {
type: ControlType.Color,
title: "Ink",
defaultValue: "#FFFFFF",
description: "Built for a dark hero. On a light background set this dark and raise Tint.",
},
accent: {
type: ControlType.Color,
title: "Accent",
defaultValue: "#F97316",
},
tint: {
type: ControlType.Number,
title: "Tint",
defaultValue: 6,
min: 0,
max: 100,
step: 1,
unit: "%",
description: "How much the box fill lifts off the background behind it.",
},
})
- 1. In Framer: Assets → Code → Create Code File.
- 2. Delete the placeholder and paste this entire file — it already carries its default export and addPropertyControls.