Skip to content
buttery

v1.0.0

Source on GitHub

Version 1.0.0

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

6 items

Sort by
Brewers
New

Ceramic, for a #2 filter
$28
Kettles

A steady pour, 1 liter
$64
Grinders

40 settings, steel burrs
$89
Brewers

600 ml, double walled
$32
Tools
Back in stock

To 0.1 g, with a timer
$45
Tools

100, unbleached
$8

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.

elevated

Speckled mug

Oatmeal glaze, 300 ml
$26
outline

Harbor mug

Deep blue glaze, 300 ml
$26
soft

Moss mug

Green celadon, 250 ml
$26
plain

Clay mug

Bare red clay, 250 ml
$26

Integrations

An app's integrations as rows of one list, with a search and an Install button that becomes a chip.

4 integrations

Label printing

Print shipping labels at the counter, straight from an order.
Installed

Bookkeeping

Sales, refunds and payouts in your books every night.

Loyalty cards

A free bag after every tenth, stamped at the till or online.

Wholesale portal

Cafés order their standing roasts and pay their invoices.

No results

A search that matches nothing, which shows the no-results state and a button that clears it.

0 coffees

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.

6 teas

Sort by
Green tea
This season

Grassy, sweet, a little sea air
$18
Black tea

Malty and bold, made for milk
$14
Oolong tea

Orchid and honey, a long finish
$24
Herbal tea

Apple and hay, no caffeine
$12
Black tea

Citrus peel over a brisk black tea
$15
Herbal tea

Cool and bright, hot or iced
$11

A gallery of email templates whose filter the app keeps in state, a page at a time with Show more.

8 templates

Harvest letter

A long read for a new arrival

New coffee

One coffee, one photo, one button

Cupping night

When, where and a reply button

Gift guide

Six picks for the holidays

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.

6 items

Sort by
Brewers
New

Ceramic, for a #2 filter
$28
Kettles

A steady pour, 1 liter
$64
Grinders

40 settings, steel burrs
$89
Brewers

600 ml, double walled
$32
Tools
Back in stock

To 0.1 g, with a timer
$45
Tools

100, unbleached
$8

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

Print shipping labels at the counter, straight from an order.
Installed

Bookkeeping

Sales, refunds and payouts in your books every night.

Loyalty cards

A free bag after every tenth, stamped at the till or online.

Label printing

Print shipping labels at the counter, straight from an order.
Installed

Bookkeeping

Sales, refunds and payouts in your books every night.

Loyalty cards

A free bag after every tenth, stamped at the till or online.

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.

Brewers
New

Ceramic, for a #2 filter
$28
Kettles

A steady pour, 1 liter
$64
Grinders

40 settings, steel burrs
$89
Brewers

600 ml, double walled
$32

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.

Brewers
New

Ceramic, for a #2 filter
$28
Kettles

A steady pour, 1 liter
$64
Grinders

40 settings, steel burrs
$89

Sizes

sm, md and lg: their padding, start tile and title grow together.

Brewers

600 ml, double walled
$32
Brewers

600 ml, double walled
$32
Brewers

600 ml, double walled
$32

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.

6 items

Sort by
Brewers
New

Ceramic, for a #2 filter
$28
Kettles

A steady pour, 1 liter
$64
Grinders

40 settings, steel burrs
$89

Showing 3 of 6

No results

When the search and filters match nothing, a button clears them.

0 items

Sort by

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

PropTypeDefaultDescription
items

required

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.
filterByReadonly<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.
sortByReadonly<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.
pageSizenumberShows this many items at first, and as many more each time Catalog.More is pressed. Shows every item when left out.
querystringWhat's searched for. Pair with onQueryChange to control it.
defaultQuerystring""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.
filtersCatalogFiltersThe filters set, by name. Pair with onFiltersChange to control them.
defaultFiltersCatalogFiltersThe filters set at first when uncontrolled. Defaults to none.
onFiltersChange((filters: CatalogFilters) => void)Called with every filter set whenever one changes.
sortstringThe sort chosen, by its value. Pair with onSortChange to control it.
defaultSortstring""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.
children

required

(shown: readonly Item[]) => ReactNodeThe 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

PropTypeDefaultDescription
title

required

stringThe item's name, an h3 that names the card and, once it opens, its dialog.
hrefstringThe 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.
detailReactNodeWhat 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.
openbooleanWhether its detail is open. Pair with onOpenChange to control it.
defaultOpenbooleanfalseWhether its detail starts open when uncontrolled. Defaults to false.
onOpenChange((open: boolean) => void)Called when its detail opens or closes.
closeLabelstring"Close"Names the detail's close button. Defaults to "Close".
mediaReactNodeIts 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.
mediaRationumber4 / 3The 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.
eyebrowReactNodeA short line over the title, such as where it's from or what kind it is, in caption.
badgesReactNodeSmall marks at the end of the eyebrow's line, such as a StatusBadge or TagChips.
metaReactNodeOne line under the title, such as what it tastes of or who made it, in body-sm.
descriptionReactNodeWhat it is, wrapping, in body-sm, or body at lg.
priceReactNodeWhat it costs, at the start of its foot, in tabular figures.
actionsReactNodeWhat 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.
childrenReactNodeMore content under the description, such as tags.

It also takes Omit<MotionElementProps<"article">, "title">.

Catalog.Count

PropTypeDefaultDescription
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

PropTypeDefaultDescription
title

required

ReactNodeWhat's missing, such as "No orders yet".
descriptionReactNodeHow to add it, 12px under the title, such as "Orders show up here once a customer checks out."
actionsReactNodeThe ways to add it, 24px under the text: a primary and a secondary pill.
iconComponentType<IconComponentProps>An icon for what's missing, drawn 20px in a 48px tile 16px above the title, such as Receipt for orders.
docsReactNodeA link to read more, 16px under the text, such as <ArrowLink href="…">How orders work</ArrowLink>.
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

PropTypeDefaultDescription
name

required

stringThe filter it sets, by its name in the catalog's filterBy.
label

required

stringWhat it filters by, such as "Roast". It names the control for screen readers.
options

required

readonly (string | CatalogOption)[]The values to filter to, as values that are also their labels or as { value, label }.
allLabelstring"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

PropTypeDefaultDescription
layout"grid" | "list" | "rows"How the items lay out. Defaults to the catalog's layout, or "grid" outside a catalog.
columns1 | 2 | 3 | 4 | 5 | 63The 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.
childrenReactNodeThe Catalog.Cards.

It also takes ComponentPropsWithRef<"div">.

Catalog.More

PropTypeDefaultDescription
labelstring"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

PropTypeDefaultDescription
clearLabelstring"Clear filters"Names the primary button that clears the search and filters, which shows while either is set. Defaults to "Clear filters".
title

required

ReactNodeWhat's missing, such as "No orders yet".
descriptionReactNodeHow to add it, 12px under the title, such as "Orders show up here once a customer checks out."
actionsReactNodeThe ways to add it, 24px under the text: a primary and a secondary pill.
iconComponentType<IconComponentProps>An icon for what's missing, drawn 20px in a 48px tile 16px above the title, such as Receipt for orders.
docsReactNodeA link to read more, 16px under the text, such as <ArrowLink href="…">How orders work</ArrowLink>.
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

PropTypeDefaultDescription
labelstring"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.
shortcutShortcutA 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.
clearLabelstring"Clear search"Names the clear button. Defaults to "Clear search".

It also takes Omit<ComponentPropsWithRef<"input">, "size" | "value" | "defaultValue" | "children" | "type">.

Catalog.Sort

PropTypeDefaultDescription
labelstring"Sort by"What it does, shown before it and naming it, such as "Sort by". Defaults to "Sort by".
options

required

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

PropTypeDefaultDescription
labelstring"Filters"Names the toolbar's group for screen readers. Defaults to "Filters".
childrenReactNodeIts 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

PropTypeDefaultDescription
labelstring"View"Names the toggle for screen readers. Defaults to "View".
labelsPartial<Readonly<Record<ViewMode, string>>>The segments' names and tooltips, for other languages. Default to "Grid" and "List".
disabledbooleanKeeps every segment from being chosen.

It also takes Omit<ComponentPropsWithRef<"div">, "defaultValue" | "onChange" | "dir">.

CatalogOption

One choice in `Catalog.Filter` or `Catalog.Sort`.

FieldTypeDefaultDescription
value

required

stringWhat choosing it sets: the filter's value, or the sort's name in sortBy.
label

required

stringWhat the control shows.

Also exported

ExportDescription
useCatalog(): CatalogApiThe 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.

KeysWhat they do
TabMoves through the toolbar's controls, then each card's title and actions
EnterFollows a card's link, or opens its detail from its title
SpaceOpens a card's detail from its title
EscapeCloses 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.

See it in the Halfmoon shop demo.