Marketplace Search Page React block for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React marketplace search results page: count, facets in a sidebar or phone sheet, filter chips, sort, grid or list and endless results.
The breadcrumb leads back to Home and the parent category; the title names the category and the place (Bikes in Cluj-Napoca) and the count under it is announced after every change. From 1024px the filters sit in a sticky sidebar and apply at once: drag the price thumbs over the histogram or pick a preset, tick Condition or Seller (each option shows how many listings it would give), choose the place with a radius (Only here to +100 km, off for All locations), switch Delivery available, choose Posted and the wheel sizes. Below 1024px the Filters button (with the active count) opens a full height sheet whose button counts the draft (See 312 listings) and applies it; Reset clears the draft, Escape or a swipe down closes it. Active filters and the query show as chips under the header (sideways scroll on phones); × removes one, Clear all removes all. Sort opens a menu (a sheet on phones), the grid or list switch cross-fades the results, the bell saves the search with a toast and pressing it again stops following it. Up to three promoted listings lead the results (a swipeable rail on phones). The next 24 listings load as you near the end (or with Show 24 more in button mode); a failed page shows Try again. Filters that match nothing show an empty state with Clear filters. Hearts save a listing at once. Related searches links close the page on laptops. Sections rise in once on mount; reduced motion skips it.
Marketplace Search 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 marketplace-search-page, together with Breadcrumb, Button, Empty State, Filter panel, Listing card and 13 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 marketplace-search-pageMarketplace Search 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 MarketplaceSearchPage from @/components/blocks/marketplace-search-page.
// app/(market)/search/page.tsx (the shell lives in app/(market)/layout.tsx)
"use client"
import { useRouter, useSearchParams } from "next/navigation"
import {
MarketplaceSearchPage,
type MarketplaceSearchState,
} from "@/components/blocks/marketplace-search-page"
export default function SearchPage() {
const router = useRouter()
const params = useSearchParams()
const state: MarketplaceSearchState = {
query: params.get("q") ?? "",
category: params.get("cat") ?? "bikes",
location: params.get("loc") ?? "cluj-napoca",
filters: JSON.parse(params.get("f") ?? "{}"),
sort: params.get("sort") ?? "newest",
view: params.get("view") === "list" ? "list" : "grid",
}
return (
<MarketplaceSearchPage
state={state}
onStateChange={(next) => {
const query = new URLSearchParams({
q: next.query,
cat: next.category,
loc: next.location,
f: JSON.stringify(next.filters),
sort: next.sort,
view: next.view,
})
router.replace(`/search?${query}`, { scroll: false })
}}
onSaveSearch={(search, saved) => fetch("/api/saved-searches", { method: saved ? "POST" : "DELETE", body: JSON.stringify(search) })}
/>
)
}Props
stateMarketplaceSearchStateThe search, controlled with onStateChange: { query, category, location (a ro-locations city id, "" for the whole country), filters (FilterValues keyed by filter id), sort, view }. A real app writes it to the url.
defaultStatePartial<MarketplaceSearchState>The starting search while uncontrolled (DEFAULT_MARKETPLACE_SEARCH_STATE: Bikes in Cluj-Napoca, no filters, newest first, grid).
onStateChange(state: MarketplaceSearchState) => voidEvery change: a filter, a chip, the sort, the view, the place.
filtersFilterDef[]The facets. Default: the sample set of the category (createSearchFilters): Price with a histogram, Condition, Location with a radius, Delivery available, Seller, Posted and Wheel size for bikes; Price, Rooms, Floor area, Location, Floor, Layout, Seller and Posted for apartments.
itemsMarketplaceSearchListing[]The local ads the default search runs over (ListingData plus cityId, facets and keywords). Default: 480 generated ads of the category (getSampleListings, l01 included).
search(state: MarketplaceSearchState, page: number) => (MarketplaceSearchResult | Promise<MarketplaceSearchResult>)Plug the server here: { items, total, promoted?, histogram?, counts? }. Page 0 replaces the results (skeletons while pending), later pages append. Default: searchListings over items during render, the next pages after 700ms.
pageSizedefault24numberHow many ads each page or Load more step adds, clamped to 6-96.
defaultViewdefault"grid""grid" | "list"The first view while the state names none.
viewPersistKeydefault"northwind-market-search-view"stringRemembers grid or list in localStorage while the state is uncontrolled; "" turns it off.
columnsListingGridColumnsCards per row at the grid's own width (container queries). Default { base: 2, md: 3, xl: 3 }: two on phones, three beside the filters.
showPromoteddefaulttruebooleanUp to three promoted ads above the results.
showRelateddefaulttruebooleanThe Related searches links under the results (from 1024px).
relatedSearchesstring[]The related searches. Default: the category's sample (E-bike, MTB 29...).
loadModedefault"auto""auto" | "button"auto: the next page loads as the end nears (the button stays as a fallback); button: Show 24 more.
filterModedefault"auto""auto" | "sidebar" | "sheet"auto: a sticky sidebar from 1024px and a sheet below; sidebar: always beside the results (above them on phones); sheet: the Filtre button and a sheet at every width.
filtersOpenbooleanThe filters sheet, controlled with onFiltersOpenChange.
defaultFiltersOpendefaultfalsebooleanOpens the filters sheet on mount (where a sheet is used: below 1024px, or filterMode sheet).
onFiltersOpenChange(open: boolean) => voidThe sheet opened or closed.
savedSearchbooleanThe bell's state, controlled. Uncontrolled it belongs to one search and resets when the search changes.
defaultSavedSearchdefaultfalsebooleanThe first search starts saved (the bell is on).
onSaveSearch(state: MarketplaceSearchState, saved: boolean) => (void | Promise<unknown>)The bell. A promise keeps it busy; resolved, a toast says Search saved; rejected, an error toast. Default: a simulated 600ms request.
onFavoriteChange(id: string, favorite: boolean) => (void | Promise<unknown>)A heart was toggled; it shows at once and a rejected promise rolls it back.
languagedefault"en""ro" | "en"The sample content's language, kept for compatibility: the default listings, filter names, category names and related searches are English in both. Copy comes from labels.
labelsPartial<MarketplaceSearchPageLabels>Every visible string, English by default (DEFAULT_MARKETPLACE_SEARCH_PAGE_LABELS; MARKETPLACE_SEARCH_PAGE_LABELS_EN is the same set under its older name), with nested sets for the grid, the cards, the filter panel and the breadcrumb.
localedefault"en-US"stringIntl locale for the count, prices and dates.
currencydefault"USD"stringThe default listings' prices (converted at fixed rates; USD, EUR or RON) and the price filter.
nowDate | numberThe clock for Today at 2:32 PM and Posted. Default: useNow() (the sample's Sep 24, 2026, 4:40 PM while hydrating); the demo passes the same.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The filters' checks, chips, sliders and apply button, the filter count badge, the empty state action and the hearts.
radiusdefault"md""sm" | "md" | "lg"The listing cards and the empty state: 12 / 16 / 24px.
loadingdefaultfalsebooleanThe first page: the count and the grid as skeletons of their real shape.
animateIndefaulttruebooleanThe title and the results rise in once when the page mounts on the client; new pages fade in (never over server HTML, never with reduced motion).
linkComponent(props: LinkComponentProps) => ReactNodeThe router link for the breadcrumb, the cards and the related searches. Default: the provider's (the shell's), then <a>.
classNamestringClasses for the root.
classNamesPartial<Record<"top" | "title" | "count" | "body" | "filters" | "results" | "related" | "empty", string>>Classes for the parts.
Parts
DEFAULT_MARKETPLACE_SEARCH_PAGE_LABELS
constantEvery English string of the results page: title, count, sort options, filters, empty state and paging. Spread it and override what you translate.
MARKETPLACE_SEARCH_PAGE_LABELS_EN
constantAn alias of DEFAULT_MARKETPLACE_SEARCH_PAGE_LABELS, kept so older imports keep working.
searchListings
functionApplies the query, location, filters and sort of a search state to a local list of listings. Use it for a client-side search or as the reference for your API.
createSearchFilters
functionBuilds the facets of one category in sidebar order, with prices in the given currency. The page uses it when you pass no filters.
getSearchCategory
functionReturns a sample category by id (label, parent categories, filter set and related searches) in English or Romanian; an unknown id falls back to the last entry.
generateListings
functionGenerates the listings of one category, newest first: 480 by default and always the same for a given seed, language and currency.
getSampleListings
functiongenerateListings memoized per category, language and currency; the page's default data.
DEFAULT_MARKETPLACE_SEARCH_STATE
constantThe starting search state of the demo: category, query, location, filters, sort and view.
MARKETPLACE_SEARCH_SAMPLE
constantThe demo content: the starting state, the filter sets for bikes, apartments and any other category, and the listing helpers. Replace it with your own data.
MARKETPLACE_SEARCH_NOW
constantThe fixed "now" of the sample (September 24, 2026, 4:40 PM) that relative dates are measured from, so server and client render the same.
USD_RATES
constantDollars per unit of each sample currency (USD, EUR, RON), used to convert the sample prices.
RON_PER_EUR
constantThe rounded RON per euro rate the sample uses to convert between its currencies.
mulberry32
functionA tiny seeded random number generator, so the generated listings are the same on the server and the client.
Examples
List view, two filters
Rows with the seller line; Price $100 to $300 and Ships are on as chips, remove one to widen the list.
No results
Filters that match nothing: the empty state clears them in one tap.
Filters in a sheet
filterMode sheet with the sheet open and brand accents; the button counts the draft before you apply it.
Apartments in Cluj-Napoca
Apartments for sale with its own facets (Rooms, Floor area, Floor, Layout) and euro prices.
Euro prices
en-GB formatting and the same listings priced in euro.
Load more fails
A custom search with a button: the second page fails once, the retry brings it in.
Loading
The count and the grid as skeletons of their real shape.
Every location, four columns
No place, cheapest first, the filters in a sheet at every width, info tone and large radius.
Dependencies
Files
- components/blocks/marketplace-search-page.tsx
- components/blocks/marketplace-search-page-data.ts
FAQ
Is the React Marketplace Search Page block free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Marketplace Search Page?
Run npx wingo-ui@latest add marketplace-search-page. It also adds the Wingo UI items it builds on (Breadcrumb, Button, Empty State, Filter panel, Listing card and 13 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/marketplace-search-page.
How does Marketplace Search Page behave on phones?
Active filters and the query show as chips under the header (sideways scroll on phones); × removes one, Clear all removes all. Sort opens a menu (a sheet on phones), the grid or list switch cross-fades the results, the bell saves the search with a toast and pressing it again stops following it.
See also
More React blocks
Articles using this component
- React Marketplace Template That Works Like an App
Build it: screens and apps15 min read