Features & services
Features Collage
Photo collage with floating highlight chips beside a heading, numbered feature cards and an action.
Layout and motion from the Findit template — see it in contextimport { FlipButton } from "@/components/ui/flip-button";
import { cn } from "@/lib/utils";
/*
* Features Collage — a photo collage with floating highlight chips beside a heading, numbered feature cards and an
* action.
* Source: marcframe/findit → components/sections/BenefitsSection.tsx (+ design-system HighlightChip, BenefitItem).
*
* Layout kept from Framer: below 1024px the collage (400px tall, 700px from 768px) sits above the text, 64px apart
* (80px from 768px); from 1024px they share the row in equal halves, 96px apart, the collage as tall as the text.
* Collage: a main photo 85% × 80% in the middle, two smaller photos (45% wide, 234:225) in the top-right and bottom-left
* corners with a shadow, and five chips floating at fixed spots (hidden below 768px). Cards: the number in a 75px
* column beside the title and text (the number above them on phones). The section sits on the muted surface.
* The button rolls its label (flip-button, spring bounce 0, 0.4s); nothing else moves.
* Dropped: Findit's dotted, uppercase label. Photos are decorative; the chips are a list.
*/
export type CollageFeature = { title: string; text: string };
export type FeaturesCollageProps = {
/** Small line above the heading. */
label?: string;
title?: string;
lead?: string;
/** Main photo, then the top-right and bottom-left ones (decorative). */
images?: [string, string, string];
/** Up to five short highlights floating over the photos from 768px. */
highlights?: string[];
features?: CollageFeature[];
action?: { label: string; href: string };
className?: string;
};
const photo = (id: string, w = 1200) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=${w}&q=80`;
const chipPositions = [
"top-[70px] left-1/2 -translate-x-1/2",
"top-[250px] left-0",
"top-1/2 right-0 -translate-y-1/2",
"bottom-[285px] left-0",
"bottom-[61px] left-1/2 -translate-x-1/2",
];
const defaults = {
label: "Why us",
title: "A home search that feels simple",
lead: "Our job goes beyond listings: we guide you through every step, so buying, renting or selling feels calm and clear.",
images: [
photo("photo-1600210492493-0946911123ea", 1400),
photo("photo-1586023492125-27b2c045efd7", 700),
photo("photo-1724582586529-62622e50c0b3", 700),
] as [string, string, string],
highlights: ["Trusted expertise", "Tailored to you", "Seamless process", "Strong market insight", "After-sales support"],
features: [
{ title: "Buy a new home", text: "A shortlist that fits your budget and your life, viewings on your schedule and honest advice on every offer." },
{ title: "Rent a home", text: "Checked listings, clear contracts and a team that answers when something needs fixing." },
{ title: "Sell a home", text: "A fair valuation, photos that do the place justice and buyers we have already met." },
],
action: { label: "More about us", href: "/about" },
};
export function FeaturesCollage({
label = defaults.label,
title = defaults.title,
lead = defaults.lead,
images = defaults.images,
highlights = defaults.highlights,
features = defaults.features,
action = defaults.action,
className,
}: FeaturesCollageProps) {
const [main, top, bottom] = images;
return (
<section className={cn("bg-muted py-(--section-y)", className)}>
<div className="mx-auto flex max-w-(--container) flex-col gap-16 px-(--gutter) md:gap-20 lg:flex-row lg:gap-24">
<div className="relative h-100 md:h-175 lg:h-auto lg:flex-1">
<img
src={main}
alt=""
className="absolute top-1/2 left-1/2 h-4/5 w-[85%] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-background object-cover"
/>
<img src={top} alt="" className="absolute top-0 right-0 aspect-[234/225] w-[45%] rounded-xl bg-background object-cover shadow-lg" />
<img src={bottom} alt="" className="absolute bottom-0 left-0 aspect-[234/225] w-[45%] rounded-xl bg-background object-cover shadow-lg" />
{highlights.length > 0 && (
<ul className="hidden md:block">
{highlights.slice(0, 5).map((highlight, i) => (
<li
key={highlight}
className={`absolute flex items-center gap-2.5 rounded-full bg-background py-3 pr-4 pl-3 whitespace-nowrap text-foreground shadow-lg ${chipPositions[i]}`}
>
<span aria-hidden className="grid size-5.5 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className="size-3">
<path d="M3.5 8.5l3 3 6-7" />
</svg>
</span>
{highlight}
</li>
))}
</ul>
)}
</div>
<div className="flex flex-col items-start gap-12 lg:flex-1">
<div className="flex flex-col gap-5">
<div className="flex flex-col gap-2.5">
{label && <p className="text-sm font-medium text-foreground/65">{label}</p>}
<h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2>
</div>
{lead && <p className="max-w-xl text-pretty text-foreground/65">{lead}</p>}
</div>
<ol className="flex w-full flex-col gap-5">
{features.map((feature, i) => (
<li key={feature.title} className="flex flex-col gap-5 rounded-xl bg-background p-5 md:flex-row md:gap-0">
<span className="w-19 shrink-0 font-heading text-h3 font-semibold tracking-tight tabular-nums">
{String(i + 1).padStart(2, "0")}.
</span>
<div className="flex flex-1 flex-col gap-2.5">
<h3 className="font-heading text-lg font-semibold tracking-tight">{feature.title}</h3>
<p className="text-muted-foreground">{feature.text}</p>
</div>
</li>
))}
</ol>
{action && <FlipButton href={action.href}>{action.label}</FlipButton>}
</div>
</div>
</section>
);
}Installation
$ npx shadcn@latest add https://framebrick.co/r/blocks/features-collage.json1. Add the Framebrick UI components it uses (they bring Motion).
npx shadcn@latest add https://framebrick.co/r/flip-button.json2. Add the block tokens to
app/globals.cssonce (theming).npx shadcn@latest add https://framebrick.co/r/blocks/tokens.json3. Copy the block into
components/features-collage.tsx.import { FlipButton } from "@/components/ui/flip-button"; import { cn } from "@/lib/utils"; /* * Features Collage — a photo collage with floating highlight chips beside a heading, numbered feature cards and an * action. * Source: marcframe/findit → components/sections/BenefitsSection.tsx (+ design-system HighlightChip, BenefitItem). * * Layout kept from Framer: below 1024px the collage (400px tall, 700px from 768px) sits above the text, 64px apart * (80px from 768px); from 1024px they share the row in equal halves, 96px apart, the collage as tall as the text. * Collage: a main photo 85% × 80% in the middle, two smaller photos (45% wide, 234:225) in the top-right and bottom-left * corners with a shadow, and five chips floating at fixed spots (hidden below 768px). Cards: the number in a 75px * column beside the title and text (the number above them on phones). The section sits on the muted surface. * The button rolls its label (flip-button, spring bounce 0, 0.4s); nothing else moves. * Dropped: Findit's dotted, uppercase label. Photos are decorative; the chips are a list. */ export type CollageFeature = { title: string; text: string }; export type FeaturesCollageProps = { /** Small line above the heading. */ label?: string; title?: string; lead?: string; /** Main photo, then the top-right and bottom-left ones (decorative). */ images?: [string, string, string]; /** Up to five short highlights floating over the photos from 768px. */ highlights?: string[]; features?: CollageFeature[]; action?: { label: string; href: string }; className?: string; }; const photo = (id: string, w = 1200) => `https://images.unsplash.com/${id}?auto=format&fit=crop&w=${w}&q=80`; const chipPositions = [ "top-[70px] left-1/2 -translate-x-1/2", "top-[250px] left-0", "top-1/2 right-0 -translate-y-1/2", "bottom-[285px] left-0", "bottom-[61px] left-1/2 -translate-x-1/2", ]; const defaults = { label: "Why us", title: "A home search that feels simple", lead: "Our job goes beyond listings: we guide you through every step, so buying, renting or selling feels calm and clear.", images: [ photo("photo-1600210492493-0946911123ea", 1400), photo("photo-1586023492125-27b2c045efd7", 700), photo("photo-1724582586529-62622e50c0b3", 700), ] as [string, string, string], highlights: ["Trusted expertise", "Tailored to you", "Seamless process", "Strong market insight", "After-sales support"], features: [ { title: "Buy a new home", text: "A shortlist that fits your budget and your life, viewings on your schedule and honest advice on every offer." }, { title: "Rent a home", text: "Checked listings, clear contracts and a team that answers when something needs fixing." }, { title: "Sell a home", text: "A fair valuation, photos that do the place justice and buyers we have already met." }, ], action: { label: "More about us", href: "/about" }, }; export function FeaturesCollage({ label = defaults.label, title = defaults.title, lead = defaults.lead, images = defaults.images, highlights = defaults.highlights, features = defaults.features, action = defaults.action, className, }: FeaturesCollageProps) { const [main, top, bottom] = images; return ( <section className={cn("bg-muted py-(--section-y)", className)}> <div className="mx-auto flex max-w-(--container) flex-col gap-16 px-(--gutter) md:gap-20 lg:flex-row lg:gap-24"> <div className="relative h-100 md:h-175 lg:h-auto lg:flex-1"> <img src={main} alt="" className="absolute top-1/2 left-1/2 h-4/5 w-[85%] -translate-x-1/2 -translate-y-1/2 rounded-xl bg-background object-cover" /> <img src={top} alt="" className="absolute top-0 right-0 aspect-[234/225] w-[45%] rounded-xl bg-background object-cover shadow-lg" /> <img src={bottom} alt="" className="absolute bottom-0 left-0 aspect-[234/225] w-[45%] rounded-xl bg-background object-cover shadow-lg" /> {highlights.length > 0 && ( <ul className="hidden md:block"> {highlights.slice(0, 5).map((highlight, i) => ( <li key={highlight} className={`absolute flex items-center gap-2.5 rounded-full bg-background py-3 pr-4 pl-3 whitespace-nowrap text-foreground shadow-lg ${chipPositions[i]}`} > <span aria-hidden className="grid size-5.5 shrink-0 place-items-center rounded-full bg-primary text-primary-foreground"> <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" className="size-3"> <path d="M3.5 8.5l3 3 6-7" /> </svg> </span> {highlight} </li> ))} </ul> )} </div> <div className="flex flex-col items-start gap-12 lg:flex-1"> <div className="flex flex-col gap-5"> <div className="flex flex-col gap-2.5"> {label && <p className="text-sm font-medium text-foreground/65">{label}</p>} <h2 className="font-heading text-h2 font-semibold tracking-tight text-balance">{title}</h2> </div> {lead && <p className="max-w-xl text-pretty text-foreground/65">{lead}</p>} </div> <ol className="flex w-full flex-col gap-5"> {features.map((feature, i) => ( <li key={feature.title} className="flex flex-col gap-5 rounded-xl bg-background p-5 md:flex-row md:gap-0"> <span className="w-19 shrink-0 font-heading text-h3 font-semibold tracking-tight tabular-nums"> {String(i + 1).padStart(2, "0")}. </span> <div className="flex flex-1 flex-col gap-2.5"> <h3 className="font-heading text-lg font-semibold tracking-tight">{feature.title}</h3> <p className="text-muted-foreground">{feature.text}</p> </div> </li> ))} </ol> {action && <FlipButton href={action.href}>{action.label}</FlipButton>} </div> </div> </section> ); }
Usage
import { FeaturesCollage } from "@/components/features-collage";
<FeaturesCollage
title="A home search that feels simple"
images={["/why/main.jpg", "/why/top.jpg", "/why/bottom.jpg"]}
highlights={["Trusted expertise", "Tailored to you"]}
features={[{ title: "Buy a new home", text: "…" }]}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | Why us | Small muted line above the heading. |
| title / lead | string | — | Heading (h2) and lead. |
| images | [main, topRight, bottomLeft] — string | — | Photo URLs for the collage (decorative). |
| highlights | string[] | 5 highlights | Chips floating over the photos from 768px; five fill the spots. |
| features | { title, text }[] | 3 features | Numbered cards in an ordered list; titles are h3. |
| action | { label, href } | More about us | Rolling-label button under the cards. |
| className | string | — | Classes for the <section>. |
- Below 1024px the collage (400px, 700px from 768px) sits above the text; from 1024px they share the row and the collage matches the text's height.
- Chips are hidden below 768px. The section sits on the muted surface; only the button moves.
AI prompt
Paste into v0, Lovable, Bolt or Claude to rebuild this block in your own project.
Create a React + Tailwind CSS block called FeaturesCollage (Next.js). - <section className="bg-muted py-(--section-y)">, container mx-auto max-w-(--container) px-(--gutter): stacked (gap-16 / md:gap-20); from 1024px a row of two equal halves (gap-24). - Collage (relative, h-100 / md:h-175, stretched to the text from 1024px): a main photo 85% × 80% centered, two photos 45% wide (234:225) in the top-right and bottom-left corners with shadow-lg, all rounded-xl object-cover; from 768px five chips at absolute spots (top 70px centered, top 250px left, middle right, 285px from the bottom left, 61px from the bottom centered): rounded-full bg-background shadow-lg, a 22px round bg-primary check and the label. - Text: small muted label, h2 (font-heading text-h2 semibold), muted lead; an <ol> of cards (rounded-xl bg-background p-5): the number "01." in a 76px column (font-heading text-h3) beside an h3 title (text-lg semibold) and muted text — the number above on phones; a button whose label rolls on hover. - Only shadcn variables and block tokens (secondary text on bg-muted is text-foreground/65: muted-foreground falls under 4.5:1 there).











