Catalog
Parts for any catalog: a root that searches, filters, sorts and pages its items, a toolbar, and cards in a grid, a list or rows.
import { Catalog } from "@butteryui/react";Demo
When to use it
Use it for
- Things someone browses and picks from: a shop's products, a gallery of templates, an app's add-ons
- A collection that reads well as cards or as rows, with a view toggle between them
- Cards that link to each item's page or open into their details in place
Not for
- Records someone works through and acts on, such as orders: use DataTable with a FilterBar
- A whole page of categories and featured items beside a filter column: use CatalogPage, which is built from these cards
- A handful of feature cards in a marketing page: use Bento
Examples
Card variants
The four card variants side by side: elevated, outline, soft and plain.
Speckled mug
Harbor mug
Moss mug
Clay mug
Integrations
An app's integrations as rows of one list, with a search and an Install button that becomes a chip.
Label printing
Bookkeeping
Loyalty cards
Wholesale portal
No results
A search that matches nothing, which shows the no-results state and a button that clears it.
No coffee matches
Try another word, or clear the search to see every coffee.
Product grid
A shop's product grid: search, a kind filter, a sort and a count over cards that open into their details.
Template gallery
A gallery of email templates whose filter the app keeps in state, a page at a time with Show more.
Harvest letter
New coffee
Cupping night
Gift guide
Showing 4 of 8
Variants and states
List
The same catalog as a list: each card a row, its media a tile at the start and its price and actions at the end once there's room.
Rows
Rows of one list, a hairline between them, for an app's add-ons. An elevated, outline or soft variant draws the list's surface; plain rows sit on the page.
Label printing
Bookkeeping
Loyalty cards
Label printing
Bookkeeping
Loyalty cards
Variants
elevated, a card that lifts on hover; outline, a 1px edge; soft, a bg-control fill; plain, no surface, its media carrying the shape.
Media placement
top, edge to edge above the words; start, a tile beside them; background, behind the card with the words on a plate over its lower edge.
Sizes
sm, md and lg: their padding, start tile and title grow together.
Show more
With pageSize, the catalog shows a page of items and a button for the next, with how many show of how many match.
Showing 3 of 6
No results
When the search and filters match nothing, a button clears them.
No gear matches
Try other words, or another kind of gear.
Empty
Before the catalog has any items, Catalog.Empty takes their place.
No gear yet
Kettles, grinders and brewers you add to the shop show here.
Props
Catalog
| Prop | Type | Default | Description |
|---|---|---|---|
itemsrequired | readonly Item[] | Every item in the catalog, in its own order. | |
searchBy | ((item: Item) => string | readonly string[]) | The words an item is found by, such as its name and what it is. Without it, a search narrows nothing. | |
filterBy | Readonly<Record<string, (item: Item) => string | readonly string[]>> | Each filter's value for an item, by the filter's name, such as { roast: (coffee) => coffee.roast }. An item
with a list of values matches a filter set to any of them. | |
sortBy | Readonly<Record<string, (a: Item, b: Item) => number>> | How each sort orders the items, by the sort's value. A sort with none here keeps the items' own order. | |
pageSize | number | Shows this many items at first, and as many more each time Catalog.More is pressed. Shows every item when left out. | |
query | string | What's searched for. Pair with onQueryChange to control it. | |
defaultQuery | string | "" | What's searched for at first when uncontrolled. Defaults to "". |
onQueryChange | ((query: string) => void) | Called with the search as it's typed, and with "" when it's cleared. | |
filters | CatalogFilters | The filters set, by name. Pair with onFiltersChange to control them. | |
defaultFilters | CatalogFilters | The filters set at first when uncontrolled. Defaults to none. | |
onFiltersChange | ((filters: CatalogFilters) => void) | Called with every filter set whenever one changes. | |
sort | string | The sort chosen, by its value. Pair with onSortChange to control it. | |
defaultSort | string | "" | The sort chosen at first when uncontrolled. Defaults to "", the items' own order, which Catalog.Sort shows as its first option. |
onSortChange | ((sort: string) => void) | Called with the sort chosen. | |
layout | "grid" | "list" | "rows" | How the items lay out. Pair with onLayoutChange to control it. | |
defaultLayout | "grid" | "list" | "rows" | "grid" | How the items lay out at first when uncontrolled. Defaults to "grid". |
onLayoutChange | ((layout: CatalogLayout) => void) | Called when Catalog.ViewToggle lays the items out another way. | |
childrenrequired | (shown: readonly Item[]) => ReactNode | The catalog's parts, given the items to show: those the search and filters match, in the sort's order,
up to the pages shown. Map them to Catalog.Cards inside Catalog.Items. |
It also takes Omit<ComponentPropsWithRef<"div">, "children">.
Catalog.Card
| Prop | Type | Default | Description |
|---|---|---|---|
titlerequired | string | The item's name, an h3 that names the card and, once it opens, its dialog. | |
href | string | The item's page. The title becomes a link to it, and a press anywhere else on the card follows it too. | |
onNavigate | ((href: string, event: MouseEvent<HTMLAnchorElement>) => void) | Runs as its link is followed, where a router can take over. A click that opens the page elsewhere, in a new tab or window, is left to the browser. | |
detail | ReactNode | What the card shows once it opens. With it, the title becomes a button, and pressing it or anywhere else on
the card turns the card into a dialog on spring.morph, its title travelling along, and back into the card on
spring.smooth. Leave it out of a card with an href. | |
open | boolean | Whether its detail is open. Pair with onOpenChange to control it. | |
defaultOpen | boolean | false | Whether its detail starts open when uncontrolled. Defaults to false. |
onOpenChange | ((open: boolean) => void) | Called when its detail opens or closes. | |
closeLabel | string | "Close" | Names the detail's close button. Defaults to "Close". |
media | ReactNode | Its picture: an image, a drawing or a tile, such as an InitialsTile. An img or svg covers its frame. | |
mediaPosition | "top" | "start" | "background" | "top" | Where its media sits in a grid: "top" (the default), "start" or "background". In a list or rows it's
always at the start. |
mediaRatio | number | 4 / 3 | The media's width ÷ height at the top, or the whole card's with the media behind it, such as 1 for a square. Defaults to 4 / 3 at the top and 3 / 4 behind. |
eyebrow | ReactNode | A short line over the title, such as where it's from or what kind it is, in caption. | |
badges | ReactNode | Small marks at the end of the eyebrow's line, such as a StatusBadge or TagChips. | |
meta | ReactNode | One line under the title, such as what it tastes of or who made it, in body-sm. | |
description | ReactNode | What it is, wrapping, in body-sm, or body at lg. | |
price | ReactNode | What it costs, at the start of its foot, in tabular figures. | |
actions | ReactNode | What can be done with it, at the end of its foot, such as an Add button. | |
variant | "elevated" | "outline" | "soft" | "plain" | "elevated" | How it draws its surface: "elevated" (the default), "outline", "soft" or "plain". A card that links
or opens lifts 2px on hover; a plain one lifts its media instead. In rows, cards draw no surface of their own. |
size | "sm" | "md" | "lg" | "md" | "sm": 12 of padding, a 40px start tile and a label title. "md" (the default): 16, 48px and label.
"lg": 20, 64px and title-md. |
children | ReactNode | More content under the description, such as tags. |
It also takes Omit<MotionElementProps<"article">, "title">.
Catalog.Count
| Prop | Type | Default | Description |
|---|---|---|---|
format | ((count: number, total: number) => string) | Says how many items match, given that count and the catalog's total. Defaults to "1 item" or "6 items". |
It also takes Omit<ComponentPropsWithRef<"p">, "children">.
Catalog.Empty
| Prop | Type | Default | Description |
|---|---|---|---|
titlerequired | ReactNode | What's missing, such as "No orders yet". | |
description | ReactNode | How to add it, 12px under the title, such as "Orders show up here once a customer checks out." | |
actions | ReactNode | The ways to add it, 24px under the text: a primary and a secondary pill. | |
icon | ComponentType<IconComponentProps> | An icon for what's missing, drawn 20px in a 48px tile 16px above the title, such as Receipt for orders. | |
docs | ReactNode | A link to read more, 16px under the text, such as <ArrowLink href="…">How orders work</. | |
titleAs | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "h2" | What the title renders as. Defaults to "h2". |
size | "sm" | "md" | "lg" | "md" | How much room it takes: "sm" for a panel or a table (a 40px tile, the line 8 under the title and the
actions 16 under the text), "md" (the default), or "lg" for a whole page (a 56px tile, a title-lg
heading over body text, and the actions 32 under it). |
variant | "plain" | "dashed" | "plain" | "plain" (the default), or "dashed": inside a 2px dashed edge with 16px corners, as a drop target is
drawn, padded 24, 32 or 48 by its size. |
It also takes Omit<ComponentPropsWithRef<"div">, "title">.
Catalog.Filter
| Prop | Type | Default | Description |
|---|---|---|---|
namerequired | string | The filter it sets, by its name in the catalog's filterBy. | |
labelrequired | string | What it filters by, such as "Roast". It names the control for screen readers. | |
optionsrequired | readonly (string | CatalogOption)[] | The values to filter to, as values that are also their labels or as { value, label }. | |
allLabel | string | "All" | The first choice, which clears the filter. Defaults to "All". |
variant | "segmented" | "select" | "segmented", the choices side by side in a SegmentedControl, or "select", a Select box. Defaults to
"segmented" for up to four choices, "All" among them, and "select" for more. |
It also takes Omit<ComponentPropsWithRef<"div">, "children">.
Catalog.Items
| Prop | Type | Default | Description |
|---|---|---|---|
layout | "grid" | "list" | "rows" | How the items lay out. Defaults to the catalog's layout, or "grid" outside a catalog. | |
columns | 1 | 2 | 3 | 4 | 5 | 6 | 3 | The most columns the grid shows, 16 apart. It shows fewer as its cards would get narrower than their size
allows (136px for sm, 160 for md, 248 for lg), down to one. Defaults to 3. |
children | ReactNode | The Catalog.Cards. |
It also takes ComponentPropsWithRef<"div">.
Catalog.More
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Show more" | What the button says. Defaults to "Show more". |
format | ((shown: number, matching: number) => string) | Says how many show of how many match, under the button. Defaults to "Showing 12 of 30". |
It also takes Omit<ComponentPropsWithRef<"div">, "children">.
Catalog.NoResults
| Prop | Type | Default | Description |
|---|---|---|---|
clearLabel | string | "Clear filters" | Names the primary button that clears the search and filters, which shows while either is set. Defaults to "Clear filters". |
titlerequired | ReactNode | What's missing, such as "No orders yet". | |
description | ReactNode | How to add it, 12px under the title, such as "Orders show up here once a customer checks out." | |
actions | ReactNode | The ways to add it, 24px under the text: a primary and a secondary pill. | |
icon | ComponentType<IconComponentProps> | An icon for what's missing, drawn 20px in a 48px tile 16px above the title, such as Receipt for orders. | |
docs | ReactNode | A link to read more, 16px under the text, such as <ArrowLink href="…">How orders work</. | |
titleAs | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "h2" | What the title renders as. Defaults to "h2". |
size | "sm" | "md" | "lg" | "md" | How much room it takes: "sm" for a panel or a table (a 40px tile, the line 8 under the title and the
actions 16 under the text), "md" (the default), or "lg" for a whole page (a 56px tile, a title-lg
heading over body text, and the actions 32 under it). |
variant | "plain" | "dashed" | "plain" | "plain" (the default), or "dashed": inside a 2px dashed edge with 16px corners, as a drop target is
drawn, padded 24, 32 or 48 by its size. |
It also takes Omit<ComponentPropsWithRef<"div">, "title">.
Catalog.Search
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Search" | Names the field for screen readers, and shows as its placeholder unless there's one. Defaults to "Search". |
variant | "outline" | "soft" | "outline" | "outline" (the default) with a 1px edge, as in a table's toolbar, or "soft" on bg-control, its edge
showing only on hover and while it has focus, as in a top bar. |
size | "sm" | "md" | "lg" | "md" | The field's height: "md" (34px, 30 compact, the default), "sm" (28px) for a dense toolbar, or "lg"
(48px, 40 compact, with 16px text) for a page's own search. |
shortcut | Shortcut | A key that focuses the field from anywhere outside a text field, written as useShortcut takes it, such as "/". It shows while the field is empty. | |
clearLabel | string | "Clear search" | Names the clear button. Defaults to "Clear search". |
It also takes Omit<ComponentPropsWithRef<"input">, "size" | "value" | "defaultValue" | "children" | "type">.
Catalog.Sort
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Sort by" | What it does, shown before it and naming it, such as "Sort by". Defaults to "Sort by". |
optionsrequired | readonly CatalogOption[] | The sorts, by their values in the catalog's sortBy. The first shows until another is chosen. |
It also takes Omit<ComponentPropsWithRef<"div">, "children">.
Catalog.Toolbar
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "Filters" | Names the toolbar's group for screen readers. Defaults to "Filters". |
children | ReactNode | Its controls: Catalog.Search and Catalog.Filters, which line up at its start, and Catalog.Count,
Catalog.Sort and Catalog.ViewToggle, which line up at its end, in the order given. |
It also takes ComponentPropsWithRef<"div">.
Catalog.ViewToggle
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "View" | Names the toggle for screen readers. Defaults to "View". |
labels | Partial<Readonly<Record<ViewMode, string>>> | The segments' names and tooltips, for other languages. Default to "Grid" and "List". | |
disabled | boolean | Keeps every segment from being chosen. |
It also takes Omit<ComponentPropsWithRef<"div">, "defaultValue" | "onChange" | "dir">.
CatalogOption
One choice in `Catalog.Filter` or `Catalog.Sort`.
| Field | Type | Default | Description |
|---|---|---|---|
valuerequired | string | What choosing it sets: the filter's value, or the sort's name in sortBy. | |
labelrequired | string | What the control shows. |
Also exported
| Export | Description |
|---|---|
useCatalog(): CatalogApi | The state of the Catalog around a part: what's searched for, the filters, the sort and the layout,
what it counts, and what changes them. Use it to wire a control of your own into a catalog. |
Accessibility
The toolbar is a group named by its label ("Filters"); its search, filters, sort and view toggle are named controls, and the count is a polite live region that says how many items match as they change. Each card is an article named by its title, an h3: a link to the item's page with href, or a button that opens its detail with detail, while a press anywhere else on the card does the same for the pointer. An open card is a modal dialog named by the title, focus inside it and back on the title's button when it closes. Clearing a search that matched nothing moves focus to the toolbar's first control, and Show more to the first item it showed.
| Keys | What they do |
|---|---|
| Tab | Moves through the toolbar's controls, then each card's title and actions |
| Enter | Follows a card's link, or opens its detail from its title |
| Space | Opens a card's detail from its title |
| Escape | Closes an open card, back into the card |
Motion
It changes shape as one surface, on `spring.smooth`, or `spring.morph` for a big change, rather than closing and reopening.
With reduced motion: An open card's dialog fades in and out in place in 100ms instead of growing out of the card, and cards don't lift on hover.
Layout
The root stacks its toolbar, items and states 24 apart. The toolbar puts the search and filters at its start and the count, sort and view at its end, which drops under the filters when they don't fit; under 640px of its own width the search and filters stack at its full width. The grid shows equal columns 16 apart, as many as fit up to columns, each at least 136, 160 or 248px wide for sm, md or lg cards; a list stacks cards 12 apart; rows sit in one list with a hairline between. In a grid a card's media keeps its ratio (4:3 at the top, 3:4 behind) or is a 40, 48 or 64px tile at the start, as it always is in a list or rows; from 560px of card, a card with its media at the start puts its price and actions at the end of its row. Titles and descriptions wrap, a title link or button is at least 24px tall, and a description stops at 60ch.
Related
See it in the Halfmoon shop demo.