API Reference / InstantSearch.js Widgets / menu
Apr. 24, 2019
Widget signature
instantsearch.widgets.menu({
  container: string|HTMLElement,
  attribute: string,
  // Optional parameters
  limit: number,
  showMore: boolean,
  showMoreLimit: number,
  sortBy: string[]|function,
  templates: object,
  cssClasses: object,
  transformItems: function,
});

About this widget #

The menu widget displays a menu that lets the user choose a single value for a specific attribute.

Requirements#

The attribute provided to the widget must be added in attributes for faceting, either on the dashboard or using attributesForFaceting with the API.

Examples #

1
2
3
4
instantsearch.widgets.menu({
  container: '#menu',
  attribute: 'categories',
});

Options #

container #
type: string|HTMLElement
Required

The CSS Selector of the DOM element inside which the widget is inserted.

Edit
1
2
3
4
instantsearch.widgets.menu({
  // ...
  container: '#menu',
});
attribute #
type: string
Required

The name of the attribute in the records.

Edit
1
2
3
4
instantsearch.widgets.menu({
  // ...
  attribute: 'categories',
});
limit #
type: number
default: 10
Optional

How many facet values to retrieve.

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

Limits the number of results and display a showMore button.

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

How many facet values to retrieve when showing more.

Edit
1
2
3
4
instantsearch.widgets.menu({
  // ...
  showMoreLimit: 30,
});
sortBy #
type: string[]|function
default: ["isRefined", "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.menu({
  // ...
  sortBy: ['count:desc', 'name:asc'],
});
templates #
type: object
Optional

The templates to use for the widget.

Edit
1
2
3
4
5
6
instantsearch.widgets.menu({
  // ...
  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.
  • item: the list items. They contain the link and separator.
  • selectedItem: the selected item in the list. This is the last one, or the root one if there are no refinements.
  • link: the link element of each item.
  • label: the label element of each item.
  • count: the count element of each item.
  • showMore: the “Show more” button.
  • disabledShowMore: the “Show more” button when disabled.
Edit
1
2
3
4
5
6
7
8
9
10
instantsearch.widgets.menu({
  // ...
  cssClasses: {
    root: 'MyCustomMenu',
    list: [
      'MyCustomMenuList',
      'MyCustomMenuList--sub-class',
    ],
  },
});
transformItems #
type: function
default: x => x
Optional

A function to transform the items passed to the templates.

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

Templates #

item #
type: string|function
Optional

Item template. It exposes:

  • count: the number of occurrences of the facet in the result set.
  • isRefined: returns true if the value is selected.
  • label: the label to display.
  • value: the value used for refining.
  • url: the URL with the selected refinement.
  • cssClasses: an object containing all the computed classes for the item.
Edit
1
2
3
4
5
6
7
8
9
10
11
12
13
instantsearch.widgets.menu({
  // ...
  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.menu({
  // ...
  templates: {
    showMoreText: `
      {{#isShowingMore}}
        Show less
      {{/isShowingMore}}
      {{^isShowingMore}}
        Show more
      {{/isShowingMore}}
    `,
  },
});

Customize the UI - connectMenu#

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

This connector is also used to build other widgets: MenuSelect

It’s a 3-step process:

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

// 2. Create the custom widget
const customMenu = instantsearch.connectors.connectMenu(
  renderMenu
);

// 3. Instantiate
search.addWidget(
  customMenu({
    // Widget parameters
  })
);

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 renderMenu = (renderOptions, isFirstRender) => {
  const {
    object[] items,
    function refine,
    function createURL,
    boolean isShowingMore,
    boolean canToggleShowMore,
    function toggleShowMore,
    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 elements that can be refined for the current search results. With each item:

  • value: string: the value of the menu item.
  • label: string: the label of the menu item.
  • count: number: the number of results matched after a refinement is applied.
  • isRefined: boolean: indicates if the refinement is applied.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const renderMenu = (renderOptions, isFirstRender) => {
  const { items } = renderOptions;

  document.querySelector('#menu').innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <a href="#">
                ${item.label} (${item.count})
              </a>
            </li>
          `
        )
        .join('')}
    </ul>
  `;
};
refine #
type: function

A function to toggle a refinement.

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
const renderMenu = (renderOptions, isFirstRender) => {
  const { items, refine } = renderOptions;

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

  container.innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <a
                href="#"
                data-value="${item.value}"
                style="font-weight: ${item.isRefined ? 'bold' : ''}"
              >
                ${item.label} (${item.count})
              </a>
            </li>
          `
        )
        .join('')}
    </ul>
  `;

  [...container.querySelectorAll('a')].forEach(element => {
    element.addEventListener('click', event => {
      event.preventDefault();
      refine(event.currentTarget.dataset.value);
    });
  });
};
createURL #
type: function
default: (item.value) => string

Generates a URL for the corresponding search state.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const renderMenu = (renderOptions, isFirstRender) => {
  const { items, createURL } = renderOptions;

  document.querySelector('#menu').innerHTML = `
    <ul>
      ${items
        .map(
          item => `
            <li>
              <a
                href="${createURL(item.value)}"
                style="font-weight: ${item.isRefined ? 'bold' : ''}"
              >
                ${item.label} (${item.count})
              </a>
            </li>
          `
        )
        .join('')}
    </ul>
  `;
};
isShowingMore #
type: boolean

Returns true if the menu is displaying all the menu items.

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
const renderMenu = (renderOptions, isFirstRender) => {
  const { items, isShowingMore, toggleShowMore } = renderOptions;

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

  if (isFirstRender) {
    const ul = document.createElement('ul');
    const button = document.createElement('button');
    button.textContent = 'Show more';

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

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

  container.querySelector('ul').innerHTML = items
    .map(
      item => `
        <li>
          <a href="#">
            ${item.label} (${item.count})
          </a>
        </li>
      `
    )
    .join('');

  container.querySelector('button').textContent = isShowingMore
    ? 'Show less'
    : 'Show more';
};
canToggleShowMore #
type: boolean

Returns true if the “Show more” button can be activated (enough items to display more and not already displaying more than limit items).

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
const renderMenu = (renderOptions, isFirstRender) => {
  const { items, canToggleShowMore, toggleShowMore } = renderOptions;

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

  if (isFirstRender) {
    const ul = document.createElement('ul');
    const button = document.createElement('button');
    button.textContent = 'Show more';

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

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

  container.querySelector('ul').innerHTML = items
    .map(
      item => `
        <li>
          <a href="#">
            ${item.label} (${item.count})
          </a>
        </li>
      `
    )
    .join('');

  container.querySelector('button').disabled = !canToggleShowMore;
};
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
const renderMenu = (renderOptions, isFirstRender) => {
  const { items, toggleShowMore } = renderOptions;

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

  if (isFirstRender) {
    const ul = document.createElement('ul');
    const button = document.createElement('button');
    button.textContent = 'Show more';

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

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

  container.querySelector('ul').innerHTML = items
    .map(
      item => `
        <li>
          <a href="#">
            ${item.label} (${item.count})
          </a>
        </li>
      `
    )
    .join('');
};
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 renderMenu = (renderOptions, isFirstRender) => {
  const { widgetParams } = renderOptions;

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

// ...

search.addWidget(
  customMenu({
    // ...
    container: document.querySelector('#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 customMenu = instantsearch.connectors.connectMenu(
  renderMenu
);

search.addWidget(
  customMenu({
    attribute: string,
    // Optional instance params
    limit: number,
    showMoreLimit: number,
    sortBy: string[]|function,
    transformItems: function,
  })
);

Instance options #

attribute #
type: string
Required

The name of the attribute for faceting.

Edit
1
2
3
customMenu({
  attribute: 'categories',
});
limit #
type: number
default: 10
Optional

How many facet values to retrieve.

Edit
1
2
3
4
customMenu({
  // ...
  limit: 20,
});
showMoreLimit #
type: number
default: 10
Optional

How many facet values to retrieve when showing more.

Edit
1
2
3
4
customMenu({
  // ...
  showMoreLimit: 20,
});
sortBy #
type: string[]|function
default: ["isRefined", "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
customMenu({
  // ...
  sortBy: ['count:desc', 'name:asc'],
});
transformItems #
type: function
Optional

A function to transform the items passed to the templates.

Edit
1
2
3
4
5
6
7
8
9
customMenu({
  // ...
  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
// 1. Create a render function
const renderMenu = (renderOptions, isFirstRender) => {
  const {
    items,
    refine,
    createURL,
    isShowingMore,
    canToggleShowMore,
    toggleShowMore,
    widgetParams,
  } = renderOptions;

  if (isFirstRender) {
    const ul = document.createElement('ul');
    const button = document.createElement('button');
    button.textContent = 'Show more';

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

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

  widgetParams.container.querySelector('ul').innerHTML = items
    .map(
      item => `
        <li>
          <a
            href="${createURL(item.value)}"
            data-value="${item.value}"
            style="font-weight: ${item.isRefined ? 'bold' : ''}"
          >
            ${item.label} (${item.count})
          </a>
        </li>
      `
    )
    .join('');

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

  const button = widgetParams.container.querySelector('button');

  button.disabled = !canToggleShowMore;
  button.textContent = isShowingMore ? 'Show less' : 'Show more';
};

// 2. Create the custom widget
const customMenu = instantsearch.connectors.connectMenu(
  renderMenu
);

// 3. Instantiate
search.addWidget(
  customMenu({
    container: document.querySelector('#menu'),
    attribute: 'categories',
    showMoreLimit: 20,
  })
);

HTML output#

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<div class="ais-Menu">
  <div class="ais-Menu-searchBox">
    <!-- SearchBox widget here -->
  </div>
  <ul class="ais-Menu-list">
    <li class="ais-Menu-item ais-Menu-item--selected">
      <a class="ais-Menu-link" href="#">
        <span class="ais-Menu-label">Appliances</span>
        <span class="ais-Menu-count">4,306</span>
      </a>
    </li>
    <li class="ais-Menu-item">
      <a class="ais-Menu-link" href="#">
        <span class="ais-Menu-label">Audio</span>
        <span class="ais-Menu-count">1,570</span>
      </a>
    </li>
  </ul>
  <button class="ais-Menu-showMore">Show more</button>
</div>

Did you find this page helpful?