Listing Detail Page React block for Tailwind CSS v4
v1.0.2Updated in 1.9.0
A React listing detail page for a classifieds marketplace: photo gallery, price card, seller, map, contact actions and similar listings.
Swipe or arrow through the photos (thumbnails under the stage or beside it, a counter on phones); hover zooms with a mouse, a click, tap or Enter opens the full screen viewer and Escape closes it. The heart on the photo and Save in the price card save the listing together, at once; a rejected onFavoriteChange rolls both back with a toast. The share button uses the system share sheet where there is one, otherwise it copies the link (Link copied). Send message opens a dialog (a bottom sheet on phones) with the listing, quick replies as tags that fill the text, and the message field: Send or Cmd/Ctrl+Enter sends, an empty message shows its error, a failed send shows an alert and keeps the text; success turns the dialog into Message sent with Go to the conversation. Show phone shows the masked number, fetches the full one through onRevealPhone and fades it in as a tel: link (announced); the sticky bar's Call reveals and dials in one tap. Read more expands the description (clamped at 8 lines); URLs in it stay plain text. The map is read only with the approximate area; on touch a tap opens it full height. Report this listing opens a dialog with the reasons as radio cards and optional details. Similar listings are a swipeable rail with arrows in its header. Below 1024px the title, price and contact buttons sit under the photos, the seller card follows the description, and once those buttons scroll away a bar with the price, Call and Message stays at the bottom. Reserved shows a warning banner; sold shows a neutral banner, disables the contact buttons and puts the similar listings first. Sections rise in once on mount; reduced motion skips it.
Listing Detail Page is a Pro React block in the React blocks category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add listing-detail-page, together with Alert, Badge, Breadcrumb, Button, Carousel and 24 more and the npm packages motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add listing-detail-pageListing Detail Page is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import ListingDetailPage from @/components/blocks/listing-detail-page.
// app/(market)/listing/[slug]/page.tsx (the shell lives in app/(market)/layout.tsx; mount <Toaster /> once there)
"use client"
import { useRouter } from "next/navigation"
import { ListingDetailPage } from "@/components/blocks/listing-detail-page"
export default function ListingPage() {
const router = useRouter()
return (
<ListingDetailPage
onMessage={(text) => fetch("/api/messages", { method: "POST", body: JSON.stringify({ listing: "l01", text }) })}
onRevealPhone={() => fetch("/api/listings/l01/phone").then((response) => response.text())}
onFavoriteChange={(favorite) => fetch("/api/favorites/l01", { method: favorite ? "PUT" : "DELETE" })}
onOpenConversation={() => router.push("/messages?listing=l01")}
/>
)
}Props
listingListingDetailThe listing (LISTING_DETAIL_SAMPLE.listing: l01, Trek FX 2 city bike, $280 negotiable, Portland Pearl District). ListingData without attributes plus images (ProductGalleryImage[]), description (plain text, blank lines split paragraphs), attributes (DescriptionListItem[]), views, favorites, adId, place { city, area, point, radiusKm }, courier { name, price } and the breadcrumb path.
sellerSellerDataThe seller card (LISTING_DETAIL_SAMPLE.seller: Daniel Reed, 4.8 from 126 reviews, replies within an hour). Its name labels the contact buttons.
sellerListingsListingData[]The seller's other listings in the seller card, up to three (LISTING_DETAIL_SAMPLE.sellerListings).
similarListingData[]The similar listings rail (LISTING_DETAIL_SAMPLE.similar, 8 listings around Portland). An empty array hides the section.
favoritebooleanSaved to favorites, controlled; pair with onFavoriteChange.
defaultFavoritedefaultfalsebooleanSaved to favorites at first, uncontrolled.
onFavoriteChange(favorite: boolean) => (void | Promise<unknown>)A heart was toggled. The change shows at once; a rejected promise rolls it back and shows a toast.
onMessage(text: string) => (void | Promise<unknown>)Sends the dialog's message. A promise drives the Send spinner; a rejection shows its message in the dialog. Default: a pretend 900 ms request.
onRevealPhone() => (string | Promise<string>)Fetches the full number when Show phone or Call is pressed. Default: resolves "(503) 555-0142" after 500 ms.
onCall(phone: string) => voidReplaces the tel: link once the number is shown (the demo shows a toast).
onShare() => voidThe share buttons. Default: navigator.share where available, else copies the listing link and shows Link copied.
onReport(report: ListingReport) => (void | Promise<unknown>)Sends the report dialog (ListingReport = { reason, details }). A rejection shows an alert in the dialog. Default: a pretend 900 ms request, then a thank you toast.
onOpenConversation() => voidGo to the conversation after a sent message. Default: navigates to /messages?listing=<id> through the link provider.
statusdefault"active""active" | "reserved" | "sold"reserved: a warning banner, contact still allowed. sold: a neutral banner, contact disabled, grey photos and similar ads first.
galleryThumbnailsdefault"bottom""start" | "bottom" | "none"Where the gallery's thumbnails sit on wide screens; phones swipe with a counter.
showMapdefaulttruebooleanThe Location section with the map.
showSafetydefaulttruebooleanThe Stay safe note under the seller.
stickyBardefaulttruebooleanBelow 1024px, the bottom bar with the price, Call and Message once the contact buttons scroll away.
defaultMessageOpendefaultfalsebooleanOpens the message dialog on mount.
labelsPartial<ListingDetailPageLabels>Every visible string (DEFAULT_LISTING_DETAIL_PAGE_LABELS, English), counted ones as functions; nested gallery, lightbox, price, favorite, listingCard, carousel, sellerCard, map, stickyBar and breadcrumbLabels for the composed items. LISTING_DETAIL_PAGE_LABELS_EN is the same set under its older name.
localedefault"en-US"stringIntl locale for prices, counts and dates.
currencydefault"USD"stringThe currency of amounts without their own (the demo converts the sample to EUR).
nowDate | numberThe clock for Today at 2:32 PM. Default: useNow() (the date while hydrating); the demo passes Sep 24, 2026, 4:40 PM.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Send message, the sticky bar, the map pin, the quick replies and the hearts (neutral hearts are red).
radiusdefault"md""sm" | "md" | "lg"The surfaces: gallery, price card, seller card, map and listing cards 12 / 16 / 24px.
loadingdefaultfalsebooleanThe gallery, the texts, the price card, the seller and the similar ads as skeletons of their real shape.
animateIndefaulttruebooleanSections rise in once when the page mounts on the client (never over server HTML, never with reduced motion).
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for the breadcrumb, the seller and the listing cards. Default: the provider's (the shell's), then <a>.
classNamestringClasses for the root.
classNamesPartial<Record<"breadcrumb" | "banner" | "gallery" | "summary" | "attributes" | "description" | "location" | "report" | "aside" | "priceCard" | "seller" | "safety" | "similar" | "stickyBar", string>>Classes for the sections.
Parts
DEFAULT_LISTING_DETAIL_PAGE_LABELS
constantThe English strings for every visible label, the default of labels. Type: ListingDetailPageLabels.
LISTING_DETAIL_PAGE_LABELS_EN
constantThe same English strings under their older name, kept for existing imports.
LISTING_DETAIL_SAMPLE
constantThe demo data in USD: the Trek FX 2 city bike listing, its seller, three seller listings and eight similar ones.
LISTING_DETAIL_SAMPLE_EN
constantThe same sample priced in EUR, converted at LISTING_DETAIL_USD_PER_EUR (the currency example).
LISTING_DETAIL_APARTMENT_SAMPLE
constantA second sample: a 2-bedroom apartment in Portland listed by a real estate agency.
createListingDetailSample
functionBuilds the bike sample: ({ currency?: "USD" | "EUR" }) => ListingDetailSample.
createListingDetailApartmentSample
functionBuilds the apartment sample in USD or EUR, same options as createListingDetailSample.
LISTING_DETAIL_NOW
constantThe demo clock (Sep 24, 2026, 4:40 PM) as a timestamp, passed as now so relative dates stay stable.
LISTING_DETAIL_USD_PER_EUR
constantThe rounded dollars per euro rate (1.09) used to convert the EUR sample.
ListingDetailPageProps
typeProps of ListingDetailPage, including the div attributes of the root.
ListingDetail
typeThe listing shape: ListingData plus images, description, attributes, views, place, courier and breadcrumb.
ListingReport
typeWhat onReport receives: { reason: string; details: string }.
Examples
Message dialog
Quick replies fill the message; send it to see the Message sent view.
Reserved
A warning banner; messaging and calling still work.
Sold
A neutral banner, contact disabled and similar listings first.
Apartment from an agency
Greenleaf Realty as a company seller, the price per sq ft, a side thumbnail rail and brand actions.
Loading
Gallery, text, price card and seller as skeletons of their real shape.
Euro prices
en-GB formatting and the same listing priced in euro.
Dependencies
Files
- components/blocks/listing-detail-page.tsx
- components/blocks/listing-detail-page-data.ts
FAQ
Is the React Listing Detail Page block free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Listing Detail Page?
Run npx wingo-ui@latest add listing-detail-page. It also adds the Wingo UI items it builds on (Alert, Badge, Breadcrumb, Button, Carousel and 24 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/listing-detail-page.
How does Listing Detail Page behave on phones?
Swipe or arrow through the photos (thumbnails under the stage or beside it, a counter on phones); hover zooms with a mouse, a click, tap or Enter opens the full screen viewer and Escape closes it.
See also
More React blocks
Articles using this component
- Shadcn Ecommerce Template: Five Screens That Work on Phones
Build it: screens and apps11 min read
- React Marketplace Template That Works Like an App
Build it: screens and apps15 min read