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.

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

The name of the attribute in the records.

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

How many facet values to retrieve.

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.

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

How many facet values to retrieve when showing more.

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.

1
2
3
4
instantsearch.widgets.menu({
  // ...
  sortBy: ['count:desc', 'name:asc'],
});
templates
type: object
Optional

The templates to use for the widget.

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

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

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

How many facet values to retrieve.

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

How many facet values to retrieve when showing more.

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.

1
2
3
4
customMenu({
  // ...
  sortBy: ['count:desc', 'name:asc'],
});
transformItems
type: function
Optional

A function to transform the items passed to the templates.

1
2
3
4
5
6
7
8
9
customMenu({
  // ...
  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
// 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?