# Recipes

Complete storefront code for two common layouts, one facet of your own and filters in a sheet on a phone.

Each recipe is complete code for the home store, built on [Your storefront](/docs/your-storefront) and the [React components](/docs/react).

## One facet in a layout of your own

`FacetFilter` draws one [facet](/docs/reference/glossary#facet) of an answer and hands the shopper's choice back to you. Here it draws the sofa page's colors; pick one to see the filters a storefront would send:

*Preview: the real `<FacetFilter>` of @orbsearch/react, drawn from the answer recorded on this page.*

```tsx twoslash title="app/color-filter.tsx"
import type { FacetResult } from "@orbsearch/client";
import { FacetFilter, withField, type Filters } from "@orbsearch/react";

type Props = {
    facets: readonly FacetResult[];
    filters: Filters;
    apply: (filters: Filters) => void;
};

export function ColorFilter({ facets, filters, apply }: Props) {
    const colors = facets.find((facet) => facet.field === "color");
    if (colors === undefined) return null;

    return (
        <FacetFilter
            facet={colors}
            value={filters.color}
            onChange={(color) => apply(withField(filters, "color", color))}
        />
    );
}
```

The facet comes from the listed section's `facets`, as the sofa page answers it:

```sh title="Terminal"
curl -s 'http://127.0.0.1:7800/v1/search?on=category_page&category=wohnen/sofas' \
  | jq '.sections[] | select(.type == "product") | .facets[] | select(.field == "color")'
```

```json title="Answer: 200 · 14 ms · release b25a6aaa"
{
  "field": "color",
  "kind": "swatch",
  "label": "Farbe",
  "values": [
    {
      "value": "anthracite",
      "label": "Anthrazit",
      "count": 2,
      "swatch": "#3B3F42"
    },
    {
      "value": "beige",
      "label": "Beige",
      "count": 2,
      "swatch": "#D9C4A3"
    },
    {
      "value": "grey",
      "label": "Grau",
      "count": 2,
      "swatch": "#9B9B9B"
    },
    {
      "value": "green",
      "label": "Grün",
      "count": 2,
      "swatch": "#5F7A4A"
    }
  ],
  "custom_display": "color_tiles"
}
```

`withField` sets one field of the filters and drops it when the shopper clears it. The color facet names its own display in `custom_display`, which a storefront draws with yours ([Facet displays](/docs/react#facet-displays)).

**Reference:** [`facets` in an answer](/docs/reference/query-api#search.answer.sections.facets)

## Filters in a sheet on a phone

`ListingPage` moves its filters into a sheet on a phone by itself. With `filters="sidebar"`, the panel beside the results gives way to a "Filter" button below 64rem:

```tsx twoslash title="app/routes/shop.tsx"
import { ListingPage, type ListingView } from "@orbsearch/react";
declare const listing: ListingView;
// ---cut---
<ListingPage listing={listing} filters="sidebar" />;
```

With the default bar, "Filter" opens every facet in the same sheet. Choices there wait for its button, "Ergebnisse anzeigen" (show results), whose count follows the pending choice. Escape or the close button discard them. Without script the sheet cannot open, so the panel's plain form shows on every screen.

In a layout of your own, `FilterSheet` takes the facets and the listing's state from `useListing`:

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

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

    return (
        <FilterSheet
            facets={listing.facets}
            value={page.value}
            onApply={page.apply}
            total={listing.total}
            count={page.count}
            chosen={page.chosen}
        />
    );
}
```

For a dialog of your own, `useFilters` holds the pending choice apart from the applied one and counts it, a moment after the shopper stops choosing:

```tsx twoslash title="app/filter-dialog.tsx"
import { FacetList, useFilters, useStorefront, type ListingState, type ListingView } from "@orbsearch/react";

type Props = { listing: ListingView; page: ListingState; close: () => void };

export function FilterDialog({ listing, page, close }: Props) {
    const { messages } = useStorefront();
    const filters = useFilters({ value: page.value, total: listing.total, count: page.count, onApply: page.apply });

    return (
        <form
            onSubmit={(event) => {
                event.preventDefault();
                filters.apply();
                close();
            }}
        >
            <FacetList facets={listing.facets} value={filters.pending} onChange={filters.change} />
            <button type="submit" aria-busy={filters.counting}>
                {messages.filters.showResults(filters.total)}
            </button>
        </form>
    );
}
```

`page` is what `useListing(listing)` returned in the page that opens the dialog. `filters.start()` begins again from the applied filters, as a dialog does each time it opens.

## Next

- [Theming](/docs/theming): give the shop its look.
- [Accessibility](/docs/accessibility): what the pages do for keyboard and screen reader users, and what stays yours.
- [Clicks and views](/docs/clicks-and-views): what the pages report for Insights.
