Velocity X Components by Aidxn Design

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.

tsx
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. 1. In Framer: Assets → Code → Create Code File.
  2. 2. Delete the placeholder and paste this entire file — it already carries its default export and addPropertyControls.