Concepts / Building Search UI / Customize an existing widget
May. 16, 2019

Customize an Existing Widget

Highlight and snippet your search results

Search is all about helping users understand the results. This is especially true when using text-based search. When a user types a query in the search box, the results must show why the results are matching the query. That’s why Algolia implements a powerful highlighting that lets you display the matching parts of text attributes in the results. On top of that, Algolia implements snippeting to get only the meaningful part of a text, when attributes have a lot of content.

This feature is already packaged for you in React InstantSearch and like most of its features it comes in two flavors, depending on your use case:

  • when using the DOM, widgets are the way to go
  • when using another rendering (such as React Native), you will use the connector

Highlight & Snippet

Highlighting is based on the results and you will need to make a custom Hit in order to use the Highlighter. The Highlight and the Snippet widgets take two props:

  • attribute: the path to the highlighted attribute of the hit (which can be either a string or an array of strings)
  • hit: a single result object

Notes:

  • Use the Highlight widget when you want to display the regular value of an attribute.
  • Use the Snippet widget when you want to display the snippet version of an attribute. To use this widget, the attribute name passed to the attribute prop must be present in “Attributes to snippet” on the Algolia dashboard or configured as attributesToSnippet via a set settings call to the Algolia API.

Here is an example in which we create a custom Hit widget for results that have a name field that is highlighted. In these examples we use the mark tag to highlight. This is a tag specially made for highlighting pieces of text. The default tag is em, mostly for legacy reasons.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, SearchBox, Hits, Highlight } from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const Hit = ({ hit }) => (
  <p>
    <Highlight attribute="name" hit={hit} tagName="mark" />
  </p>
);

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

export default App;

connectHighlight

The connector provides a function that will extract the highlighting data from the results. This function takes a single parameter object with three properties:

  • attribute: the highlighted attribute name
  • hit: a single result object
  • highlightProperty: the path to the structure containing the highlighted attribute. The value is either _highlightResult or _snippetResult depending on whether you want to make a Highlight or a Snippet widget.

Those parameters are taken from the context in which the custom component is used, therefore it’s reasonable to have them as props.

Here is an example of a custom Highlight widget. It can be used the same way as the widgets.

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
import algoliasearch from 'algoliasearch/lite';
import { InstantSearch, SearchBox, Hits, connectHighlight } from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const CustomHighlight = connectHighlight(({ highlight, attribute, hit }) => {
  const parsedHit = highlight({
    highlightProperty: '_highlightResult',
    attribute,
    hit
  });

  return (
    <div>
      {parsedHit.map(
        part => (part.isHighlighted ? <mark>{part.value}</mark> : part.value)
      )}
    </div>
  );
});

const Hit = ({ hit }) => (
  <p>
    <CustomHighlight attribute="name" hit={hit} />
  </p>
);

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

Style your widgets

All widgets under the react-instantsearch-dom namespace are shipped with fixed CSS class names.

The format for those class names is ais-NameOfWidget-element--modifier. We are following the naming convention defined by SUIT CSS.

The different class names used by each widget are described on their respective documentation pages. You can also inspect the underlying DOM and style accordingly.

Loading the theme

We do not load any CSS into your page automatically but we provide two themes that you can load manually:

  • reset.css
  • algolia.css

We strongly recommend that you use at least reset.css in order to avoid visual side effects caused by the new HTML semantics.

The reset theme CSS is included within the algolia CSS, so there is no need to import it separately when you are using the algolia theme.

Via CDN

The themes are available on jsDelivr:

unminified:

minified:

You can either copy paste the content into your own app or use a direct link to jsDelivr:

1
2
3
4
<!-- Include only the reset -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/reset-min.css" integrity="sha256-t2ATOGCtAIZNnzER679jwcFcKYfLlw01gli6F6oszk8=" crossorigin="anonymous">
<!-- or include the full Algolia theme -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia-min.css" integrity="sha256-HB49n/BZjuqiCtQQf49OdZn63XuKFaxcIHWf0HNKte8=" crossorigin="anonymous">

Via npm & Webpack

1
2
npm install instantsearch.css
npm install --save-dev style-loader css-loader
1
2
3
4
// Include only the reset
import 'instantsearch.css/themes/reset.css';
// or include the full Algolia theme
import 'instantsearch.css/themes/algolia.css';
1
2
3
4
5
6
7
8
9
10
module.exports = {
  module: {
    loaders: [
      {
        test: /\.css$/,
        loaders: ['style?insertAt=top', 'css'],
      },
    ],
  },
};

Other bundlers

Any other module bundler like Browserify or Parcel can be used to load our CSS. React InstantSearch does not rely on any specific module bundler or module loader.

Styling icons

You can style the icon colors using the widget class names:

1
2
3
4
.ais-SearchBox-submitIcon path,
.ais-SearchBox-resetIcon path {
  fill: red,
}

Translate your widgets

All static text rendered by widgets, such as “Load more”, “Show more” are translatable using the translations prop on relevant widgets.

This prop is a mapping of keys-to-translation values. Translation values can be either a string or a function, as some take parameters.

The different translation keys supported by widgets and their optional parameters are described on their respective documentation page.

With a string

Here’s an example configuring the “Show more” label with a string on a <Menu>:

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

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <Menu
      attribute="categories"
      showMore={true}
      translations={{
        showMore: 'Voir plus'
      }}
    />
  </InstantSearch>
);

With a function

Here’s an example configuring the “Show more” label with a function on a <Menu>:

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

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <Menu
      attribute="categories"
      showMore={true}
      translations={{
        showMore(extended) {
          return extended ? 'Voir moins' : 'Voir plus';
        }
      }}
    />
  </InstantSearch>
);

Modify the list of items in widgets

Every widget and connector that handles a list of items exposes a transformItems option. This option is a function that takes the items as a parameter and expect to return the items back. This option can be used to sort, filter and add manual values.

Sorting

In this example we use the transformItems option to order the items by label in a ascending mode:

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

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <SearchBox />
    <RefinementList
      attribute="categories"
      transformItems={items => orderBy(items, "label", "asc")}
    />
  </InstantSearch>
);

Filtering

In this example we use the transformItems option to filter out items when the count is lower than 150

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

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

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

Add manual values

By default, the values in a RefinementList or a Menu are dynamic. This means that the values are updated with the context of the search. Most of the time this is the expected behavior, but in some cases you may want to have a static list of values that never change. To achieve this we can use the connectors.

In this example we are using the RefinementListconnector to display a static list of values. This RefinementList will always display and only display 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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import algoliasearch from 'algoliasearch/lite';
import {
  InstantSearch,
  SearchBox,
  connectRefinementList
} from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <SearchBox />
    <StaticRefinementList
      attribute="categories"
      values={[
        { label: 'iPad', value: 'iPad' },
        { label: 'iPhone', value: 'iPhone' },
      ]}
    />
  </InstantSearch>
);

const StaticRefinementList = connectRefinementList(
  ({ values, currentRefinement, items, refine }) => (
    <ul className="ais-RefinementList-list">
      {values.map(staticItem => {
        const { isRefined } = items.find(
          item => item.label === staticItem.label
        ) || {
          isRefined: false,
        };

        return (
          <li key={staticItem.value}>
            <label>
              <input
                type="checkbox"
                value={staticItem.value}
                checked={isRefined}
                onChange={event => {
                  const value = event.currentTarget.value;
                  const next = currentRefinement.includes(value)
                    ? currentRefinement.filter(current => current !== value)
                    : currentRefinement.concat(value);

                  refine(next);
                }}
              />
              {staticItem.label}
            </label>
          </li>
        );
      })}
    </ul>
  )
);

Searching long lists

For some cases, you want to be able to directly search into a list of facet values. This can be achieved using the searchable prop on widgets like RefinementList, Menu, or on connectors RefinementList and Menu. To enable this feature, you’ll need to make the attribute searchable using the API or the Dashboard.

With widgets

Use the searchable prop to add a search box to supported widgets:

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

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <RefinementList
      attribute="brand"
      searchable={true}
    />
  </InstantSearch>
);

With connectors

You can implement your own search box for searching for items in lists when using supported connectors by using those provided props:

  • searchForItems(query): call this function with a search query to trigger a new search for items
  • isFromSearch: true when you are in search mode and the provided items are search items results
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 algoliasearch from 'algoliasearch/lite';
import {
  InstantSearch,
  Highlight,
  connectRefinementList
} from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);


const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <RefinementListWithSearchBox attribute="brand" />
  </InstantSearch>
);

const RefinementListWithSearchBox = connectRefinementList(
  ({ items, refine, searchForItems }) => {
    const values = items.map(item => {
      const label = item._highlightResult ? (
        <Highlight attribute="label" hit={item} />
      ) : (
        item.label
      );

      return (
        <li key={item.value}>
          <span onClick={() => refine(item.value)}>
            {label} {item.isRefined ? "- selected" : ""}
          </span>
        </li>
      );
    });

    return (
      <div>
        <input
          type="input"
          onChange={e => searchForItems(e.currentTarget.value)}
        />
        <ul>{values}</ul>
      </div>
    );
  }
);

Apply default value to widgets

A question that comes up frequently is “how do I instantiate a RefinementList widget with a pre-selected item?”. For this use case, you can use the Configure widget.

The following example instantiates a search page with a default query of “apple” and will show a category menu where the item “Cell Phones” is already selected:

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

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <SearchBox defaultRefinement="apple" />
    <RefinementList attribute="categories" defaultRefinement="Cell Phones" />
    <Hits />
  </InstantSearch>
);

Virtual Widgets

Many websites have “category pages” where the search context is already refined without the user having to do it. This constrains the search to only the specific results that you want to display. For example, an online shop for electronics devices could have a page like electronics.com/cell-phones that only shows cell phones:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import algoliasearch from 'algoliasearch/lite';
import {
  connectRefinementList,
  Hits,
  InstantSearch,
  SearchBox,
} from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const VirtualRefinementList = connectRefinementList(() => null);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <VirtualRefinementList attribute="categories" defaultRefinement="Cell Phones" />
    <SearchBox />
    <Hits />
  </InstantSearch>
);

In this case, we are using the VirtualRefinementList with the defaultRefinement to pre-refine our results (within the categories search, only display Cell Phones). Think of the VirtualRefinementList as a hidden filter where we define attributes and values that will always be applied to our search results.

Hiding default refinements

In some situations not only do you want default refinements but you also do not want the user to be able to unselect them.

By default, the CurrentRefinements widget or the CurrentRefinements connector will display the defaultRefinement. If you want to hide it, you need to filter the items with transformItems.

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 algoliasearch from 'algoliasearch/lite';
import {
  InstantSearch,
  CurrentRefinements,
  connectMenu,
} from 'react-instantsearch-dom';

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const VirtualMenu = connectMenu(() => null);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <VirtualMenu attribute="categories" defaultRefinement="Cell Phones" />
    <CurrentRefinements
      transformItems={items =>
        items.filter(item => item.currentRefinement !== 'Cell Phones')
      }
    />
  </InstantSearch>
);

Configure

It might happen that the predefined widgets don’t fit your use case; in that case you can still apply search parameters by using the Configure widget.

Here’s how you can then preselect a brand without even having to have the underlying RefinementList widget used:

1
2
3
4
5
<InstantSearch searchClient={searchClient} indexName="instant_search">
  <Configure filters="brand:Samsung" />
  <SearchBox />
  <Hits />
</InstantSearch>

To understand how the filters syntax works and what can you put inside, read the filtering guide.

How to provide search parameters

Algolia has a wide range of parameters. If one of the parameters you want to use is not covered by any widget or connector, then you can use the configure widget.

Here’s an example configuring the distinct parameter:

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

const searchClient = algoliasearch(
  'latency',
  '6be0576ff61c053d5f9a3225e2a90f76'
);

const App = () => (
  <InstantSearch indexName="instant_search" searchClient={searchClient}>
    <Configure distinct={1}/>
    {/* Widgets */}
  </InstantSearch>
);

Notes:

  • There’s a dedicated guide showing how to configure default refinements on widgets.
  • You could also pass hitsPerPage: 20 to configure the number of hits being shown when not using the /doc/api-reference/widgets/instantsearch/react/.

Dynamic update of search parameters

Every applied search parameter can be retrieved by listening to the onSearchStateChange hook from the /doc/api-reference/widgets/instantsearch/react/ root component.

But to update search parameters, you will need to pass updated props to the Configure widget. Directly modifying the search state prop and injecting it will have no effect.

Read the example performing geo-search with React InstantSearch to see how you can update search parameters.

Filters your results in a way that is not covered by any widget

Our widgets already provides a lot of different ways to filter your results but sometimes you might have more complicated use cases that require the usage of the filters search parameter.

Don’t use filters on a attribute already used with a widget, it will conflict.

1
<Configure filters="NOT categories:"Cell Phones"/>

Customize the complete UI of the widgets

Extending React InstantSearch widgets is the second layer of our API. Read about the two others possibilities in the “What is InstantSearch?” guide.

This guide explains what you need to know before using connectors (the API feature behind extending widgets use case). Some parts may seem abstract at first but they will make more sense once you try at least one connector.

When do I need to extend widgets?

By extending widgets we mean being able to redefine the rendering output of an existing widget. Let’s say you want to render the Menu widget as an HTML select element. To do this you need to extend the Menu widget.

Here are some common examples that require the usage of the connectors API:

  • When you want to display our widgets using another UI library like Material-UI
  • When you want to have full control on the rendering without having to reimplement business logic
  • As soon as you hit a feature wall using our default widgets
  • When you are a React Native, read our guide on React Native for more information

How widgets are built

React InstantSearch widgets are built in two parts:

  1. business logic code
  2. rendering code

The business logic is what we call connectors. They are implemented with higher order components. They encapsulate the logic for a specific kind of widget and they provide a way to interact with the InstantSearch context. Those connectors allow you to completely extend existing widgets. The rendering is the React specific code that is tied to each platform (DOM or Native).

Connectors

Connectors are the API implementation we provide for you to extend widgets. Connectors are functions you import and use to get the business logic of a particular widget.

There’s a 1-1 mapping between widgets and connectors, every widget has a connector and vice-versa.

This means that anytime you want to extend a widget, you need to use its connector.

Connectors for every widget are documented in the API reference, for example the menu widget.

Connectors render API

We try to share as much of a common API between all connectors. So that once you know how to use one connector, you can use them all.

As higher order components, they have an outer component API that we call exposed props. They will also provide some other props to the wrapped components - these are called the provided props.

Exposed props

Connectors expose props to configure their behavior. Like the attribute being refined in a Menu. One common exposed prop that you can use is the defaultRefinement. Use it as a way to provide the default refinement when the connected component will be mounted.

Provided props

Most of the connectors will use the same naming for properties passed down to your components.

  • items[]: array of items to display, for example the brands list of a custom Refinement List. Every extended widget displaying a list gets an items property to the data passed to its render function.
  • refine(value|item.value): will refine the current state of the widget. Examples include: updating the query for a custom SearchBox or selecting a new item in a custom RefinementList.
  • currentRefinement: currently applied refinement value (usually the call value of refine()).
  • createURL(value|item.value): will return a full url you can display for the specific refine value given you are using the routing feature.

An item is an object that you will find in items arrays. The shape of those objects is always the same.

  • item.value: The underlying precomputed state value to pass to refine or createURL
  • item.label: The label to display, for example “Samsung”
  • item.count: The number of hits matching this item
  • item.isRefined: Is the item currently selected as a filter

Some connectors will have more data than others. Read their API reference to know more. Connectors for every widget are documented in the API reference, for example the menu widget.

Extending widget example

Below is a fully working example of a Menu widget rendered as a select HTML element.

The files to look at are src/MenuSelect.js where you will find the code using connectMenu. And src/App.js where we use the newly created component.

Head over to our community forum if you still have questions about extending widgets.

The search state format explained

The searchState contains all widgets states. If a widget uses an attribute, we store it under its widget category to prevent collision.

Here’s the searchState shape for all the connectors or widgets that we provide:

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
const searchState = {
  range: {
    price: {
      min: 20,
      max: 3000
    }
  },
  configure: {
    aroundLatLngViaIP: true,
  },
  refinementList: {
    fruits: ['lemon', 'orange']
  },
  hierarchicalMenu: {
    products: 'Laptops > Surface'
  },
  menu: {
    brands: 'Sony'
  },
  multiRange: {
    rank: '2:5'
  },
  toggle: {
    freeShipping: true
  },
  hitsPerPage: 10,
  sortBy: 'mostPopular',
  query: 'ora',
  page: 2
}

If you are performing a search on multiple indices using the Index component, you’ll get the following shape:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const searchState = {
  query: 'ora', //shared state between all indices
  page: 2, //shared state between all indices
  indices: {
    index1: {
      configure: {
        hitsPerPage: 3,
      },
    },
    index2: {
      configure: {
        hitsPerPage: 10,
      },
    },
  },
}

Note: Widgets remain mandatory for applying state to queries. In other words, to apply a refinement, the widget that controls this refinement must be mounted on the page. For example, without having a mounted <SearchBox> component, you cannot apply URL syncing. To apply a refinement without having anything render on the page you can use a Virtual Widget.

Did you find this page helpful?