API Reference / InstantSearch.js Widgets / hierarchicalMenu
Apr. 24, 2019

hierarchicalMenu

Widget signature
instantsearch.widgets.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,
});

About this widget #

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

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
[
  {
    "objectID": "321432",
    "name": "lemon",
    "categories.lvl0": "products",
    "categories.lvl1": "products > fruits"
  },
  {
    "objectID": "8976987",
    "name": "orange",
    "categories.lvl0": "products",
    "categories.lvl1": "products > fruits"
  }
]

It’s also possible to provide more than one path for each level:

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

The attributes provided to the widget must be added in attributes for faceting, either on the dashboard or using attributesForFaceting with the API. By default, the separator we expect 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
instantsearch.widgets.hierarchicalMenu({
  container: '#hierarchical-menu',
  attributes: [
    'hierarchicalCategories.lvl0',
    'hierarchicalCategories.lvl1',
    'hierarchicalCategories.lvl2',
    'hierarchicalCategories.lvl3',
  ],
});

Options #

container #
type: string|HTMLElement
Required

The CSS Selector or HTMLElement to insert the widget into.

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

The name of the attributes to generate the menu with.

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

How many facet values to retrieve. When the showMore feature is active this is the minimum number of requested facets (the “Show more” button is inactive).

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

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

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

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

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

The level separator used in the records.

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

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

Edit
1
2
3
4
instantsearch.widgets.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.

Edit
1
2
3
4
instantsearch.widgets.hierarchicalMenu({
  // ...
  showParentLevel: false,
});
sortBy #
type: string[]|function
default: ["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"

It’s also possible to give a function, which receives items two by two, like JavaScript’s Array.sort.

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

The templates to use for the widget.

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

The CSS classes to 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.
  • label: the label of each item.
  • count: the count of each item.
  • showMore: the “Show more” button.
  • disabledShowMore: the disabled “Show more” button.
Edit
1
2
3
4
5
6
7
8
9
10
instantsearch.widgets.hierarchicalMenu({
  // ...
  cssClasses: {
    root: 'MyCustomHierarchicalMenu',
    list: [
      'MyCustomHierarchicalMenuList',
      'MyCustomHierarchicalMenuList--subclass',
    ],
  },
});
transformItems #
type: function
default: x => x
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 mapping over the items to transform, and remove or reorder them.

Edit
1
2
3
4
5
6
7
8
9
instantsearch.widgets.hierarchicalMenu({
  // ...
  transformItems(items) {
    return items.map(item => ({
      ...item,
      label: item.label.toUpperCase(),
    }));
  },
});

Templates #

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 or not the item is selected.
  • url: string: the URL with the applied refinement.
Edit
1
2
3
4
5
6
7
8
9
10
11
12
13
instantsearch.widgets.hierarchicalMenu({
  // ...
  templates: {
    item: `
      <a class="{{cssClasses.link}}" href="{{url}}">
        <span class="{{cssClasses.label}}">{{label}}</span>
        <span class="{{cssClasses.count}}">
          {{#helpers.formatNumber}}{{count}}{{/helpers.formatNumber}}
        </span>
      </a>
    `,
  },
});
showMoreText #
type: string|function
Optional

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

  • isShowingMore: boolean: whether or not the list is expanded.
Edit
1
2
3
4
5
6
7
8
9
10
11
12
13
instantsearch.widgets.hierarchicalMenu({
  // ...
  templates: {
    showMoreText: `
      {{#isShowingMore}}
        Show less
      {{/isShowingMore}}
      {{^isShowingMore}}
        Show more
      {{/isShowingMore}}
    `,
  },
});

Customize the UI - connectHierarchicalMenu#

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

It’s a 3-step process:

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

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

// 3. Instantiate
search.addWidget(
  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,
    function refine,
    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 #

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 or not 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 or not 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 or not the “Show more” button can be clicked.

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>
  `;
};
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);
    });
  });
};
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 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.addWidget(
  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 = instantsearch.connectors.connectHierarchicalMenu(
  renderHierarchicalMenu
);

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

Instance options #

attributes #
type: string[]
Required

The name of the attributes to generate the menu with.

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

The minimum number of facet values to retrieve.

Edit
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).

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

The level separator used in the records.

Edit
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.

Edit
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.

Edit
1
2
3
4
customHierarchicalMenu({
  // ...
  showParentLevel: false,
});
sortBy #
type: string[]|function
default: ["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"

It’s also possible to give a function, which receives items two by two, like JavaScript’s Array.sort.

Edit
1
2
3
4
customHierarchicalMenu({
  // ...
  sortBy: ['isRefined'],
});
transformItems #
type: function
default: x => x
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 mapping over the items to transform, and remove or reorder them.

Edit
1
2
3
4
5
6
7
8
9
customHierarchicalMenu({
  // ...
  transformItems(items) {
    return items.map(item => ({
      ...item,
      label: item.label.toUpperCase(),
    }));
  },
});

Full example#

Edit
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 = instantsearch.connectors.connectHierarchicalMenu(
  renderHierarchicalMenu
);

// Instantiate the custom widget
search.addWidget(
  customHierarchicalMenu({
    container: document.querySelector('#hierarchical-menu'),
    attributes: [
      'hierarchicalCategories.lvl0',
      'hierarchicalCategories.lvl1',
      'hierarchicalCategories.lvl2',
      'hierarchicalCategories.lvl3',
    ],
    limit: 5,
    showMoreLimit: 10,
  })
);

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" 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>

Did you find this page helpful?