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.

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

The name of the attributes to generate the menu with.

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

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.

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

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

The level separator used in the records.

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.

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.

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.

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

The templates to use for the widget.

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

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

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.

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.

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: ["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.

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.

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

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 = 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?