# React components

The search box, listing and product pages for React 19, built on the client, with every part yours to restyle or swap.

`@orbsearch/react` draws a storefront in React 19.3 or a later React 19: the search box, a page for every listing, a product page and the parts they are made of. It brings the behavior, the semantics and a plain look, and reads every answer through [the client](/docs/client):

```tsx twoslash title="app/page.tsx"
import { createSearch, isListing, type PageResponse } from "@orbsearch/client";
import { ListingPage, ProductPage, StorefrontProvider, listingView } from "@orbsearch/react";
import type { ReactNode } from "react";

const search = createSearch({ endpoint: "http://127.0.0.1:7800" });

export function Storefront({ children }: { children: ReactNode }) {
    return (
        <StorefrontProvider search={search} locale="de">
            {children}
        </StorefrontProvider>
    );
}

export function Page({ page, path }: { page: PageResponse; path: string }) {
    if (isListing(page)) return <ListingPage listing={listingView(page, { path, locale: "de" })} />;
    if (page.product) return <ProductPage product={page.product} trail={[{ title: page.product.title, href: path }]} />;
    return null;
}
```

`StorefrontProvider` holds the client, the language and your router's `navigate`; without one, a link loads the next page. `page` is what `resolve` answered, and `listingView` reads a listing out of it. [Your storefront](/docs/your-storefront) is the whole shop in React Router.

## The two pages

`ListingPage` draws a search, category or brand page from `listing`, the view `listingView` reads out of `resolve`'s answer. It takes a few props beside it:

- `filters`: `"bar"`, the default, for quick filters above the results and all of them in a sheet, or `"sidebar"` for a panel beside the results.
- `tile` and `banner`: your own, built from the parts ([Customize](/docs/customize#swap-a-part)).
- `sorts`: the sort codes to offer instead of the answer's, the first where the page starts.
- `sectionTitle`, `departments` and `tip`: the heading of a section beside the products, where an empty search sends the shopper, and your advice when nothing is found.
- `busy`: your router says the next page is on its way, as `useNavigation` does.

What only your shop knows is empty in the view and yours to set, as `{ ...view, trail, categories, popular }`: your home in the trail, your category navigation and the products an empty search offers.

`ProductPage` takes the `product` and its `trail`, the categories from the top down with the product last. `variant` is the variant the URL names, read with `variantAt(product, url)?.id`; `more` adds a rail of other products, and `buyBox` replaces the part a shopper decides on.

## The parts

Each block of the pages is a part with one job, exported on its own for a layout of your own:

| Part                                                          | What it is                                                                             |
| ------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| `SearchBox`                                                   | The box with suggestions, a GET form underneath                                        |
| `ListingToolbar`, `CategoryLinks`, `SectionLinks`             | The filters, count and sort; the categories one level on; the hits beside the products |
| `ProductResults`, `ProductGrid`, `ProductTile`, `ProductRail` | Results with "Load more", the grid, the tile, a row that scrolls sideways              |
| `Banner`                                                      | A banner or teaser a rule places                                                       |
| `FilterBar`, `FilterPanel`, `FilterSheet`                     | Quick filters above the results, a panel beside them, a modal sheet                    |
| `FacetList`, `FacetFilter`, `GroupFilter`                     | Every facet, one facet, one group of facets                                            |
| `AppliedFilters`                                              | A chip per applied filter, which takes it back                                         |
| `NoMatches`, `NoResults`                                      | Filters that leave nothing, a search that finds nothing, each with a next step         |
| `ProductGallery`, `BuyBox`, `ProductInfo`                     | A product's photos, what a shopper decides on, its details                             |
| `SortSelect`, `Breadcrumb`, `Price`, `Rating`, `ProductImage` | The small parts                                                                        |

Hooks sit below them: `useListing` holds a listing's state, `useFacet` one facet's values and semantics, `useFilters` a pending choice with its count, and `useLiveFilters` a choice that applies after a short pause.

## Change an element

To change a part's element or classes and keep its semantics, use Base UI's `render` prop:

```tsx twoslash title="app/tile.tsx"
import { ProductTile } from "@orbsearch/react";
// ---cut---
<ProductTile.Price render={(props) => <p {...props} className="shop-price" />} />;
```

`Root`, `SponsoredLabel`, `Media`, `Badges`, `Badge`, `Brand`, `Price` and `Facts` take `render`; `Title`, `Image` and `Rating` take a `className`. The search box takes `groups` to reorder or add groups of suggestions, and `suggestion` for a row of your own built on `SuggestionItem`. To change what a part holds, [swap it](/docs/customize#swap-a-part).

## Every ad is marked

A product a [sponsored pin](/docs/sponsored-products) placed starts its name with "Anzeige" (ad) or "Sponsored", `messages.sponsored`, so the tile marks the ad where its name is seen and read. Restyle the label through the title:

```tsx twoslash title="app/tile.tsx"
import { ProductTile } from "@orbsearch/react";
// ---cut---
<ProductTile.Title label={<ProductTile.SponsoredLabel className="shop-ad" />} />;
```

To show it elsewhere in the tile, pass `label={null}` and place `<ProductTile.SponsoredLabel />` yourself, since the law wants every ad marked. A suggestion row of your own renders `<SuggestionSponsoredLabel suggestion={suggestion} />` before the name.

## Facet displays

One registry of displays draws every [facet](/docs/reference/glossary#facet), keyed by name, in the bar, the panel, the sheet and a layout of your own. Ours are exported as `facetDisplays`: `list`, `stars`, `images`, `swatch`, `grades`, `buttons`, `select`, `range`, `slider`, `histogram`, `hierarchy` and `toggle`.

A facet gets the display its release names in `custom_display`, else the one `display` names, else its kind's own. The shop's displays, passed to `StorefrontProvider` as `displays`, come before ours, so a storefront without yours still draws the facet as `display` says. A group of facets draws from `groupDisplays`, where ours is `finder`. [Customize](/docs/customize#build-a-component-of-your-own) builds a display of your own.

**Reference:** [`custom_display`](/docs/reference/release-files/categories#nodes.facets.custom_display) · [`display`](/docs/reference/release-files/categories#nodes.facets.display)

## Facets that load when opened

The Query API counts a page's first 30 facets and lists the rest as `deferred`, and cuts a long list to 50 values with `more`, since every counted facet costs the engine on each request. `ListingPage` wraps its parts in `LateFacets`, so the rest arrive when a shopper wants them:

```tsx twoslash title="app/facets.tsx"
import { FacetList, LateFacets, useListing, type ListingView } from "@orbsearch/react";

export function Facets({ listing }: { listing: ListingView }) {
    const page = useListing(listing);

    return (
        <LateFacets request={{ ...page.count, filters: page.filters }}>
            <FacetList facets={listing.facets} value={page.value} onChange={page.apply} />
        </LateFacets>
    );
}
```

A facet the shopper opens, or whose "Show more" they press, asks for its values with `search.facets`, under the choices the page shows, and again when they change. Meanwhile it says "Loading values …", or that they could not be loaded. A facet in a popover of your own calls `useLoadWhenOpen`. Without `LateFacets`, facets draw as given.

**Reference:** [`deferred`](/docs/reference/query-api#search.answer.sections.facets.deferred) · [`more`](/docs/reference/query-api#search.answer.sections.facets.more) · [`counted_facets`](/docs/reference/limits#counted_facets)

## Words and photos

The components speak German for `de` and its regions and English for every other locale. Pass your own `messages` to change a word or add a language, and `images` to load photos through your image host's widths:

```tsx twoslash title="app/storefront.tsx"
import type { Search } from "@orbsearch/client";
import { messagesFor, StorefrontProvider, type ImageHost } from "@orbsearch/react";
import type { ReactNode } from "react";

const messages = { ...messagesFor("de"), searchPlaceholder: "Sofas, Teppiche, Leuchten" };
const images: ImageHost = { widths: [320, 640, 1024], url: (src, width) => `${src}?w=${width}` };

export function Storefront({ search, children }: { search: Search; children: ReactNode }) {
    return (
        <StorefrontProvider search={search} locale="de" messages={messages} images={images}>
            {children}
        </StorefrontProvider>
    );
}
```

The placeholder lists sofas, rugs and lights. Without `images`, photos load at the size the catalog's URL gives, and `nouns` names in your words the types a listing counts.

## Next

- [Customize](/docs/customize): tokens, slots, swapped parts and components of your own.
- [Recipes](/docs/recipes): one facet in a layout of your own, filters in a sheet on a phone.
- [Accessibility](/docs/accessibility): what the components do for keyboard and screen reader users.
