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.
A three-column product feed led by a row of category pills (with an "All" pill), alongside a search box and a sort dropdown in one row. Instead of pagination, a Load more button appends the next batch to the feed and a progress bar tracks how many of the total have loaded. After each batch lands, the feed scrolls to the first newly added card.
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/productlist5?email=YOUR_EMAIL&license_key=YOUR_KEY"Base UI flavor
npx shadcn add "https://ui.beste.co/r-base/productlist5?email=YOUR_EMAIL&license_key=YOUR_KEY"The installed file exports productlist5Demo alongside the block: the exact props behind the preview above. Spread it to get a working feed in one line.
import { Productlist5, productlist5Demo } from "@/components/beste/block/productlist5";
export default function Feed() {
return <Productlist5 {...productlist5Demo} />;
}Then replace the demo with your own props. Written out, the same setup looks like this:
import { Productlist5 } from "@/components/beste/block/productlist5";
export default function Feed() {
return (
<Productlist5
heading="The edit"
pageSize={9}
productsUrl="/api/products"
pillGroupId="category"
allPillLabel="All"
filterGroups={[
{ id: "q", label: "Search", type: "search", placeholder: "Search the edit" },
{
id: "category",
label: "Category",
type: "segmented",
options: [
{ label: "Shoes", value: "shoes" },
{ label: "Apparel", value: "apparel" },
],
},
]}
sortOptions={[{ label: "Featured", value: "featured" }]}
/>
);
}The group whose id equals pillGroupId renders as the pill row (a leading "All" pill clears it); the search group and sort render inline in the same top row. Other filter groups in filterGroups still drive matching but are not rendered as controls, so keep this block's filterGroups focused on search + the pill facet.
pageSize is the batch size, not a page window. "Load more" requests the next page and the engine appends the results to the feed (useProductFetch({ append: true })). Any filter or sort change resets to page 1 and replaces the list. The progress bar width is loaded / total, and the button hides once everything is loaded. In client mode the feed reveals page * pageSize items cumulatively; in remote/server mode it shows every batch fetched so far.
| 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 batch; skips the initial client fetch |
initialTotalCount | number | – | Total matches for initialProducts |
products | ProductItem[] | [] | Client mode dataset; ignored while productsUrl is set |
filterGroups | ProductFilterGroup[] | [] | Filter definitions; search renders inline, the pillGroupId group as pills |
pillGroupId | string | "category" | Id of the group shown as the pill row |
allPillLabel | string | "All" | Label of the leading pill that clears the pill group |
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. Restore from the URL |
pageSize | number | 9 | Items loaded per "Load more" batch |
loadMoreLabel | string | "Load more" | Label for the load-more button |
mode | "client" | "server" | "client" | See Data modes. productsUrl implies server behavior |
totalCount | number | products.length | Server mode: total matches across all batches |
isLoading | boolean | false | Server mode: disables the button while a batch loads |
onQueryChange | (query: ProductQuery) => void | – | Called on every filter, sort, and load-more action |
className | string | – | Extra classes for the outer <section> |
ProductItem{
title: string;
href: string;
brand?: string;
price: number;
compareAtPrice?: number;
image: { src: string; alt: string };
badge?: string;
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"] }.
Each entry in filterGroups is a ProductFilterGroup:
| Field | Type | Description |
|---|---|---|
id | string | Unique key; option-based groups match product.attributes[id] |
label | string | Group label |
type | ProductFilterType | One of the types below (default checkbox) |
options | ProductFilterOption[] | Required for option-based types; the pill group reads these |
min / max / step | number | Bounds for slider (and placeholders for range) |
unit | string | Prefix for numeric values, e.g. "$" |
placeholder | string | Search input placeholder |
dependsOn | string | Cascading: parent group id |
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 |
The pill group's type only affects how it would render as a standard control; here it is drawn as pills, and matching still uses attributes[pillGroupId].
dependsOn)A group with dependsOn: "<parentId>" stays disabled until its parent has a selection, and clears whenever the parent changes. Static: tag options with parentValue. Dynamic: leave options empty and pass loadOptions to the engine.
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 and sorting run locally, and the feed reveals a batch at a time.productsUrl) — the block fetches (and appends) each batch itself. The endpoint receives ?filters=<json>&sort=&page=&pageSize= and returns { products, totalCount }. Pair with initialProducts for an SSR first batch.mode: "server") — you own the fetching; every action calls onQueryChange. Accumulate batches yourself and pass the full loaded list as products with totalCount.ProductQuery{
filters: Record<string, string[]>; // selections per group id
sort: string; // current sort value
page: number; // 1-based batch number
pageSize: number;
}Filter value shapes inside filters: option groups → arrays of option values; slider/range → a single "min-max" string (open ends allowed, "50-" = 50 and up); search → a single string.
scroll-mt keeps a small offset).featured, price-asc, price-desc, rating; other values keep input order.<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.
productlist3
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.