The courtyard
The warehouse and the new wing face each other across a planted courtyard, with the river walk along the far edge. Hover a marker to read about each part.

Three floors of 1870s brick with the arched windows restored in oak. The two bedroom homes and the penthouse are here, facing the river.
A photo explainer for a development: eyebrow, heading and lead, then a 16:9 photograph with numbered markers placed by percentage; hovering, focusing or clicking a marker or its row in the list under the photo cross-fades that part's title and note into a column beside the photo and moves a primary bar to the matching row.
A photo explainer for a development's sales page: eyebrow, heading and lead, then a wide photograph carrying numbered markers placed by percentage. Hovering, focusing or clicking a marker, or its row in the list under the photo, cross-fades that part's title and note into the column beside the photo, turns the marker to the primary colour and slides a primary bar to the matching row.
Upgrade to Pro
Pro blocks install through the shadcn CLI with your license key and ship their full source. Docs and live previews stay open to everyone, so you can read every block's details first.
Swap YOUR_EMAIL and YOUR_KEY for the email and license key on your account. Find your license key on your account page.
Radix flavor
npx shadcn add "https://beste.dev/r/realestate24?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://beste.dev/r-base/realestate24?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block to components/beste/block/realestate24.tsx plus the badge24 rule eyebrow; it uses no shadcn/ui primitives.
The installed file exports realestate24Demo alongside the block: the exact props behind the preview above. Spread it to get a working section in one line.
import { Realestate24, realestate24Demo } from "@/components/beste/block/realestate24";
export default function Page() {
return <Realestate24 {...realestate24Demo} />;
}Then replace the demo with your own props. Written out, a trimmed setup looks like this:
import { Realestate24 } from "@/components/beste/block/realestate24";
export default function Page() {
return (
<Realestate24
eyebrow="The courtyard"
heading="Two buildings, one garden between them"
description="Hover a marker to read about each part."
image={{
src: "https://oud.pics/b/realestate/alder-courtyard-exterior.webp",
alt: "The brick warehouse and the concrete wing around a birch courtyard",
}}
hotspots={[
{ x: 27, y: 34, title: "The warehouse", description: "Three floors of 1870s brick with the arched windows restored in oak." },
{ x: 74, y: 50, title: "The courtyard wing", description: "Pale concrete with deep reveals and a balcony to every home." },
{ x: 9, y: 72, title: "The river walk", description: "The repaired promenade joins the public path to the station." },
]}
/>
);
}| Prop | Type | Default | Description |
|---|---|---|---|
eyebrow | string | – | Short line above the heading, drawn with a rule in front of it. |
heading | string | – | Section heading. |
description | string | – | Lead paragraph beside the heading. |
image | { src: string; alt: string } | – | The 16:9 photograph the markers sit on. |
hotspots | Hotspot[] | [] | The parts of the picture, in marker order; the first is shown on load. |
className | string | – | Extra classes merged onto the section. |
type Hotspot = {
x: number;
y: number;
title: string;
description: string;
};x and y are percentages of the photo's width and height, measured to the marker's centre, so the markers stay on the same part of the picture at every size; markers are numbered in array order and the list under the photo follows the same order.AnimatePresence with a 0.2 second exit; the current row carries a two-pixel primary bar that slides between rows with a shared layout animation, and the current marker turns primary.load event and on being in view, opens from the top over 1.6 seconds and drifts from -6% to 6% against the scroll; with prefers-reduced-motion it renders open and still.