Skip to content

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-grid

Also 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:

productGrid-view.tsx
'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
      }}
    />
  )
}

Reference

Props36 props, 1 types

Props

ProductGrid props
PropType
themeThemeInput

Default: the theme around it. The showcase passes the chosen preset here.

–
embeddedboolean

A section inside another page: the host lays it out (no page background, section padding or max width).

default false

false
productsProduct[]

The catalog, in its featured order.

default the demo's five photographed products

the demo's five photographed products
featuredstring | null

The product shown wide in the Featured order (it should come first); null keeps every tile the same.

default 'bud-vases'

'bud-vases'
onAdd(product: Product, choice: { color: string; size?: string }) => Promise<unknown> | unknown

Called to put a product in the bag, from a card's quick add or quick view; awaited. Default: a short demo wait.

–
categorystring

The category shown ('' is all), controlled.

–
defaultCategorystring

The category shown first, uncontrolled.

default ''

''
onCategoryChange(category: string) => void–
price[number, number]

The price range [low, high], controlled; the top of the range means "and up".

–
defaultPrice[number, number]

The price range first set, uncontrolled. Default the whole range.

–
onPriceChange(price: [number, number]) => void–
priceMaxnumber

The top of the price range. Default: the dearest product, rounded up to 10.

–
colorsstring[]

The colorway names picked, controlled.

–
defaultColorsstring[]

The colorway names picked first, uncontrolled.

default []

[]
onColorsChange(colors: string[]) => void–
sortProductSort
'featured''price-asc''price-desc''rating'

The order, controlled.

–
defaultSortProductSort
'featured''price-asc''price-desc''rating'

The order first shown, uncontrolled.

default 'featured'

'featured'
onSortChange(sort: ProductSort) => void–
savedstring[]

The saved products' ids, controlled.

–
defaultSavedstring[]

The products saved at first, uncontrolled.

default []

[]
onSavedChange(saved: string[]) => void–
quickViewstring | null

The product in quick view (its id; null is closed), controlled.

–
defaultQuickViewstring | null

The product in quick view at first, uncontrolled.

default null

null
onQuickViewChange(id: string | null) => void–
pageSizenumber

How many products show before Load more.

default 12

12
loadingboolean

Shows skeleton cards while the catalog loads.

default false

false
errorReactNode

Why the catalog didn't load: shown with a retry instead of the grid.

–
onRetry() => void–
emptyReactNode

Replaces the "no matches" message.

–
renderItem(product: Product) => ReactNode

Draws each product instead of a Product card.

–
currencystring

An ISO 4217 code. Default: the format around it, else 'USD'.

–
localestring

A BCP 47 locale for prices and counts. Default: the app's (YoujProvider).

–
linkComponentElementType

Renders each product link, e.g. Next.js Link.

default 'a'

'a'
headingLevel1 | 2 | 3

The heading's level.

default 1

1
copyPartial<ProductGridCopy>

Any of the grid's strings, to rewrite or translate.

–
cardCopyPartial<ProductCardCopy>

The cards' strings.

–

Product

Product props
PropType
idRequiredstring–
nameRequiredstring–
imagestring

A photo's URL: it pictures the product (a colorway's own image wins).

–
anglestring

The product from its other side; hover and keyboard focus turn to it.

–
focusreadonly [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]`
hrefstring

The product's page.

default `#<id>`

`#<id>`
categoryRequiredstring–
priceRequirednumber–
compareAtnumber–
ratingRequirednumber–
reviewsRequirednumber–
badgestring–
colorsRequiredColorway[]

The colorways it's sold in; the first is the default.

–
sizesstring[]–
soldOutstring[]

Sizes out of stock right now.

–
descriptionRequiredstring–
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.

eyebrowtitledescriptioncategoriesallfiltersfiltersSetpriceminPricemaxPricecolorclearFiltersclearAllshowResultsactiveFiltersremoveFiltersortBysortssavedsavesavedNoticeunsavedNoticequickViewcountshowingloadMoreloadinglistemptyTitleemptyDescriptionsavedEmptyTitlesavedEmptyDescriptionshowAllerrorTitleretryclosepreviousnextpositionviewsfrontsidesizesoldOutpickSizeaddToBagaddingaddedaddErrordetailsreviewsrated
Built from11 components, 1 blocks
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.