Customize an Existing Widget
On this page
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 theattribute
prop must be present in “Attributes to snippet” on the Algolia dashboard or configured asattributesToSnippet
via aset 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 namehit
: a single result objecthighlightProperty
: the path to the structure containing the highlighted attribute. The value is either_highlightResult
or_snippetResult
depending on whether you want to make aHighlight
or aSnippet
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:
- https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/reset.css
- https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia.css
minified:
- https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/reset-min.css
- https://cdn.jsdelivr.net/npm/instantsearch.css@7.3.1/themes/algolia-min.css
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 RefinementList
connector 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 itemsisFromSearch
:true
when you are in search mode and the provideditems
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:
- business logic code
- 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 torefine
orcreateURL
item.label
: The label to display, for example “Samsung”item.count
: The number of hits matching this itemitem.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.