Product grid
A shop's catalog on real photographs, with filters, saves and a quick view, for a storefront or collection page.
Installation & usage
npx @youj/cli add commerce-product-gridAlso installs async-button, button, commerce-product-card, counter, dialog, empty, histogram-range, icon-alert-triangle, icon-arrow-right, icon-chevron-left, icon-chevron-right, icon-eye, icon-heart, icon-search, icon-shopping-bag, icon-sliders, icon-sort, icon-star, icon-x, popover, presence-list, segmented-control, select, skeleton, toggle, use-announcer, use-controllable-state, use-mobile, youj-core, youj-presets, youj-ui-core.
The CLI copies the block to blocks/commerce/product-grid.tsx with every component it uses, so it’s yours to edit.
Every prop is optional and falls back to the demo, so it renders as you see it until you pass your own. Give it your data, rewrite or translate its strings through copy, and connect its actions:
'use client'
import Link from 'next/link'
import { ProductGrid, type ProductGridProps } from '@/blocks/commerce/product-grid'
export function ProductGridView({ products, colors, sort, saved }: Pick<ProductGridProps, 'products' | 'colors' | 'sort' | 'saved'>) {
return (
<ProductGrid
products={products}
colors={colors}
sort={sort}
saved={saved}
copy={{ eyebrow: 'The shop' }}
linkComponent={Link}
onAdd={async (product, choice) => {
// await your API here; the block shows progress until it settles
}}
onCategoryChange={(category) => {
// your code
}}
onPriceChange={(price) => {
// your code
}}
/>
)
}Description
A shop's catalog on the editorial Product card: categories as tabs with live counts, a Saved view, a Filters popover (price over its histogram, colors as swatches) whose choices become chips you can remove, and a sort. The featured product takes a wide tile, cards that stop matching fade while the rest glide into place, and a long catalog shows a page at a time.
Look closer without leaving the grid: the photo flies out of its tile into the dialog while the details rise around it, ← and → step through the grid's order, and closing flies it home onto its tile. Pick a colorway, a size and add it to the bag, or save it for later.
Code
Product Grid, the Product grid story: the files youj add writes.
blocks/commerce
Reference
Props36 props, 1 types
Props
| Prop | Type | |
|---|---|---|
theme | ThemeInputDefault: the theme around it. The showcase passes the chosen preset here. | – |
embedded | booleanA section inside another page: the host lays it out (no page background, section padding or max width). default false | false |
products | Product[]The catalog, in its featured order. default the demo's five photographed products | the demo's five photographed products |
featured | string | nullThe product shown wide in the Featured order (it should come first); default 'bud-vases' | 'bud-vases' |
onAdd | (product: Product, choice: { color: string; size?: string }) => Promise<unknown> | unknownCalled to put a product in the bag, from a card's quick add or quick view; awaited. Default: a short demo wait. | – |
category | stringThe category shown ( | – |
defaultCategory | stringThe category shown first, uncontrolled. default '' | '' |
onCategoryChange | (category: string) => void | – |
price | [number, number]The price range | – |
defaultPrice | [number, number]The price range first set, uncontrolled. Default the whole range. | – |
onPriceChange | (price: [number, number]) => void | – |
priceMax | numberThe top of the price range. Default: the dearest product, rounded up to 10. | – |
colors | string[]The colorway names picked, controlled. | – |
defaultColors | string[]The colorway names picked first, uncontrolled. default [] | [] |
onColorsChange | (colors: string[]) => void | – |
sort | ProductSort'featured''price-asc''price-desc''rating'The order, controlled. | – |
defaultSort | ProductSort'featured''price-asc''price-desc''rating'The order first shown, uncontrolled. default 'featured' | 'featured' |
onSortChange | (sort: ProductSort) => void | – |
saved | string[]The saved products' ids, controlled. | – |
defaultSaved | string[]The products saved at first, uncontrolled. default [] | [] |
onSavedChange | (saved: string[]) => void | – |
quickView | string | nullThe product in quick view (its id; | – |
defaultQuickView | string | nullThe product in quick view at first, uncontrolled. default null | null |
onQuickViewChange | (id: string | null) => void | – |
pageSize | numberHow many products show before Load more. default 12 | 12 |
loading | booleanShows skeleton cards while the catalog loads. default false | false |
error | ReactNodeWhy the catalog didn't load: shown with a retry instead of the grid. | – |
onRetry | () => void | – |
empty | ReactNodeReplaces the "no matches" message. | – |
renderItem | (product: Product) => ReactNodeDraws each product instead of a Product card. | – |
currency | stringAn ISO 4217 code. Default: the format around it, else 'USD'. | – |
locale | stringA BCP 47 locale for prices and counts. Default: the app's (YoujProvider). | – |
linkComponent | ElementTypeRenders each product link, e.g. Next.js default 'a' | 'a' |
headingLevel | 1 | 2 | 3The heading's level. default 1 | 1 |
copy | Partial<ProductGridCopy>Any of the grid's strings, to rewrite or translate. | – |
cardCopy | Partial<ProductCardCopy>The cards' strings. | – |
Product
| Prop | Type | |
|---|---|---|
idRequired | string | – |
nameRequired | string | – |
image | stringA photo's URL: it pictures the product (a colorway's own | – |
angle | stringThe product from its other side; hover and keyboard focus turn to it. | – |
focus | readonly [number, number]Where the product sits in its photos, in percent from the top start: a crop of another shape keeps it in frame. default `[50, 50]` | `[50, 50]` |
href | stringThe product's page. default `#<id>` | `#<id>` |
categoryRequired | string | – |
priceRequired | number | – |
compareAt | number | – |
ratingRequired | number | – |
reviewsRequired | number | – |
badge | string | – |
colorsRequired | Colorway[]The colorways it's sold in; the first is the default. | – |
sizes | string[] | – |
soldOut | string[]Sizes out of stock right now. | – |
descriptionRequired | string | – |
Strings52 keys for copy
Strings
copy takes any of these keys; the rest keep their defaults. A key whose text holds a value (a count, a name) takes a function.
eyebrowtitledescriptioncategoriesallfiltersfiltersSetpriceminPricemaxPricecolorclearFiltersclearAllshowResultsactiveFiltersremoveFiltersortBysortssavedsavesavedNoticeunsavedNoticequickViewcountshowingloadMoreloadinglistemptyTitleemptyDescriptionsavedEmptyTitlesavedEmptyDescriptionshowAllerrorTitleretryclosepreviousnextpositionviewsfrontsidesizesoldOutpickSizeaddToBagaddingaddedaddErrordetailsreviewsratedBuilt from11 components, 1 blocks
Built from
Accessibilitylandmarks, headings, filters, dialog, live counts
Accessibility
The grid is a <section> named by its own heading (aria-labelledby); the heading is an h1 by default and headingLevel changes it, and every product is an article named by its link. The category tabs are a labeled group, the Filters popover's price is a two-thumb slider with named ends and its colors are checkboxes, and every chip is a button that says what it removes, handing focus to its neighbor (or the count) when it goes. The count of matching products is a polite status, and saving one is announced.
Each card's heart is a toggle button (aria-pressed) named for its product. Quick view is a modal dialog named by the product: focus moves to it and back to the card's quick view button when it closes, Escape closes it, ← and → step through the grid when focus isn't in a field, and its sizes are a radio group with the sold-out ones disabled. Under reduced motion nothing flies and the dialog simply appears.