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:

app/page.tsx
import { , , type  } from "@orbsearch/client";
import { , , ,  } from "@orbsearch/react";
import type {  } from "react";

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

export function ({  }: { :  }) {
    return (
        < ={} ="de">
            {}
        </>
    );
}

export function ({ ,  }: { : ; : string }) {
    if (()) return < ={(, { , : "de" })} />;
    if (.) return < ={.} ={[{ : .., :  }]} />;
    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 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).
  • 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:

PartWhat it is
SearchBoxThe box with suggestions, a GET form underneath
ListingToolbar, CategoryLinks, SectionLinksThe filters, count and sort; the categories one level on; the hits beside the products
ProductResults, ProductGrid, ProductTile, ProductRailResults with "Load more", the grid, the tile, a row that scrolls sideways
BannerA banner or teaser a rule places
FilterBar, FilterPanel, FilterSheetQuick filters above the results, a panel beside them, a modal sheet
FacetList, FacetFilter, GroupFilterEvery facet, one facet, one group of facets
AppliedFiltersA chip per applied filter, which takes it back
NoMatches, NoResultsFilters that leave nothing, a search that finds nothing, each with a next step
ProductGallery, BuyBox, ProductInfoA product's photos, what a shopper decides on, its details
SortSelect, Breadcrumb, Price, Rating, ProductImageThe 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:

app/tile.tsx
<. ={() => < {...} ="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.

Every ad is marked

A product a sponsored pin 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:

app/tile.tsx
<. ={<. ="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, 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 builds a display of your own.

Reference: custom_display · 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:

app/facets.tsx
import { , , , type  } from "@orbsearch/react";

export function ({  }: { :  }) {
    const  = ();

    return (
        < ={{ ...., : . }}>
            < ={.} ={.} ={.} />
        </>
    );
}

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 · more · 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:

app/storefront.tsx
import type {  } from "@orbsearch/client";
import { , , type  } from "@orbsearch/react";
import type {  } from "react";

const  = { ...("de"), : "Sofas, Teppiche, Leuchten" };
const :  = { : [320, 640, 1024], : (, ) => `${}?w=${}` };

export function ({ ,  }: { : ; :  }) {
    return (
        < ={} ="de" ={} ={}>
            {}
        </>
    );
}

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: tokens, slots, swapped parts and components of your own.
  • Recipes: one facet in a layout of your own, filters in a sheet on a phone.
  • Accessibility: what the components do for keyboard and screen reader users.

On this page