Dense product grid with an always-visible inline filter toolbar: search, a segmented category, a cascading subcategory select, and a searchable brand combobox, powered by the shared product-filters engine. Self-fetches from your API via productsUrl with an SSR-friendly first page, runs client-side on a dataset, or emits queries in controlled server mode.
A dense product grid with an always-visible inline filter toolbar at the top (no sidebar, no drawer). The demo toolbar holds a search box, a segmented category, a cascading subcategory select, and a searchable multi-brand combobox; below it a result count with removable active-filter chips and a sort dropdown, then a compact grid (up to 5 columns) with numbered pagination.
Filtering is powered by the shared product-filters engine, which installs automatically with the block.
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://ui.beste.co/r/productlist3?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://ui.beste.co/r-base/productlist3?email=YOUR_EMAIL&license_key=YOUR_KEY"This installs the block, the engine (components/beste/component/product-filters.tsx), the individual filter controls, and the shadcn/ui dependencies they need.
The installed file exports productlist3Demo alongside the block: the exact props behind the preview above. Spread it to get a working grid in one line.
import { Productlist3, productlist3Demo } from "@/components/beste/block/productlist3";
export default function Marketplace() {
return <Productlist3 {...productlist3Demo} />;
}Then replace the demo with your own props. The demo self-fetches from a hosted sample API; in your app, point productsUrl at your own endpoint (see Data modes) or pass a local products array.
import { Productlist3 } from "@/components/beste/block/productlist3";
export default function Marketplace() {
return (
<Productlist3
heading="Marketplace"
pageSize={15}
productsUrl="/api/products"
sortOptions={[
{ label: "Featured", value: "featured" },
{ label: "Price: Low to High", value: "price-asc" },
]}
filterGroups={[
{ id: "q", label: "Search", type: "search", placeholder: "Search the catalog" },
{
id: "category",
label: "Category",
type: "segmented",
options: [
{ label: "Shoes", value: "shoes" },
{ label: "Apparel", value: "apparel" },
],
},
{
id: "brand",
label: "Brand",
type: "combobox",
multiple: true,
options: [{ label: "Acme", value: "acme" }],
},
]}
/>
);
}The toolbar renders the search group stretched on the left; every other group sits inline and wraps. segmented keeps its natural width, other controls get a fixed width.
| Prop | Type | Default | Description |
|---|---|---|---|
heading | string | – | Section heading |
description | string | – | Section intro text |
productsUrl | string | – | Endpoint the block fetches itself on every query (Data modes); products is ignored while set |
initialProducts | ProductItem[] | – | SSR-fetched first page; skips the initial client fetch |
initialTotalCount | number | – | Total matches for initialProducts |
products | ProductItem[] | [] | Client mode dataset; ignored while productsUrl is set |
filterGroups | ProductFilterGroup[] | [] | Toolbar filter definitions, rendered in order |
sortOptions | ProductSortOption[] | [] | Sort dropdown entries ({ label, value }). Omit to hide the dropdown |
defaultSort | string | first sort option | Initially selected sort value |
defaultFilters | Record<string, string[]> | {} | Initial selections keyed by group id, e.g. { category: ["shoes"] }. Restore from the URL |
pageSize | number | 15 | Products per page |
mode | "client" | "server" | "client" | See Data modes. productsUrl implies server behavior |
totalCount | number | products.length | Server mode: total matches across all pages, drives pagination |
isLoading | boolean | false | Server mode: shows the skeleton grid while fetching |
onQueryChange | (query: ProductQuery) => void | – | Called on every filter, sort, and page action |
className | string | – | Extra classes for the outer <section> |
ProductItem{
title: string;
href: string;
brand?: string;
price: number;
compareAtPrice?: number; // struck-through original price
image: { src: string; alt: string };
badge?: string; // corner badge, e.g. "Sale"
rating?: number;
reviewCount?: number;
keywords?: string[]; // extra terms matched by "search"
attributes?: Record<string, string[]>; // filterable values keyed by group id
}attributes keys must match your filterGroups ids, e.g. { category: ["sneakers"], color: ["black"] }. A product matches an option group when any of its attribute values is selected.
Each entry in filterGroups is a ProductFilterGroup:
| Field | Type | Description |
|---|---|---|
id | string | Unique key; option-based groups match product.attributes[id] |
label | string | Control label |
type | ProductFilterType | One of the types below (default checkbox) |
options | ProductFilterOption[] | Required for option-based types |
min / max / step | number | Bounds for slider (and placeholders for range) |
unit | string | Prefix for numeric values, e.g. "$" |
placeholder | string | Search input / dropdown trigger placeholder |
searchPlaceholder | string | Search box inside a combobox |
clearLabel | string | select: label of the first item that clears the selection |
multiple | boolean | combobox: allow multiple selections |
dependsOn | string | Cascading: parent group id (see below) |
type ProductFilterOption = {
label: string;
value: string;
count?: number;
swatch?: string;
parentValue?: string;
children?: ProductFilterOption[];
};type | Control | Selection | Matched against |
|---|---|---|---|
checkbox | checkbox list | multi | attributes[id] |
radio | radio list | single (click active to clear) | attributes[id] |
chips | button chips | multi | attributes[id] |
segmented | segmented tabs | single (click active to clear) | attributes[id] |
swatch | color circles | multi | attributes[id] |
toggle | switch rows | multi | attributes[id] |
select | dropdown | single | attributes[id] |
multiselect | checkbox dropdown | multi | attributes[id] |
combobox | searchable dropdown | single or multi (multiple) | attributes[id] |
tree | nested checkboxes | multi (parent covers subtree) | attributes[id] |
rating | star rows ("n & up") | single | product.rating (minimum) |
price | checkbox buckets | multi | product.price (option values are "min-max") |
slider | range slider | single range | product.price; needs min/max/step |
range | min/max number inputs | single range, open-ended | product.price |
search | text input | text | product.title + product.keywords, every word must match |
dependsOn)A group with dependsOn: "<parentId>" stays disabled until its parent has a selection, and its own selection clears whenever the parent changes.
parentValue; the engine filters options by the parent selection.options empty and pass loadOptions to the engine to fetch them per parent selection (results are cached).{ id: "category", label: "Category", type: "select",
options: [{ label: "Shoes", value: "shoes" }] },
{ id: "subcategory", label: "Subcategory", type: "select", dependsOn: "category",
options: [{ label: "Sneakers", value: "sneakers", parentValue: "shoes" }] },Every control this block renders is an individual Beste UI filter component from the Filter category — browse, preview, and install any of them on their own there:
filter-checkbox, filter-radio, filter-chips, filter-segmented, filter-swatch, filter-toggle, filter-select, filter-multiselect, filter-combobox, filter-tree, filter-rating, filter-slider, filter-range, filter-search.
They are wired together by the hidden product-filters engine that ships with this block, so a single install pulls in every control plus the state, debounce, cascading, and query logic. To build your own layout, use the engine directly: useProductFilters + ProductFilterField (renders the right control per group) + ProductFilterChips, with matchesProductFilters and sortProducts backing client mode.
Supply products in one of three ways:
products array; filtering, sorting, and pagination run locally.productsUrl) — the block fetches each page itself. The endpoint receives ?filters=<json>&sort=&page=&pageSize= and must return { products, totalCount }. Pair with initialProducts/initialTotalCount for an SSR-rendered first page (fetch page 1 in a server component, pass it down, and only later interactions hit the network from the client).mode: "server") — you own the fetching (custom caching, URL sync). Every action calls onQueryChange; you fetch and feed products + totalCount back through props.ProductQueryWhat onQueryChange and the productsUrl request encode:
{
filters: Record<string, string[]>; // selections per group id
sort: string; // current sort value (passed through verbatim in server mode)
page: number; // 1-based
pageSize: number;
}Filter value shapes inside filters:
checkbox, radio, chips, segmented, swatch, toggle, select, multiselect, combobox, tree, rating, price buckets): arrays of option values, e.g. { category: ["sneakers", "hoodies"] }.slider / range: a single "min-max" string, e.g. { price: ["25-150"] }; either side may be empty for an open range ("50-" = 50 and up).search: a single string, e.g. { q: ["sneaker"] }.dependsOn it.featured (input order), price-asc, price-desc, rating; any other value keeps input order (use custom values freely in server mode).<img>, so no next.config images.remotePatterns setup is needed. next/link handles product links.Wiring the form up
This block ships the form markup only; state, validation, and submit are yours to add. Our guide wires the shadcn Field primitives to React Hook Form, TanStack Form, and Formisch on one field system.
productlist1
Product listing with sidebar filters, sorting, pagination, and active filter chips, powered by the shared product-filters engine. Fifteen filter types including checkboxes, chips, color swatches, price sliders, rating, search, dropdowns, and cascading selects. Runs fully client-side on a given dataset, self-fetches from your API via productsUrl with an SSR-friendly first page, or emits queries in controlled server mode.
productlist2
Product catalog with a single Filters button that opens a side drawer: keyword search, cascading category and subcategory selects, a searchable multi-brand combobox, a size multiselect, and a segmented gender control, powered by the shared product-filters engine. Runs fully client-side on a given dataset, self-fetches from your API via productsUrl with an SSR-friendly first page, or emits queries in controlled server mode.
productlist4
Product grid with a left filter sidebar where hovering a card reveals a quick-shop tray with sizes and an add-to-cart button, plus a category tag and sale badge, powered by the shared product-filters engine. Self-fetches from your API via productsUrl with an SSR-friendly first page, runs client-side on a dataset, or emits queries in controlled server mode.
productlist5
Three-column product feed led by a row of category pills, with a search and sort, and a Load more button plus progress bar that appends the next batch instead of paginating, powered by the shared product-filters engine. Self-fetches from your API via productsUrl with an SSR-friendly first page, runs client-side on a dataset, or emits queries in controlled server mode.