Guides / Building Search UI / Widgets / Customize an existing widget

Customize a React InstantSearch Hooks Widget

React InstantSearch Hooks offers various APIs to let you customize your search and discovery experience without considering its inner workings.

For example, you can customize how the experience looks:

You can also adjust the data sent to or received from Algolia:

Highlight and snippet your search results

Search interfaces are all about helping users understand the results. When users perform a textual search, they need to know why they’re getting the search results they received.

Algolia provides a typo-tolerant highlighting feature to display the matching parts in the results. Algolia also provides snippeting to create an excerpt of a longer piece of text, truncated to a fixed size around the match.

Snippeted attributes are also highlighted. When working with <Snippet>, the attribute must be set up in attributesToSnippet either inside the Algolia dashboard or at runtime.

On the web

React InstantSearch Hooks provides the <Highlight> and <Snippet> components to highlight and snippet your Algolia search results. Both widgets take two props:

  • attribute: the path to the highlighted attribute of the hit
  • hit: a single result object
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import {
  InstantSearch,
  Hits,
  Highlight,
  Snippet,
} from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

function Hit({ hit }) {
  return (
    <article>
      <h1>
        <Highlight attribute="name" hit={hit} />
      </h1>
      <Snippet hit={hit} attribute="description" />
    </article>
  );
}

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <Hits hitComponent={Hit} />
    </InstantSearch>
  );
}

With React Native

The <Highlight> and <Snippet> components are designed for the web platform.

To highlight matches in a React Native app, you can build a custom <Highlight> component using the provided getHighlightedParts and getPropertyByPath utilities from instantsearch.js.

Style your widgets

All React InstantSearch Hooks widgets use a set of conventional CSS classes compatible with the InstantSearch CSS themes.

If you don’t want to use the themes, you can either write your own theme by following the existing classes and customizing the styles, or override the classes to pass yours instead.

Loading the InstantSearch theme

React InstantSearch Hooks doesn’t inject any styles by default but comes with two optional themes: Algolia and Satellite. You can import them from a CDN or using npm.

Using a CDN

The themes are available on jsDelivr.

Unminified:

  • https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/reset.css
  • https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/algolia.css
  • https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite.css

Minified:

  • https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/reset-min.css
  • https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/algolia-min.css
  • https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css

The reset stylesheet is included by default in the Satellite theme, so there’s no need to import it separately.

Either copy the files into your own app or use a direct link to jsDelivr.

1
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.4.5/themes/satellite-min.css" integrity="sha256-TehzF/2QvNKhGQrrNpoOb2Ck4iGZ1J/DI4pkd2oUsBc=" crossorigin="anonymous">

Using npm

If your project imports CSS into JavaScript files using build tools (such as webpack, Parcel, Vite, or a framework that uses build tools (like Next.js or Nuxt), you can install the theme with npm.

1
2
3
npm install instantsearch.css
# or
yarn add instantsearch.css

Then, you can import the theme directly into your app.

1
import 'instantsearch.css/themes/satellite.css';

Writing your own theme

You can create your own theme based on the InstantSearch CSS classes.

Either inspect the DOM with your developer tools and style accordingly or reuse an existing theme and customize it.

1
2
3
4
5
6
7
.ais-Breadcrumb-item--selected,
.ais-HierarchicalMenu-item--selected,
.ais-Menu-item--selected {
  font-weight: semibold;
}

/* ... */

Passing custom CSS classes

If you’re using a class-based CSS framework like Bootstrap or Tailwind CSS, you can pass your own CSS classes to the classNames prop to style each element of the widgets.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ...

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <SearchBox
        classNames={{
          root: 'p-3 shadow-sm',
          form: 'relative',
          input: 'block w-full pl-9 pr-3 py-2 bg-white border border-slate-300 placeholder-slate-400 focus:outline-none focus:border-sky-500 focus:ring-sky-500 rounded-md focus:ring-1',
          submitIcon: 'absolute top-0 left-0 bottom-0 w-6',
        }}
      />
      {/* ... */}
    </InstantSearch>
  );
}

If you only want to pass classes to the root element, use the className React prop.

1
2
3
4
5
6
7
8
9
10
// ...

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <SearchBox className="p-3 shadow-sm" />
      {/* ... */}
    </InstantSearch>
  );
}

Change the list of items in widgets

Every widget and Hook that handles a list of items exposes a transformItems prop that lets you transform the items before displaying them in the UI. This is useful to sort or filter items.

You can also leverage Hooks to add items that don’t come from Algolia, or append cached results to the list of displayed items.

Sorting items

This example uses the transformItems prop to order the current refinements by ascending attribute:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ...
import { orderBy } from 'lodash';

// ...

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      {/* ... */}
      <CurrentRefinements
        transformItems={(items) => orderBy(items, 'attribute', 'asc')}
      />
    </InstantSearch>
  );
}

If you want to sort items in a <RefinementList>, use the sortBy prop.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      {/* ... */}
      <RefinementList
        attribute="categories"
        sortBy={['label:asc']}
      />
    </InstantSearch>
  );
}

Filtering items

This example uses the transformItems prop to filter out items when the count is lower than 150.

1
2
3
4
5
6
7
8
9
10
11
12
13
// ...

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      {/* ... */}
      <RefinementList
        attribute="categories"
        transformItems={(items) => items.filter((item) => item.count >= 150)}
      />
    </InstantSearch>
  );
}

Displaying static values

The facet values exposed in widgets like <RefinementList> or <Menu> are dynamic, and update with the context of the search. However, sometimes you may want to display a static list that never changes. You can do so using transformItems.

This example uses transformItems to display a static list of values. This <RefinementList> always and only displays the items “iPad” and “Printers”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// ...
import { InstantSearch, RefinementList } from 'react-instantsearch-web';

// ...

const staticItems = [
  { label: 'Appliances', value: 'Appliances' },
  { label: 'Cell Phones', value: 'Cell Phones' },
];

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      {/* ... */}
      <RefinementList
        attribute="categories"
        transformItems={(items) => {
          return staticItems.map((staticItem) => ({
            ...staticItem,
            ...items.find((item) => item.value === staticItem.value),
          }));
        }}
      />
    </InstantSearch>
  );
}

Displaying facets with no matches

Hiding facets when they don’t match a query can be counter-intuitive. However, because of how Algolia handles faceting, you have to rely on workarounds on the front end to display facets with no hits.

One way of displaying facets with no matches is to cache results the first time you receive them. Then, if the amount of actual facet hits that Algolia returns is below the limit set, you can append the cached facets to the list.

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import { InstantSearch } from 'react-instantsearch-hooks-web';

import { CustomRefinementList } from './CustomRefinementList';
import { indexName, searchClient } from './searchClient';

function App() (
  return (
    <InstantSearch indexName={indexName} searchClient={searchClient}>
      <CustomRefinementList attribute="brand" />
    </InstantSearch>
  );
);

This solution comes with limitations:

  • Facet hits from a faceted search won’t work because Algolia only returns matching facets (the highlighting can’t apply to cached items).
  • You might need to sort again in the custom widget because the internal sorting happens before rendering.

Apply default values to widgets

When first loading the page, you might want to assign default values to some widgets. For example, you may want to set a default filter based on a user setting, pre-select an item in a <RefinementList> when you’re on a category page, or sync the UI with your URL state.

Providing an initial state

Providing an initial state is useful when you want to start from a given state, but you expect it to change from user interactions with the widgets.

This example provides an initialUiState to React InstantSearch Hooks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import {
  InstantSearch,
  Pagination,
  SearchBox,
} from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
const indexName= 'instant_search';

function App() {
  return (
    <InstantSearch
      indexName={indexName}
      searchClient={searchClient}
      initialUiState={{
        [indexName]: {
          query: 'phone',
          page: 5,
        },
      }}
    >
      <SearchBox />
      {/* ... */}
      <Pagination />
    </InstantSearch>
  );
}

Note the presence of the <SearchBox> and <Pagination> widgets. When providing an initialUiState, you must also add the corresponding widgets to your implementation.

Synchronizing the UI state and the URL

Synchronizing your UI with the browser URL is a good practice. It lets users take one of your results pages, copy the URL, and share it. It also improves the user experience by enabling the use of the back and next browser buttons to keep track of previous searches.

To sync the UI state and the URL, use the routing option.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { InstantSearch, SearchBox } from 'react-instantsearch-hooks-web';
import algoliasearch from 'algoliasearch/lite';

const searchClient = algoliasearch(YourApplicationID, YourSearchOnlyAPIKey);

function App() {
  return (
    <InstantSearch
      searchClient={searchClient}
      indexName="instant_search"
      routing={true}
    >
      <SearchBox />
      {/* ... */}
    </InstantSearch>
  );
}

Note the presence of the <SearchBox> widget. When using routing, you must also add the corresponding widgets to your implementation.

Manually set search parameters

Algolia supports a wide range of search parameters. If you want to use a search parameter that isn’t covered by any widget or Hook, use the <Configure> widget.

Using <Configure> is also useful to provide a search parameter that users aren’t meant to interact with directly.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Configure } from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <Configure
        analytics={false}
        distinct={true}
        getRankingInfo={true}
      />
    </InstantSearch>
  );
}

Filtering results without using widgets or Hooks

If you need to set a fixed filter without letting users change it, pass filters to the <Configure> widget. This can be useful, for example, to reflect user preferences saved in your app.

1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Configure } from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <Configure filters="free_shipping:true" />
    </InstantSearch>
  );
}

Don’t set filters on <Configure> for an attribute already managed by a widget, or they will conflict.

Dynamically updating search parameters

The <InstantSearch> root component exposes an onStateChange prop. This function triggers whenever the UI state changes (such as typing in a search box or selecting a refinement). You get a chance to alter the next UI state or perform custom logic before applying it.

In this example, there’s a static “All” item at the top of the categories refinement list. When checked, it clears all existing category refinements. This is achieved by intercepting the next UI state in onStateChange before it’s applied and changing it with custom logic.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, RefinementList } from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

const indexName = 'instant_search';

export function App() {
  return (
    <InstantSearch
      indexName={indexName}
      searchClient={searchClient}
      onStateChange={({ uiState, setUiState }) => {
        const categories = uiState[indexName].refinementList?.categories || [];
        const [lastSelectedCategory] = categories.slice(-1);

        setUiState({
          ...uiState,
          [indexName]: {
            ...uiState[indexName],
            refinementList: {
              ...uiState[indexName].refinementList,
              categories: categories.filter((category) =>
                lastSelectedCategory === 'All' ? false : category !== 'All'
              ),
            },
          },
        });
      }}
    >
      <RefinementList
        attribute="categories"
        transformItems={(items) =>
          [
            {
              label: 'All',
              value: 'All',
              count: null,
              isRefined: items.every((item) => !item.isRefined),
            },
          ].concat(items)
        }
      />
      {/* ... */}
    </InstantSearch>
  );
}

If you’ve set a search parameter using <Configure>, you can update it like any React state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { useEffect, useState } from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, Configure } from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

const languages = [
  { label: 'Français', value: 'fr_FR' },
  { label: 'English', value: 'en_US' },
];

export function App() {
  const [language, setLanguage] = useState(languages[0].value);

  return (
    <>
      <label htmlFor="language">Language</label>
      <select
        id="language"
        value={language}
        onChange={(event) => setLanguage(event.target.value)}
      >
        {languages.map(({ label, value }) => (
          <option key={value} value={value}>
            {label}
          </option>
        ))}
      </select>
      <InstantSearch indexName={indexName} searchClient={searchClient}>
        <Configure filters={`language:${language}`} />
        {/* ... */}
      </InstantSearch>
    </>
  );
}

Customize the complete UI of the widgets

Hooks are the headless counterparts of widgets. They return APIs to build the UI as you see fit.

If you feel limited by the provided customization options, you can use Hooks to control the render output. This is useful when using React InstantSearch Hooks together with a component library or when rendering to a non-DOM target like React Native.

Hooks can also be helpful if you want to create a custom behavior or a radically different UI based on the APIs of an existing widget.

For example, to set pre-determined search queries by clicking on a button, you could use useSearchBox() and render a button that sets a given query on click.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, useSearchBox } from 'react-instantsearch-hooks-web';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <PopularQueries queries={['apple', 'samsung']} />
    </InstantSearch>
  );
}

function PopularQueries({ queries, ...props }) {
  const { refine } = useSearchBox(props);

  return (
    <div>
      {queries.map((query) => (
        <button key={query} onClick={() => refine(query)}>
          Look for "{query}"
        </button>
      ))}
    </div>
  );
}

Another example is when you need to create a virtual widget to apply a refinement without displaying anything on the page.

When not to use Hooks

When you need to customize the UI of a widget, you might be tempted to use Hooks. While this grants you complete control, it also means you become responsible for adequately implementing UI logic and accessibility.

The provided UI components already handle this complexity and provide many customization options for you to change the way they look. If you only need to make visual changes to a widget, you should use customization options instead of reaching for Hooks.

To summarize, you should avoid using Hooks to:

Using custom components with Hooks

If you’re using a component library like Material UI or your own, you might want to use these components instead of the provided ones.

You can use Hooks to access the necessary state and APIs to stitch React InstantSearch Hooks together with your custom components.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch } from 'react-instantsearch-hooks-web';

import { CustomSortBy } from './CustomSortBy';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');

function App() {
  return (
    <InstantSearch indexName="instant_search" searchClient={searchClient}>
      <CustomSortBy
        items={[
          { label: 'Featured', value: 'instant_search' },
          { label: 'Price (asc)', value: 'instant_search_price_asc' },
          { label: 'Price (desc)', value: 'instant_search_price_desc' },
        ]}
      />
    </InstantSearch>
  );
}

Check the React InstantSearch Hooks API reference for any widget you want to customize and look up the Hook section.

Building a virtual widget with Hooks

A virtual widget is an InstantSearch widget mounted in the app but which doesn’t render anything.

Widgets do more than displaying a UI: they each interact with a piece of the UI state that maps to one or more Algolia search parameters. For example, the <SearchBox> controls the query, the <RefinementList> interacts with facetFilters, etc. When mounting a widget or using the corresponding Hook, it’s reflected in the InstantSearch UI state and included in search requests.

Issues can arise when you don’t want to render a widget, or in more complex mounting and unmounting scenarios. For example, imagine a mobile search interface with filters in a modal, using a <RefinementList>. The <Modal> component mounts and unmounts its content when toggled, meaning that when the app first loads, there’s no <RefinementList>. When opening the modal, the <RefinementList> is added to the search state, triggering a new request even before selecting a refinement. When closing the modal, the <RefinementList> is removed from the search state, losing all applied refinements.

Unless you can toggle visibility and keep the modal content mounted, the recommended way to solve this problem is to use a virtual widget.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React from 'react';
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, RefinementList } from 'react-instantsearch-hooks-web';

import { Modal } from './Modal';
import { VirtualRefinementList } from './VirtualRefinementList';

const searchClient = algoliasearch('YourApplicationID', 'YourSearchOnlyAPIKey');
const refinementAttribute = "categories";

function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  return (
    <InstantSearch indexName="instant_search" searchClient="instant_search">
      <VirtualRefinementList attribute={refinementAttribute} />
      {/* ... */}
      <button onClick={() => setIsModalOpen(true)}>Filters</button>
      <Modal open={isModalOpen}>
        <RefinementList attribute={refinementAttribute} />
      </Modal>
    </InstantSearch>
  );
}

The <VirtualRefinementList> component is renderless, but it uses useRefinementList() which registers it in the UI state for the categories attribute. It remains synchronized with the <RefinementList> nested in the modal. When the modal isn’t mounted, this virtual widget preserves the applied search state.

Next steps

You now have a good starting point to create an even more custom experience with React InstantSearch Hooks. Next up, you could improve this app by:

  • Checking the API reference to learn more about the widgets and Hooks.
Did you find this page helpful?