Skip to main content
InstantSearch.js provides a comprehensive set of widgets to build search interfaces. All widgets follow a consistent API pattern.

Widget API Pattern

Every widget is a function that accepts an options object:
widgetName({
  container: '#selector',
  // widget-specific options
})
Widgets must be added before calling search.start():
import instantsearch from 'instantsearch.js';
import { searchBox, hits } from 'instantsearch.js/es/widgets';

const search = instantsearch({ /* options */ });

search.addWidgets([
  searchBox({ container: '#searchbox' }),
  hits({ container: '#hits' }),
]);

search.start();

Search Widgets

A search input with submit and reset buttons:
import { searchBox } from 'instantsearch.js/es/widgets';

searchBox({
  container: '#searchbox',
  placeholder: 'Search for products...',
  autofocus: true,
  searchAsYouType: true,
  showReset: true,
  showSubmit: true,
  showLoadingIndicator: true,
  queryHook: (query, search) => {
    // Modify query before searching (e.g., debouncing)
    search(query);
  },
  templates: {
    submit: ({ html }) => html`<span>🔍</span>`,
    reset: ({ html }) => html`<span>✕</span>`,
    loadingIndicator: ({ html }) => html`<span>⏳</span>`,
  },
  cssClasses: {
    root: 'my-SearchBox',
    form: 'my-SearchBox-form',
    input: 'my-SearchBox-input',
  },
})
Key Options:
  • searchAsYouType (default: true) - Search as user types vs. on submit only
  • queryHook - Transform query before search (useful for debouncing)
  • autofocus - Focus input on page load

hits

Display search results:
import { hits } from 'instantsearch.js/es/widgets';

hits({
  container: '#hits',
  templates: {
    item: (hit, { html, components, sendEvent }) => html`
      <article>
        <img src="${hit.image}" alt="${hit.name}" />
        <h3>${components.Highlight({ hit, attribute: 'name' })}</h3>
        <p>${components.Snippet({ hit, attribute: 'description' })}</p>
        <span class="price">$${hit.price}</span>
        <button
          onClick="${() => sendEvent('click', hit, 'Product Clicked')}"
        >
          View Details
        </button>
      </article>
    `,
    empty: ({ html }) => html`
      <div class="no-results">
        <p>No results found</p>
      </div>
    `,
  },
  transformItems: (items) =>
    items.map((item) => ({
      ...item,
      price: (item.price / 100).toFixed(2), // Convert cents to dollars
    })),
  cssClasses: {
    root: 'my-Hits',
    list: 'my-Hits-list',
    item: 'my-Hits-item',
    emptyRoot: 'my-Hits--empty',
  },
})
Template Parameters:
  • html - Tagged template function for rendering
  • components - Highlight, Snippet, and other helper components
  • sendEvent - Function to track insights events

infiniteHits

Load more results with infinite scrolling:
import { infiniteHits } from 'instantsearch.js/es/widgets';

infiniteHits({
  container: '#infinite-hits',
  showPrevious: true,
  templates: {
    item: (hit, { html, components }) => html`
      <article>
        <h3>${components.Highlight({ hit, attribute: 'name' })}</h3>
      </article>
    `,
    showPreviousText: ({ html }) => html`<span>← Show previous</span>`,
    showMoreText: ({ html }) => html`<span>Show more →</span>`,
  },
  transformItems: (items) => items,
})

configure

Set search parameters without UI:
import { configure } from 'instantsearch.js/es/widgets';

configure({
  hitsPerPage: 20,
  attributesToSnippet: ['description:50'],
  snippetEllipsisText: '...',
  removeWordsIfNoResults: 'allOptional',
  distinct: 1,
  enablePersonalization: true,
  clickAnalytics: true,
  analytics: true,
  analyticsTags: ['desktop', 'search-page'],
})
Accepts any search parameter from the Algolia API.

Filtering Widgets

refinementList

Faceted search filters with checkboxes:
import { refinementList } from 'instantsearch.js/es/widgets';

refinementList({
  container: '#brand',
  attribute: 'brand',
  searchable: true,
  searchablePlaceholder: 'Search brands...',
  searchableIsAlwaysActive: true,
  showMore: true,
  showMoreLimit: 20,
  limit: 10,
  operator: 'or', // or 'and'
  sortBy: ['name:asc'], // or ['count:desc', 'name:asc']
  templates: {
    searchableNoResults: ({ html }) => html`<p>No brands found</p>`,
    showMoreText: ({ isShowingMore, html }) => html`
      <span>${isShowingMore ? 'Show less' : 'Show more'}</span>
    `,
    item: ({ label, count, isRefined, html }) => html`
      <label>
        <input type="checkbox" checked="${isRefined}" />
        <span>${label} (${count})</span>
      </label>
    `,
  },
  transformItems: (items) =>
    items.map((item) => ({
      ...item,
      label: item.label.toUpperCase(),
    })),
  cssClasses: {
    searchableInput: 'my-RefinementList-searchBox',
    showMore: 'my-RefinementList-showMore',
  },
})
Key Options:
  • attribute - Index attribute to filter on
  • operator - "or" (default) or "and" for combining values
  • searchable - Add search within facets
  • showMore - Enable “Show more” button

hierarchicalMenu

Multi-level categorization:
import { hierarchicalMenu } from 'instantsearch.js/es/widgets';

hierarchicalMenu({
  container: '#categories',
  attributes: [
    'hierarchicalCategories.lvl0',
    'hierarchicalCategories.lvl1',
    'hierarchicalCategories.lvl2',
  ],
  separator: ' > ',
  rootPath: null,
  showParentLevel: true,
  limit: 10,
  showMore: true,
  showMoreLimit: 20,
  sortBy: ['name:asc'],
  templates: {
    item: ({ label, count, html }) => html`
      <span>${label} <small>(${count})</small></span>
    `,
  },
})
Single-select filter:
import { menu } from 'instantsearch.js/es/widgets';

menu({
  container: '#category',
  attribute: 'category',
  limit: 10,
  showMore: true,
  showMoreLimit: 20,
  sortBy: ['name:asc'],
  templates: {
    item: ({ label, count, isRefined, html }) => html`
      <span class="${isRefined ? 'selected' : ''}">
        ${label} (${count})
      </span>
    `,
  },
})

rangeInput

Numeric range filter with min/max inputs:
import { rangeInput } from 'instantsearch.js/es/widgets';

rangeInput({
  container: '#price',
  attribute: 'price',
  min: 0,
  max: 1000,
  precision: 2,
  templates: {
    separatorText: ({ html }) => html`<span>to</span>`,
    submitText: ({ html }) => html`<span>Apply</span>`,
  },
})

rangeSlider

Numeric range filter with slider:
import { rangeSlider } from 'instantsearch.js/es/widgets';

rangeSlider({
  container: '#price-slider',
  attribute: 'price',
  min: 0,
  max: 1000,
  step: 10,
  pips: true,
  precision: 2,
  tooltips: true,
})
The rangeSlider widget requires the nouislider library:
npm install nouislider

toggleRefinement

Boolean on/off filter:
import { toggleRefinement } from 'instantsearch.js/es/widgets';

toggleRefinement({
  container: '#free-shipping',
  attribute: 'free_shipping',
  on: true,
  off: false,
  templates: {
    labelText: ({ isRefined, html }) => html`
      <span>Free shipping ${isRefined ? '✓' : ''}</span>
    `,
  },
})

ratingMenu

Star rating filter:
import { ratingMenu } from 'instantsearch.js/es/widgets';

ratingMenu({
  container: '#rating',
  attribute: 'rating',
  max: 5,
  templates: {
    item: ({ stars, html }) => html`<span>${stars}</span>`,
  },
})

numericMenu

Predefined numeric ranges:
import { numericMenu } from 'instantsearch.js/es/widgets';

numericMenu({
  container: '#price-ranges',
  attribute: 'price',
  items: [
    { label: 'All' },
    { label: 'Under $25', end: 25 },
    { label: '$25 - $100', start: 25, end: 100 },
    { label: '$100 - $500', start: 100, end: 500 },
    { label: '$500 & above', start: 500 },
  ],
})

Refinement Management

currentRefinements

Display active filters:
import { currentRefinements } from 'instantsearch.js/es/widgets';

currentRefinements({
  container: '#current-refinements',
  excludedAttributes: ['query'],
  includedAttributes: [], // Include only these attributes
  transformItems: (items) =>
    items.filter((item) => item.attribute !== 'internal_field'),
  cssClasses: {
    root: 'my-CurrentRefinements',
    list: 'my-CurrentRefinements-list',
    item: 'my-CurrentRefinements-item',
  },
})

clearRefinements

Clear all or specific filters:
import { clearRefinements } from 'instantsearch.js/es/widgets';

clearRefinements({
  container: '#clear-refinements',
  excludedAttributes: ['category'], // Don't clear these
  includedAttributes: [], // Only clear these
  templates: {
    resetLabel: ({ hasRefinements, html }) => html`
      <span>Clear ${hasRefinements ? 'all' : 'filters'}</span>
    `,
  },
})

pagination

Navigate between result pages:
import { pagination } from 'instantsearch.js/es/widgets';

pagination({
  container: '#pagination',
  totalPages: 20,
  padding: 3,
  showFirst: true,
  showPrevious: true,
  showNext: true,
  showLast: true,
  scrollTo: '#searchbox', // Scroll to element on page change
  templates: {
    first: ({ html }) => html`<span>‹‹</span>`,
    previous: ({ html }) => html`<span>‹</span>`,
    next: ({ html }) => html`<span>›</span>`,
    last: ({ html }) => html`<span>››</span>`,
    page: ({ page, html }) => html`<span>${page}</span>`,
  },
})
Show hierarchical navigation path:
import { breadcrumb } from 'instantsearch.js/es/widgets';

breadcrumb({
  container: '#breadcrumb',
  attributes: [
    'hierarchicalCategories.lvl0',
    'hierarchicalCategories.lvl1',
    'hierarchicalCategories.lvl2',
  ],
  separator: ' / ',
  rootPath: null,
  transformItems: (items) => items,
  templates: {
    home: ({ html }) => html`<span>Home</span>`,
    separator: ({ html }) => html`<span>/</span>`,
  },
})

Utility Widgets

stats

Display search statistics:
import { stats } from 'instantsearch.js/es/widgets';

stats({
  container: '#stats',
  templates: {
    text: ({ nbHits, processingTimeMS, query, html }) => html`
      <span>
        ${nbHits.toLocaleString()} results found in ${processingTimeMS}ms
        ${query ? `for "<strong>${query}</strong>"` : ''}
      </span>
    `,
  },
})

sortBy

Switch between index replicas:
import { sortBy } from 'instantsearch.js/es/widgets';

sortBy({
  container: '#sort-by',
  items: [
    { label: 'Relevance', value: 'products' },
    { label: 'Price (asc)', value: 'products_price_asc' },
    { label: 'Price (desc)', value: 'products_price_desc' },
    { label: 'Most popular', value: 'products_popularity_desc' },
  ],
})

hitsPerPage

Select results per page:
import { hitsPerPage } from 'instantsearch.js/es/widgets';

hitsPerPage({
  container: '#hits-per-page',
  items: [
    { label: '12 hits per page', value: 12, default: true },
    { label: '24 hits per page', value: 24 },
    { label: '48 hits per page', value: 48 },
  ],
})

poweredBy

Show “Search by Algolia” logo:
import { poweredBy } from 'instantsearch.js/es/widgets';

poweredBy({
  container: '#powered-by',
  theme: 'light', // or 'dark'
})

Advanced Widgets

panel

Wrap any widget with a header/footer:
import { panel, refinementList } from 'instantsearch.js/es/widgets';

panel({
  templates: {
    header: ({ html }) => html`<h3>Brands</h3>`,
    footer: ({ html }) => html`<p>Select brands to filter</p>`,
  },
  hidden: ({ results }) => results.nbHits === 0,
  collapsed: ({ state }) => !state.query,
  cssClasses: {
    root: 'my-Panel',
    header: 'my-Panel-header',
    body: 'my-Panel-body',
    footer: 'my-Panel-footer',
  },
})(refinementList)({
  container: '#brand-list',
  attribute: 'brand',
})

index

Query multiple indices:
import { index, hits, configure } from 'instantsearch.js/es/widgets';

const search = instantsearch({
  indexName: 'products',
  searchClient,
});

search.addWidgets([
  searchBox({ container: '#searchbox' }),
  
  // Main index
  hits({ container: '#products' }),
  
  // Secondary index
  index({ indexName: 'categories' })
    .addWidgets([
      configure({ hitsPerPage: 3 }),
      hits({ container: '#categories' }),
    ]),
]);

search.start();

dynamicWidgets

Automatically render facets based on search results:
import { dynamicWidgets, refinementList, menu } from 'instantsearch.js/es/widgets';

dynamicWidgets({
  container: '#dynamic-widgets',
  widgets: [
    (container) => refinementList({ container, attribute: 'brand' }),
    (container) => menu({ container, attribute: 'category' }),
    (container) => refinementList({ container, attribute: 'color' }),
  ],
  fallbackWidget: ({ container, attribute }) =>
    refinementList({ container, attribute }),
})

AI-Powered Widgets

chat

Conversational search interface:
import { chat } from 'instantsearch.js/es/widgets';
import { carousel } from 'instantsearch.js/es/templates';

chat({
  container: '#chat',
  agentId: 'your-agent-id',
  templates: {
    item: (item, { html }) => html`
      <article>
        <img src="${item.image}" />
        <h3>${item.name}</h3>
      </article>
    `,
    layout: carousel(),
  },
})

filterSuggestions

AI-powered filter recommendations:
import { filterSuggestions } from 'instantsearch.js/es/widgets';

filterSuggestions({
  container: '#filter-suggestions',
  agentId: 'your-agent-id',
  attributes: ['brand', 'categories', 'color'],
  templates: {
    header: ({ html }) => html`<h3>Suggested Filters</h3>`,
  },
})

Recommendation Widgets

trendingItems

Show trending products:
import { trendingItems } from 'instantsearch.js/es/widgets';
import { carousel } from 'instantsearch.js/es/templates';

trendingItems({
  container: '#trending',
  limit: 6,
  templates: {
    item: (item, { html }) => html`
      <article>
        <img src="${item.image}" />
        <h3>${item.name}</h3>
        <span>$${item.price}</span>
      </article>
    `,
    layout: carousel(),
  },
})

relatedProducts

Show related products based on objectID:
import { relatedProducts } from 'instantsearch.js/es/widgets';

relatedProducts({
  container: '#related-products',
  objectIDs: ['product-123'],
  limit: 4,
  templates: {
    item: (item, { html }) => html`
      <article>
        <img src="${item.image}" />
        <h3>${item.name}</h3>
      </article>
    `,
  },
})

frequentlyBoughtTogether

Show products frequently bought together:
import { frequentlyBoughtTogether } from 'instantsearch.js/es/widgets';

frequentlyBoughtTogether({
  container: '#frequently-bought-together',
  objectIDs: ['product-123'],
  limit: 4,
  templates: {
    item: (item, { html }) => html`
      <article>
        <img src="${item.image}" />
        <h3>${item.name}</h3>
      </article>
    `,
  },
})

Next Steps

Customization

Customize templates and create custom widgets

Styling

Style your search interface with CSS

Routing

Synchronize search state with URLs

Custom Widgets

Build your own widgets with connectors

Build docs developers (and LLMs) love