UI libraries / InstantSearch.js / Widgets
Signature
hierarchicalMenu({
  container: string|HTMLElement,
  attributes: string[],
  // Optional parameters
  limit: number,
  showMore: boolean,
  showMoreLimit: number,
  separator: string,
  rootPath: string,
  showParentLevel: boolean,
  sortBy: string[]|function,
  templates: object,
  cssClasses: object,
  transformItems: function,
});
Import
1
import { hierarchicalMenu } from 'instantsearch.js/es/widgets';

About this widget

The hierarchicalMenu widget is used to create a navigation menu based on a hierarchy of facet attributes. It’s commonly used for categories with subcategories.

By default, the count of the refined root level is updated to match the count of the actively refined parent level. You can choose to keep the root level count intact by setting persistHierarchicalRootCount in instantsearch.

Requirements

The objects to use in the hierarchical menu must follow this structure:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[
  {
    "objectID": "321432",
    "name": "lemon",
    "categories": {
      "lvl0": "products",
      "lvl1": "products > fruits",
  },
  {
    "objectID": "8976987",
    "name": "orange",
    "categories": {
      "lvl0": "products",
      "lvl1": "products > fruits",
    }
  } 
]

You can also provide more than one path for each level:

1
2
3
4
5
6
7
8
9
10
[
  {
    "objectID": "321432",
    "name": "lemon",
     "categories": {
       "lvl0": ["products", "goods"],
       "lvl1": ["products > fruits", "goods > to eat"]
     } 
  }
]

The attributes provided to the widget must be in attributes for faceting, either on the dashboard) or using attributesForFaceting with the API. By default, the separator is > (with spaces) but you can use a different one by using the separator option.

Examples

1
2
3
4
5
6
7
8
9
hierarchicalMenu({
  container: '#hierarchical-menu',
  attributes: [
    'categories.lvl0',
    'categories.lvl1',
    'categories.lvl2',
    'categories.lvl3',
  ],
});

Options

Parameter Description
container
type: string|HTMLElement
Required

The CSS Selector or HTMLElement to insert the widget into.

1
2
3
4
hierarchicalMenu({
  // ...
  container: '#hierarchical-menu',
});
attributes
type: string[]
Required

The name of the attributes to generate the menu with.

To avoid unexpected behavior, you can’t use the same attribute prop in a different type of widget.

1
2
3
4
5
6
7
8
9
hierarchicalMenu({
  // ...
  attributes: [
    'categories.lvl0',
    'categories.lvl1',
    'categories.lvl2',
    'categories.lvl3',
  ],
});
limit
type: number
default: 10
Optional

How many facet values to retrieve. When you enable the showMore feature, this is the number of facet values to display before clicking the “Show more” button.

1
2
3
4
hierarchicalMenu({
  // ...
  limit: 5,
});
showMore
type: boolean
default: false
Optional

Whether to display a button that expands the number of items.

1
2
3
4
hierarchicalMenu({
  // ...
  showMore: true,
});
showMoreLimit
type: number
Optional

The maximum number of displayed items (only used when showMore is set to true).

1
2
3
4
hierarchicalMenu({
  // ...
  showMoreLimit: 20,
});
separator
type: string
default: >
Optional

The level separator used in the records.

1
2
3
4
hierarchicalMenu({
  // ...
  separator: ' / ',
});
rootPath
type: string
default: null
Optional

The prefix path to use if the first level is not the root level.

Make sure to also include the root path in your UI state—for example, by setting initialUiState or calling setUiState().

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
instantsearch({
  // ...
  initialUiState: {
    YourIndexName: {
      hierarchicalMenu: {
        'categories.lvl0': ['Computers & Tablets'],
      },
    },
  },
}).addWidgets([
  hierarchicalMenu({
    // ...
    rootPath: 'Computers & Tablets',
  }),
]);
showParentLevel
type: boolean
default: true
Optional

Whether to show the siblings of the selected parent level of the current refined value.

This option doesn’t impact the root level. All root items are always visible.

1
2
3
4
hierarchicalMenu({
  // ...
  showParentLevel: false,
});
sortBy
type: string[]|function
default: Uses facetOrdering if set, ["name:asc"]
Optional

How to sort refinements. Must be one or more of the following strings:

  • "count" (same as "count:desc")
  • "count:asc"
  • "count:desc"
  • "name" (same as "name:asc")
  • "name:asc"
  • "name:desc"
  • "isRefined" (same as "isRefined:asc")
  • "isRefined:asc"
  • "isRefined:desc"

You can also give a function, which receives items two by two, like JavaScript’s Array.sort.

1
2
3
4
hierarchicalMenu({
  // ...
  sortBy: ['isRefined'],
});
templates
type: object
Optional

The templates to use for the widget.

1
2
3
4
5
6
hierarchicalMenu({
  // ...
  templates: {
    // ...
  },
});
cssClasses
type: object
default: {}
Optional

The CSS classes you can override:

  • root: the root element of the widget.
  • noRefinementRoot: the root element if there are no refinements.
  • list: the list of results.
  • childList: the child list element.
  • item: the list items.
  • selectedItem: the selected item of the list.
  • parentItem: the parent item of the list.
  • link: the link of each item.
  • selectedItemLink: the link of each selected item.
  • label: the label of each item.
  • count: the count of each item.
  • showMore: the “Show more” button.
  • disabledShowMore: the disabled “Show more” button.
1
2
3
4
5
6
7
8
9
10
hierarchicalMenu({
  // ...
  cssClasses: {
    root: 'MyCustomHierarchicalMenu',
    list: [
      'MyCustomHierarchicalMenuList',
      'MyCustomHierarchicalMenuList--subclass',
    ],
  },
});
transformItems
type: function
default: items => items
Optional

Receives the items and is called before displaying them. Should return a new array with the same shape as the original array. Useful for transforming, removing, or reordering items.

In addition, the full results data is available, which includes all regular response parameters, as well as parameters from the helper (for example disjunctiveFacetsRefinements).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
hierarchicalMenu({
  // ...
  transformItems(items) {
    return items.map(item => ({
      ...item,
      label: item.label.toUpperCase(),
    }));
  },
});

/* or, combined with results */
hierarchicalMenu({
  // ...
  transformItems(items, { results }) {
    return items.map(item => ({
      ...item,
      label: item.isRefined
        ? `${item.label} (page ${results.page + 1}/${results.nbPages})`
        : item.label,
    }));
  },
});

Templates

You can customize parts of the widget’s UI using the Templates API.

Every template provides an html function you can use as a tagged template. Using html lets you safely provide templates as an HTML string. It works directly in the browser without a build step. See Templating your UI for more information.

The html function is available starting from v4.46.0.

Parameter Description
item
type: string|function
Optional

The template for each item. It exposes:

  • label: string: the label of the item.
  • value: string: the value of the item.
  • count: number: the number of results matching the value.
  • isRefined: boolean: whether the item is selected.
  • url: string: the URL with the applied refinement.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
hierarchicalMenu({
  // ...
  templates: {
    item(data, { html }) {
      return html`
        <a class="${data.cssClasses.link}" href="${data.url}">
          <span class="${data.cssClasses.label}">${data.label}</span>
          <span class="${data.cssClasses.count}">
            ${data.count.toLocaleString()}
          </span>
        </a>
      `;
    },
  },
});
showMoreText
type: string|function
Optional

The template for the “Show more” button text. It exposes:

  • isShowingMore: boolean: whether the list is expanded.
1
2
3
4
5
6
7
8
hierarchicalMenu({
  // ...
  templates: {
    showMoreText(data, { html }) {
      return html`<span>${data.isShowingMore ? 'Show less' : 'Show more'}</span>`;
    },
  },
});

HTML output

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
<div class="ais-HierarchicalMenu">
  <ul class="ais-HierarchicalMenu-list ais-HierarchicalMenu-list--lvl0">
    <li class="ais-HierarchicalMenu-item ais-HierarchicalMenu-item--parent ais-HierarchicalMenu-item--selected">
      <a class="ais-HierarchicalMenu-link ais-HierarchicalMenu-link--selected" href="#">
        <span class="ais-HierarchicalMenu-label">Appliances</span>
        <span class="ais-HierarchicalMenu-count">4,306</span>
      </a>
      <ul class="ais-HierarchicalMenu-list ais-HierarchicalMenu-list--child ais-HierarchicalMenu-list--lvl1">
        <li class="ais-HierarchicalMenu-item ais-HierarchicalMenu-item--parent">
          <a class="ais-HierarchicalMenu-link" href="#">
            <span class="ais-HierarchicalMenu-label">Dishwashers</span>
            <span class="ais-HierarchicalMenu-count">181</span>
          </a>
        </li>
        <li class="ais-HierarchicalMenu-item">
          <a class="ais-HierarchicalMenu-link" href="#">
            <span class="ais-HierarchicalMenu-label">Fans</span>
            <span class="ais-HierarchicalMenu-count">91</span>
          </a>
        </li>
      </ul>
    </li>
    <li class="ais-HierarchicalMenu-item ais-HierarchicalMenu-item--parent">
      <a class="ais-HierarchicalMenu-link" href="#">
        <span class="ais-HierarchicalMenu-label">Audio</span>
        <span class="ais-HierarchicalMenu-count">1,570</span>
      </a>
    </li>
  </ul>
  <button class="ais-HierarchicalMenu-showMore">Show more</button>
</div>

Customize the UI with connectHierarchicalMenu

If you want to create your own UI of the hierarchicalMenu widget, you can use connectors.

To use connectHierarchicalMenu, you can import it with the declaration relevant to how you installed InstantSearch.js.

1
import { connectHierarchicalMenu } from 'instantsearch.js/es/connectors';

Then it’s a 3-step process:

// 1. Create a render function
const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  // Rendering logic
};

// 2. Create the custom widget
const customHierarchicalMenu = connectHierarchicalMenu(
  renderHierarchicalMenu
);

// 3. Instantiate
search.addWidgets([
  customHierarchicalMenu({
    // instance params
  })
]);

Create a render function

This rendering function is called before the first search (init lifecycle step) and each time results come back from Algolia (render lifecycle step).

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const {
    object[] items,
    boolean isShowingMore,
    boolean canToggleShowMore,
    boolean canRefine,
    function refine,
    function sendEvent,
    function toggleShowMore,
    function createURL,
    object widgetParams,
  } = renderOptions;

  if (isFirstRender) {
    // Do some initial rendering and bind events
  }

  // Render the widget
}

If SEO is critical to your search page, your custom HTML markup needs to be parsable:

  • use plain <a> tags with href attributes for search engines bots to follow them,
  • use semantic markup with structured data when relevant, and test it.

Refer to our SEO checklist for building SEO-ready search experiences.

Rendering options

Parameter Description
items
type: object[]

The list of available items, with each item:

  • label: string: the label of the item.
  • value: string: the value of the item.
  • count: number: the number results matching this value.
  • isRefined: boolean: whether the item is selected.
  • data: object[]|null: the list of children for the current item.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const renderList = items => `
  <ul>
    ${items
      .map(
        item => `
          <li>
            <a href="#">${item.label} (${item.count})</a>
            ${item.data ? renderList(item.data) : ''}
          </li>
        `
      )
      .join('')}
  </ul>
`;

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { items } = renderOptions;

  const children = renderList(items);

  document.querySelector('#hierarchical-menu').innerHTML = children;
};
isShowingMore
type: boolean

Whether the list is expanded.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const renderList = items => `
  <ul>
    ${items
      .map(
        item => `
          <li>
            <a href="#">${item.label} (${item.count})</a>
            ${item.data ? renderList(item.data) : ''}
          </li>
        `
      )
      .join('')}
  </ul>
`;

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { items, isShowingMore } = renderOptions;

  document.querySelector('#hierarchical-menu').innerHTML = `
    ${renderList(items)}
    <button>${isShowingMore ? 'Show less' : 'Show more'}</button>
  `;
};
canToggleShowMore
type: boolean

Whether users can click the “Show more” button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const renderList = items => `
  <ul>
    ${items
      .map(
        item => `
          <li>
            <a href="#">${item.label} (${item.count})</a>
            ${item.data ? renderList(item.data) : ''}
          </li>
        `
      )
      .join('')}
  </ul>
`;

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { items, canToggleShowMore } = renderOptions;

  document.querySelector('#hierarchical-menu').innerHTML = `
    ${renderList(items)}
    <button ${!canToggleShowMore ? 'disabled' : ''}>Show more</button>
  `;
};
canRefine
type: boolean
Required

Indicates if search state can be refined.

1
2
3
4
5
6
7
8
const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { items, canRefine } = renderOptions;

  if (!canRefine) {
    document.querySelector('#hierarchical-menu').innerHTML = '';
    return;
  }
};
refine
type: function

Sets the path of the hierarchical filter and triggers a new search.

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
const renderList = items => `
  <ul>
    ${items
      .map(
        item => `
          <li>
            <a
              href="#"
              data-value="${item.value}"
              style="font-weight: ${item.isRefined ? 'bold' : ''}"
            >
              ${item.label} (${item.count})
            </a>
            ${item.data ? renderList(item.data) : ''}
          </li>
        `
      )
      .join('')}
  </ul>
`;

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { items, refine } = renderOptions;

  const container = document.querySelector('#hierarchical-menu');

  container.innerHTML = renderList(items);

  [...container.querySelectorAll('a')].forEach(element => {
    element.addEventListener('click', event => {
      event.preventDefault();
      refine(event.target.dataset.value);
    });
  });
};
sendEvent
type: (eventType, facetValue) => void

The function to send click events.

  • The view event is automatically sent when the facets are rendered.
  • The click event is automatically sent when refine is called.
  • You can learn more about the insights middleware.

  • eventType: 'click'
  • facetValue: string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// For example,
sendEvent('click', 'Laptop');

/*
  A payload like the following will be sent to the `insights` middleware.
  {
    eventType: 'click',
    insightsMethod: 'clickedFilters',
    payload: {
      eventName: 'Filter Applied',
      filters: ['category:"Laptop"'],
      index: '<index-name>',
    },
    widgetType: 'ais.hierarchicalMenu',
  }
*/
toggleShowMore
type: function

Toggles the number of displayed values between limit and showMoreLimit.

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
const renderList = items => `
  <ul>
    ${items
      .map(
        item => `
          <li>
            <a href="#">${item.label} (${item.count})</a>
            ${item.data ? renderList(item.data) : ''}
          </li>
        `
      )
      .join('')}
  </ul>
`;

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { items, isShowingMore, toggleShowMore } = renderOptions;

  const container = document.querySelector('#hierarchical-menu');

  if (isFirstRender) {
    const list = document.createElement('div');
    const button = document.createElement('button');

    button.addEventListener('click', () => {
      toggleShowMore();
    });

    container.appendChild(list);
    container.appendChild(button);
  }

  container.querySelector('div').innerHTML = renderList(items);
  container.querySelector('button').textContent = isShowingMore
    ? 'Show less'
    : 'Show more';
};
createURL
type: function

Generates a URL for the next 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
const renderList = ({ items, createURL }) => `
  <ul>
    ${items
      .map(
        item => `
          <li>
            <a href="${createURL(item.value)}">
              ${item.label} (${item.count})
            </a>
            ${item.data ? renderList({ items: item.data, createURL }) : ''}
          </li>
        `
      )
      .join('')}
  </ul>
`;

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { items, createURL } = renderOptions;

  const children = renderList({ items, createURL });

  document.querySelector('#hierarchical-menu').innerHTML = children;
};
widgetParams
type: object

All original widget options are forwarded to the render function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const { widgetParams } = renderOptions;

  widgetParams.container.innerHTML = '...';
};

// ...

search.addWidgets([
  customHierarchicalMenu({
    // ...
    container: document.querySelector('#hierarchical-menu'),
  })
]);

Create and instantiate the custom widget

We first create custom widgets from our rendering function, then we instantiate them. When doing that, there are two types of parameters you can give:

  • Instance parameters: they are predefined parameters that you can use to configure the behavior of Algolia.
  • Your own parameters: to make the custom widget generic.

Both instance and custom parameters are available in connector.widgetParams, inside the renderFunction.

const customHierarchicalMenu = connectHierarchicalMenu(
  renderHierarchicalMenu
);

search.addWidgets([
  customHierarchicalMenu({
    attributes: string[],
    // Optional parameters
    limit: number,
    showMoreLimit: number,
    separator: string,
    rootPath: string,
    showParentLevel: boolean,
    sortBy: string[]|function,
    transformItems: function,
  })
]);

Instance options

Parameter Description
attributes
type: string[]
Required

The name of the attributes to generate the menu with.

To avoid unexpected behavior, you can’t use the same attribute prop in a different type of widget.

1
2
3
4
5
6
7
8
customHierarchicalMenu({
  attributes: [
    'categories.lvl0',
    'categories.lvl1',
    'categories.lvl2',
    'categories.lvl3',
  ],
});
limit
type: number
default: 10
Optional

The number of facet values to retrieve. When isShowingMore is false, this is the number of facet values displayed before clicking the “Show more” button.

1
2
3
4
customHierarchicalMenu({
  // ...
  limit: 5,
});
showMoreLimit
type: number
Optional

The maximum number of displayed items (only used when the showMore feature is implemented).

1
2
3
4
customHierarchicalMenu({
  // ...
  showMoreLimit: 20,
});
separator
type: string
default: >
Optional

The level separator used in the records.

1
2
3
4
customHierarchicalMenu({
  // ...
  separator: ' / ',
});
rootPath
type: string
default: null
Optional

The prefix path to use if the first level is not the root level.

Make sure to also include the root path in your UI state—for example, by setting initialUiState or calling setUiState().

1
2
3
4
customHierarchicalMenu({
  // ...
  rootPath: 'Computers & Tablets',
});
showParentLevel
type: boolean
default: true
Optional

Whether to show the siblings of the selected parent level of the current refined value.

1
2
3
4
customHierarchicalMenu({
  // ...
  showParentLevel: false,
});
sortBy
type: string[]|function
default: Uses facetOrdering if set, ["name:asc"]
Optional

How to sort refinements. Must be one or more of the following strings:

  • "count:asc"
  • "count:desc"
  • "name:asc"
  • "name:desc"
  • "isRefined"

You can also give a function that receives items two by two, like JavaScript’s Array.sort

If facetOrdering is set for this facet in renderingContent, and no value for sortBy is passed to this widget, facetOrdering is used, and the default order as a fallback.

1
2
3
4
customHierarchicalMenu({
  // ...
  sortBy: ['isRefined'],
});
transformItems
type: function
default: items => items
Optional

Receives the items and is called before displaying them. Should return a new array with the same shape as the original array. This is helpful when transforming, removing, or reordering items.

In addition, the full results data is available, which includes all regular response parameters, as well as parameters from the helper (for example disjunctiveFacetsRefinements).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
customHierarchicalMenu({
  // ...
  transformItems(items) {
    return items.map(item => ({
      ...item,
      label: item.label.toUpperCase(),
    }));
  },
});

/* or, combined with results */
customHierarchicalMenu({
  // ...
  transformItems(items, { results }) {
    return items.map(item => ({
      ...item,
      label: item.isRefined
        ? `${item.label} (page ${results.page + 1}/${results.nbPages})`
        : item.label,
    }));
  },
});

Full example

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
// Create the render function
const renderList = ({ items, createURL }) => `
  <ul>
    ${items
      .map(
        item => `
          <li>
            <a
              href="${createURL(item.value)}"
              data-value="${item.value}"
              style="font-weight: ${item.isRefined ? 'bold' : ''}"
            >
              ${item.label} (${item.count})
            </a>
            ${item.data ? renderList({ items: item.data, createURL }) : ''}
          </li>
        `
      )
      .join('')}
  </ul>
`;

const renderHierarchicalMenu = (renderOptions, isFirstRender) => {
  const {
    items,
    isShowingMore,
    refine,
    toggleShowMore,
    createURL,
    widgetParams,
  } = renderOptions;

  if (isFirstRender) {
    const list = document.createElement('div');
    const button = document.createElement('button');

    button.addEventListener('click', () => {
      toggleShowMore();
    });

    widgetParams.container.appendChild(list);
    widgetParams.container.appendChild(button);
  }

  const children = renderList({ items, createURL });

  widgetParams.container.querySelector('div').innerHTML = children;
  widgetParams.container.querySelector('button').textContent = isShowingMore
    ? 'Show less'
    : 'Show more';

  [...widgetParams.container.querySelectorAll('a')].forEach(element => {
    element.addEventListener('click', event => {
      event.preventDefault();
      refine(event.target.dataset.value);
    });
  });
};

// Create the custom widget
const customHierarchicalMenu = connectHierarchicalMenu(
  renderHierarchicalMenu
);

// Instantiate the custom widget
search.addWidgets([
  customHierarchicalMenu({
    container: document.querySelector('#hierarchical-menu'),
    attributes: [
      'categories.lvl0',
      'categories.lvl1',
      'categories.lvl2',
      'categories.lvl3',
    ],
    limit: 5,
    showMoreLimit: 10,
  })
]);
Did you find this page helpful?