> ## Documentation Index
> Fetch the complete documentation index at: https://algolia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Create your own widgets

> Learn how you can create a custom widget for your InstantSearch.js app.

export const Facet = () => <Tooltip tip="An attribute in your records that lets users filter or group results (for example, by color, brand, or price)." cta="Faceting" href="/doc/guides/managing-results/refine-results/faceting">
    facet
  </Tooltip>;

export const Records = () => <Tooltip tip="A record is a searchable object in an Algolia index. Each record consists of named attributes." cta="Algolia records" href="/doc/guides/sending-and-managing-data/prepare-your-data#algolia-records">
    records
  </Tooltip>;

export const Filter = () => <Tooltip tip="A filter is a condition that limits which records Algolia returns. Filters often use one or more facet-value pairs, such as brand:Apple AND color:red. You can also filter by numeric values, dates, tags, booleans, or geographic constraints." cta="Filtering" href="/doc/guides/managing-results/refine-results/faceting">
    filter
  </Tooltip>;

<div className="not-prose algolia-flavor-switcher">
  <div className="afs-dropdown">
    <div className="afs-trigger" role="button" tabIndex="0" aria-haspopup="listbox">
      <span className="afs-current">JavaScript</span>

      <svg className="afs-chevron lucide lucide-chevron-down" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <path d="m6 9 6 6 6-6" />
      </svg>
    </div>

    <ul className="afs-menu" role="listbox">
      <li role="option" aria-selected="true"><a className="afs-option is-current" href="/doc/guides/building-search-ui/widgets/create-your-own-widgets/js"><span className="afs-option-name">JavaScript</span><span className="afs-option-lib">InstantSearch.js</span></a></li>
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/widgets/create-your-own-widgets/react"><span className="afs-option-name">React</span><span className="afs-option-lib">React InstantSearch</span></a></li>
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/widgets/create-your-own-widgets/vue"><span className="afs-option-name">Vue</span><span className="afs-option-lib">Vue InstantSearch</span></a></li>
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/widgets/create-your-own-widgets/ios"><span className="afs-option-name">iOS</span><span className="afs-option-lib">InstantSearch iOS</span></a></li>
      <li role="option" aria-selected="false"><a className="afs-option" href="/doc/guides/building-search-ui/widgets/create-your-own-widgets/android"><span className="afs-option-name">Android</span><span className="afs-option-lib">InstantSearch Android</span></a></li>
    </ul>
  </div>
</div>

InstantSearch.js comes with [multiple widgets](/doc/api-reference/widgets/js) that you can [extensively customize](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js).

You can also use connectors to completely change your rendering.

If no existing widget or connector suits your needs, you can create your own.
Creating custom widgets is the most advanced way to tailor your search experience.
It requires in-depth knowledge of InstantSearch and Algolia.

This guide covers how to build a custom widget:

* [Build a custom connector](#build-a-custom-connector)
* [Build a UI with a connector](#render-a-custom-user-interface)

<Info>
  If you're creating a custom InstantSearch widget because you didn't find a built-in option for your use case,
  consider [opening a feature request](https://github.com/algolia/instantsearch/discussions/new?category=ideas\&labels=triage\&title=Feature%20request%3A%20) to describe what you're trying to build.
</Info>

## When to create custom widgets

You can create a new widget **when none of the [existing widgets](/doc/api-reference/widgets/js) fit your functional needs.**
However, if you're trying to redefine the UI or DOM output of a widget,
[you should, instead, extend it by using its connector counterpart](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js#customize-the-complete-ui-of-the-widgets).

**Existing widgets and connectors should fit most of your use cases** and you should look into them before creating custom connectors.
For example, to create buttons that set predefined queries, you could use `useSearchBox()`.
Although you're not rendering a search box, the connector provides the necessary APIs for this, so there's no need to re-develop it.

**For help, [explain your situation and ask questions on GitHub](https://github.com/algolia/instantsearch/discussions/new?category=q-a\&labels=triage).**

If you're using TypeScript,
install the [Algolia search helper](/doc/guides/building-search-ui/upgrade-guides/js/#algolia-search-helper) as a development dependency to access the necessary types.

## Build a custom connector

When creating a custom widget, start by **writing a connector that encapsulates all the logic of your widget**,
yet keeps the rendering separate.

**This guide uses the example of a negative refinement list widget.**
It's similar to a [`RefinementList`](/doc/api-reference/widgets/refinement-list/js),
but instead of filtering on the selected items, it excludes them from the search.

For example, selecting the brand "Apple" would <Filter /> results to all matching <Records /> that aren't Apple products.

<img src="https://mintcdn.com/algolia/WOi5v-PGZrMZ2rOj/images/build-search-ui/negative-refinement-list.png?fit=max&auto=format&n=WOi5v-PGZrMZ2rOj&q=85&s=29f03f9f476cc8c7e2847d1babe43671" alt="Negative refinement list custom widget" width="181" height="320" data-path="images/build-search-ui/negative-refinement-list.png" />

### Write the connector function

Create a `connectNegativeRefinementList` function that takes a render and an unmount function.
It should return a `negativeRefinementList` function (the widget factory) that takes widget parameters and returns an object (the widget).

For the sake of simplicity, the only parameter that the widget accepts is `attribute`.
This lets users specify which record attribute to filter on.

<CodeGroup>
  ```js JavaScript theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList({ attribute }) {
      return {
        // …
      };
    };
  }

  const noop = () => {};
  ```

  ```ts TypeScript theme={"system"}
  import type { SearchResults } from "algoliasearch-helper";
  import type { Connector } from "instantsearch.js";

  export type NegativeRefinementListConnectorParams = {
    attribute: string;
  };

  type NegativeRefinementListRenderState = {
    items: SearchResults.FacetValue[];
    refine(value: string): void;
  };

  type NegativeRefinementListWidgetDescription = {
    $$type: "myOrganization.negativeRefinementList";
    renderState: NegativeRefinementListRenderState;
    indexRenderState: {
      negativeRefinementList: {
        [attribute: string]: NegativeRefinementListRenderState;
      };
    };
    indexUiState: {
      negativeRefinementList: {
        [attribute: string]: string[];
      };
    };
  };

  type NegativeRefinementListConnector = Connector<
    NegativeRefinementListWidgetDescription,
    NegativeRefinementListConnectorParams
  >;

  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList({ attribute }) {
      return {
        // …
      };
    };
  };

  const noop = () => {};
  ```
</CodeGroup>

Your custom connector needs an identifier.
The naming convention is `"myOrganization.myWidget"` (for example, `"microsoft.negativeRefinementList"`).
If you don't have an organization, you can use your name.

<CodeGroup>
  ```js JavaScript theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList({ attribute }) {
      return {
        $$type: "myOrganization.negativeRefinementList",
      };
    };
  }

  // …
  ```

  ```ts TypeScript theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList({ attribute }) {
      return {
        $$type: "myOrganization.negativeRefinementList",
      };
    };
  };

  // …
  ```
</CodeGroup>

## Compute the render state

At this point, the widget doesn't perform any custom logic.
To make it functional, you need to hook into the InstantSearch lifecycle to update search parameters, extract response data, and expose values for rendering.

#### Widget render state

<Steps>
  <Step title="Define the method">
    Implement the `getWidgetRenderState` method.
    This is where you consume data from the API response.

    It should return an object with the data and APIs you want to expose to the render function.

    For the negative refinement list, you need to expose:

    * The `items` to display in the list.
    * A `refine` function to trigger a new search from the UI with new items to exclude.

    <Info>
      The widget parameters are also passed under the `widgetParams` key. This is necessary for internal purposes.
    </Info>

    In the returned object from the `negativeRefinementList` function, add a function `getWidgetRenderState`.

    <CodeGroup>
      ```js JavaScript icon="code" theme={"system"}
      export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
        return function negativeRefinementList({ attribute }) {
          return {
            // …
            getWidgetRenderState({ results, helper }) {
              // …
            },
          };
        };
      }

      // …
      ```

      ```ts TypeScript icon="code" theme={"system"}
      export const connectNegativeRefinementList: NegativeRefinementListConnector = (
        renderFn,
        unmountFn = noop,
      ) => {
        return function negativeRefinementList({ attribute }) {
          return {
            // …
            getWidgetRenderState({ results, helper }) {
              // …
            },
          };
        };
      };

      // …
      ```
    </CodeGroup>
  </Step>

  <Step title="Initialize stable helper functions">
    You need to expose a `refine` function to toggle <Facet /> exclusions for the given attribute.

    **The `refine` function must keep the same reference whenever `getWidgetRenderState` is called** so that UI frameworks can consider it stable between renders.
    To do so, create a `connectorState` object outside the widget and attach the `refine` function to it.

    <CodeGroup>
      ```js JavaScript icon="code" theme={"system"}
      export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
        return function negativeRefinementList({ attribute }) {
          // An empty `connectorState` object is used to store information
          // that needs to be shared across multiple method calls.
          const connectorState = {};

          return {
            getWidgetRenderState({ results, helper }) {
              // To ensure `refine` keeps the same reference across renders, create
              // and store it once outside the method scope.
              if (!connectorState.refine) {
                connectorState.refine = (value) =>
                  helper.toggleFacetExclusion(attribute, value).search();
              }

              // …
            },
          };
        };
      }

      // …
      ```

      ```ts TypeScript icon="code" theme={"system"}
      export const connectNegativeRefinementList: NegativeRefinementListConnector = (
        renderFn,
        unmountFn = noop,
      ) => {
        return function negativeRefinementList({ attribute }) {
          type ConnectorState = {
            refine?(value: string): void;
          };

          // An empty `connectorState` object is used to store information
          // that needs to be shared across multiple method calls.
          const connectorState: ConnectorState = {};

          return {
            getWidgetRenderState({ results, helper }) {
              // To ensure `refine` keeps the same reference across renders, create
              // and store it once outside the method scope.
              if (!connectorState.refine) {
                connectorState.refine = (value) =>
                  helper.toggleFacetExclusion(attribute, value).search();
              }

              // …
            },
          };
        };
      };

      // …
      ```
    </CodeGroup>
  </Step>

  <Step title="Extract data from results">
    You need to be able to consume the facet values from the API response so you can display them on the UI,
    let users click them, and so on.

    Find them on the exposed `results` and store them in an `items` variable.

    <CodeGroup>
      ```js JavaScript icon="code" theme={"system"}
      export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
        return function negativeRefinementList({ attribute }) {
          // …

          return {
            getWidgetRenderState({ results, helper }) {
              // …

              // Retrieve the facet values for the given attribute and sort them by
              // ascending name. Store the facet values in the `items` variable.
              const items =
                results.getFacetValues(attribute, {
                  sortBy: ["name:asc"],
                }) || [];

              // …
            },
          };
        };
      }

      // …
      ```

      ```ts TypeScript icon="code" theme={"system"}
      export const connectNegativeRefinementList: NegativeRefinementListConnector = (
        renderFn,
        unmountFn = noop,
      ) => {
        return function negativeRefinementList({ attribute }) {
          // …

          return {
            getWidgetRenderState({ results, helper }) {
              // …

              // Retrieve facet values from the results for the given attribute
              // and sort them by ascending name.
              // Later on, you could let users pass a `sortBy` parameter.
              const items =
                (results.getFacetValues(attribute, {
                  sortBy: ["name:asc"],
                }) as SearchResults.FacetValue[]) || [];

              // …
            },
          };
        };
      };

      // …
      ```
    </CodeGroup>
  </Step>

  <Step title="Handle no results">
    You need to handle what happens when there are no search results.

    <CodeGroup>
      ```js JavaScript icon="code" theme={"system"}
      export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
        return function negativeRefinementList(widgetParams) {
          // …

          return {
            getWidgetRenderState({ results, helper }) {
              // …

              // When there are no results, return the API with default values.
              // This shows a fallback UI while waiting for results.
              if (!results) {
                return { items: [], refine: connectorState.refine, widgetParams };
              }

              // …
            },
          };
        };
      }

      // …
      ```

      ```ts TypeScript icon="code" theme={"system"}
      export const connectNegativeRefinementList: NegativeRefinementListConnector = (
        renderFn,
        unmountFn = noop,
      ) => {
        return function negativeRefinementList(widgetParams) {
          // …

          return {
            getWidgetRenderState({ results, helper }) {
              // …

              // When there are no results, return the API with default values.
              // It's helpful to render a default UI until results are available.
              if (!results) {
                return { items: [], refine: connectorState.refine, widgetParams };
              }

              // …
            },
          };
        };
      };

      // …
      ```
    </CodeGroup>
  </Step>

  <Step title="Return the render state">
    Return the `items` containing the facet values and the `refine` function.
    You should also return the parameters passed to the widget under the `widgetParams` key.

    You can use all this data and APIs in the render function later.

    <CodeGroup>
      ```js JavaScript icon="code" theme={"system"}
      export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
        return function negativeRefinementList(widgetParams) {
          // …

          return {
            getWidgetRenderState({ results, helper }) {
              // …

              return {
                items,
                // Toggles exclusion of a facet value when selected.
                // If the value is already excluded, the exclusion is unset.
                // Otherwise, it's added to the exclusion list.
                // Then, a search is triggered.
                refine: connectorState.refine,
                widgetParams,
              };
            },
          };
        };
      }

      // …
      ```

      ```ts TypeScript icon="code" theme={"system"}
      export const connectNegativeRefinementList: NegativeRefinementListConnector = (
        renderFn,
        unmountFn = noop,
      ) => {
        return function negativeRefinementList(widgetParams) {
          // …

          return {
            getWidgetRenderState({ results, helper }) {
              // …

              return {
                items,
                // A function to toggle a value when selected.
                // If the value is already excluded, the exclusion is unset.
                // Otherwise, it's added to the exclusion list.
                // Then, a search is triggered.
                refine: connectorState.refine,
                widgetParams,
              };
            },
          };
        };
      };

      // …
      ```
    </CodeGroup>
  </Step>
</Steps>

#### Global render state

In InstantSearch, each widget you add registers its render state in one global object.
You need to specify how to store your widget render state in this global tree by implementing the `getRenderState` method.

You might use multiple negative refinement lists in your application but with different attributes.
For example,
you might want to exclude by brand and by categories.
Here, you want to store each widget's render state individually so they don't override each other.

<CodeGroup>
  ```js JavaScript icon="code" theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList(widgetParams) {
      const { attribute } = widgetParams;

      return {
        // …
        getRenderState(renderState, renderOptions) {
          // The global render state is merged with a new one to store the render
          // state of the current widget.
          return {
            ...renderState,
            negativeRefinementList: {
              ...renderState.negativeRefinementList,
              // You can use multiple `negativeRefinementList` widgets in a single
              // app so you need to register each of them separately.
              // Each `negativeRefinementList` widget's render state is stored
              // by the `attribute` it affects.
              [attribute]: this.getWidgetRenderState(renderOptions),
            },
          };
        },
      };
    };
  }

  // …
  ```

  ```ts TypeScript icon="code" theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList(widgetParams) {
      const { attribute } = widgetParams;

      return {
        // …
        getRenderState(renderState, renderOptions) {
          // The global render state is merged with a new one to store the render
          // state of the current widget.
          return {
            ...renderState,
            negativeRefinementList: {
              ...renderState.negativeRefinementList,
              // You can use multiple `negativeRefinementList` widgets in a single
              // app so you need to register each of them separately.
              // Each `negativeRefinementList` widget's render state is stored
              // by the `attribute` it affects.
              [attribute]: this.getWidgetRenderState(renderOptions),
            },
          };
        },
      };
    };
  };

  // …
  ```
</CodeGroup>

### Set up the lifecycle

When you add InstantSearch widgets to your app, they go through several steps in response to internal events.
**These steps are the InstantSearch lifecycle.**

You must register lifecycle hooks on your widget to run code at the `init`, `render`, and `dispose` stages.
Use these functions to call the user-provided render and unmount functions with the correct information.

<Info>
  The lifecycle code shouldn't change, so you can copy the code from this step without modifying it.
  Most of the custom logic happens in `getWidgetRenderState`.
</Info>

The `init` step runs when the app starts (before the initial search is performed).
**Don't use this function to add Algolia-related logic**.
Instead, use `getWidgetSearchParameters`, `getWidgetUiState`, or `getWidgetRenderState`.

<CodeGroup>
  ```js JavaScript icon="code" theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList({ attribute }) {
      return {
        // …

        init(initOptions) {
          const { instantSearchInstance } = initOptions;

          renderFn(
            // The render state is the data provided to the render function,
            // necessary to build the UI.
            {
              ...this.getWidgetRenderState(initOptions),
              instantSearchInstance,
            },
            // Calling the function with `isFirstRender=true` lets you perform
            // conditional logic in the render function.
            true,
          );
        },

        // …
      };
    };
  }

  // …
  ```

  ```ts TypeScript icon="code" theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList({ attribute }) {
      return {
        // …

        init(initOptions) {
          const { instantSearchInstance } = initOptions;

          renderFn(
            // The render state is the data provided to the render function,
            // necessary to build the UI.
            {
              ...this.getWidgetRenderState(initOptions),
              instantSearchInstance,
            },
            // Calling the function with `isFirstRender=true` lets you perform
            // conditional logic in the render function.
            true,
          );
        },

        // …
      };
    };
  };

  // …
  ```
</CodeGroup>

The `render` step runs whenever new results come back from Algolia.
It's usually triggered by search state changes,
such as when a user submits a new query or clicks on a filter.

During this step, the widget can react to the updated search results by re-rendering with the new information.
This lets the widget remain synchronized with the current state of the search experience.

<CodeGroup>
  ```js JavaScript icon="code" theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList({ attribute }) {
      return {
        // …

        render(renderOptions) {
          const { instantSearchInstance } = renderOptions;

          renderFn(
            // The render state is the data provided to the render function,
            // necessary to build the UI.
            {
              ...this.getWidgetRenderState(renderOptions),
              instantSearchInstance,
            },
            // Calling the function with `isFirstRender=false` lets you perform
            // conditional logic in the render function.
            false,
          );
        },

        // …
      };
    };
  }

  // …
  ```

  ```ts TypeScript icon="code" theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList({ attribute }) {
      return {
        // …

        render(renderOptions) {
          const { instantSearchInstance } = renderOptions;

          renderFn(
            // The render state is the data provided to the render function,
            // necessary to build the UI.
            {
              ...this.getWidgetRenderState(renderOptions),
              instantSearchInstance,
            },
            // Calling the function with `isFirstRender=false` lets you perform
            // conditional logic in the render function.
            false,
          );
        },

        // …
      };
    };
  };

  // …
  ```
</CodeGroup>

The `dispose` step runs when removing the widget.
Use it to clean up anything the widget created during its "lifetime" such as search parameters, UI, and event listeners.
This helps prevent memory leaks and ensures the widget doesn't continue affecting the search experience once it's no longer in use.

<CodeGroup>
  ```js JavaScript icon="code" theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList({ attribute }) {
      return {
        // …

        dispose(disposeOptions) {
          unmountFn();
        },

        // …
      };
    };
  }

  // …
  ```

  ```ts TypeScript icon="code" theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList({ attribute }) {
      return {
        // …

        dispose(disposeOptions) {
          unmountFn();
        },

        // …
      };
    };
  };

  // …
  ```
</CodeGroup>

### Interact with routing

An important aspect of building an InstantSearch widget is how to make it work with [routing](/doc/guides/building-search-ui/going-further/routing-urls/js).
**Your custom widget should be able to synchronize its state with the browser URL** so you can share a link to your search experience in any given state.

#### Set the widget UI state

In InstantSearch, routing uses an internal [`uiState`](/doc/api-reference/widgets/ui-state/js) object to derive the route.
As with the [render state](#compute-the-render-state),
you need to specify how to store your widget UI state in the global UI state by implementing the `getWidgetUiState` method.

As with `getRenderState`, since you might use the widget multiple times with different attributes,
you need to store each widget's UI state individually so they don't override each other.

<CodeGroup>
  ```js JavaScript theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList(widgetParams) {
      const { attribute } = widgetParams;

      return {
        // …
        getWidgetUiState(uiState, { searchParameters }) {
          // The global UI state is merged with a new one to store the UI
          // state of the current widget.
          return {
            ...uiState,
            negativeRefinementList: {
              ...uiState.negativeRefinementList,
              // You can use multiple `negativeRefinementList` widgets in a single
              // app so you need to register each of them separately.
              // Each `negativeRefinementList` widget's UI state is stored by
              // the `attribute` it affects.
              [attribute]: searchParameters.getExcludeRefinements(attribute),
            },
          };
        },
      };
    };
  }

  // …
  ```

  ```ts TypeScript theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList(widgetParams) {
      const { attribute } = widgetParams;

      return {
        // …
        getWidgetUiState(uiState, { searchParameters }) {
          // The global UI state is merged with a new one to store the UI
          // state of the current widget.
          return {
            ...uiState,
            negativeRefinementList: {
              ...uiState.negativeRefinementList,
              // You can use multiple `negativeRefinementList` widgets in a single
              // app so you need to register each of them separately.
              // Each `negativeRefinementList` widget's UI state is stored by
              // the `attribute` it affects.
              [attribute]: searchParameters.getExcludeRefinements(attribute),
            },
          };
        },
      };
    };
  };

  // …
  ```
</CodeGroup>

#### Set the widget search parameters

When initializing InstantSearch state from a URL, you need to convert the URL into [search parameters](/doc/api-reference/search-api-parameters) to trigger the first search.

To define how to derive search parameters from the UI state, use the `getWidgetSearchParameters` method.
This method lets you modify the existing search parameters based on the widget's configuration and the current UI state.

<CodeGroup>
  ```js JavaScript theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList(widgetParams) {
      const { attribute } = widgetParams;

      return {
        // …
        getWidgetSearchParameters(searchParameters, { uiState }) {
          const state = searchParameters.addFacet(attribute);
          const values = uiState.negativeRefinementList?.[attribute];

          if (Array.isArray(values)) {
            return values.reduce(
              (acc, curr) => acc.addExcludeRefinement(attribute, curr),
              state,
            );
          }

          return state;
        },
      };
    };
  }

  // …
  ```

  ```ts TypeScript theme={"system"}
  import type { SearchResults, SearchParameters } from "algoliasearch-helper";
  // …

  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList(widgetParams) {
      const { attribute } = widgetParams;

      return {
        // …
        getWidgetSearchParameters(searchParameters, { uiState }) {
          const state = searchParameters.addFacet(attribute);
          const values = uiState.negativeRefinementList?.[attribute];

          if (Array.isArray(values)) {
            return values.reduce(
              (acc, curr) => acc.addExcludeRefinement(attribute, curr),
              state,
            );
          }

          return state;
        },
      };
    };
  };

  // …
  ```
</CodeGroup>

## Send events to the Insights API

To better understand your users,
you could capture when they use the widget to exclude refinements.
The Insights API lets you collect such events from the frontend so that you can, later on,
unlock features such as [Algolia Recommend](/doc/guides/algolia-recommend/overview),
[analytics](/doc/guides/search-analytics/overview), and [more](/doc/guides/sending-events).

You can set up your widget so it automatically sends the right events to Algolia Insights when using the [Insights middleware](/doc/api-reference/widgets/insights/js).

<CodeGroup>
  ```js JavaScript theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList(widgetParams) {
      // …

      return {
        // …
        getWidgetRenderState({ results, helper, instantSearchInstance }) {
          // To ensure `sendEvent` keeps the same reference across renders, create
          // and store it once outside the method scope.
          if (!connectorState.sendEvent) {
            connectorState.sendEvent = (
              eventType,
              facetValue,
              eventName = "Negative Filter Applied",
            ) => {
              if (helper.state.isExcludeRefined(attribute, facetValue)) {
                instantSearchInstance.sendEventToInsights({
                  insightsMethod: "clickedFilters",
                  widgetType: this.$$type,
                  eventType,
                  payload: {
                    eventName,
                    index: helper.getIndex(),
                    filters: [`${attribute}:-${facetValue}`],
                  },
                  attribute,
                });
              }
            };
          }

          if (!connectorState.refine) {
            connectorState.refine = (value) => {
              helper.toggleFacetExclusion(attribute, value);
              // Send `click` event once the facet is toggled.
              connectorState.sendEvent("click", value);

              return helper.search();
            };
          }

          // …
        },
      };
    };
  }

  // …
  ```

  ```ts TypeScript theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList(widgetParams) {
      // …

      type ConnectorState = {
        // …
        sendEvent?(
          eventType: "view" | "click" | "conversion",
          facetValue: string,
          eventName?: string,
        ): void;
      };

      // …

      return {
        // …
        getWidgetRenderState({ results, helper, instantSearchInstance }) {
          // To ensure `sendEvent` keeps the same reference across renders, create
          // and store it once outside the method scope.
          if (!connectorState.sendEvent) {
            connectorState.sendEvent = (
              eventType,
              facetValue,
              eventName = "Negative Filter Applied",
            ) => {
              if (helper.state.isExcludeRefined(attribute, facetValue)) {
                instantSearchInstance.sendEventToInsights({
                  insightsMethod: "clickedFilters",
                  widgetType: this.$$type,
                  eventType,
                  payload: {
                    eventName,
                    index: helper.getIndex(),
                    filters: [`${attribute}:-${facetValue}`],
                  },
                  attribute,
                });
              }
            };
          }

          if (!connectorState.refine) {
            connectorState.refine = (value) => {
              helper.toggleFacetExclusion(attribute, value);
              // Send `click` event once the facet is toggled.
              connectorState.sendEvent!("click", value);

              return helper.search();
            };
          }

          // …
        },
      };
    };
  };

  // …
  ```
</CodeGroup>

Now when clicking on a refinement, it automatically sends an event to Algolia Insights.

<Note>
  This only works when [providing an Insights client with the Insights middleware](/doc/api-reference/widgets/insights/js).
</Note>

You can make the connector even more flexible by providing the `sendEvent` function to the render function. This lets you customize events depending on the use case.

<CodeGroup>
  ```js JavaScript theme={"system"}
  export function connectNegativeRefinementList(renderFn, unmountFn = noop) {
    return function negativeRefinementList(widgetParams) {
      // …

      return {
        // …
        getWidgetRenderState({ results, helper, instantSearchInstance }) {
          // …

          return {
            // …
            sendEvent: connectorState.sendEvent,
          };
        },
      };
    };
  }

  // …
  ```

  ```ts TypeScript theme={"system"}
  export const connectNegativeRefinementList: NegativeRefinementListConnector = (
    renderFn,
    unmountFn = noop,
  ) => {
    return function negativeRefinementList(widgetParams) {
      // …

      return {
        // …
        getWidgetRenderState({ results, helper, instantSearchInstance }) {
          // …

          return {
            // …
            sendEvent: connectorState.sendEvent,
          };
        },
      };
    };
  };

  // …
  ```
</CodeGroup>

## Render a custom user interface

An InstantSearch widget is a custom connector with a render function.

In this example, the `NegativeRefinementList` component uses the `useNegativeRefinementList()` connector to build a reactive, stateful UI.

Rendering consists of two parts:

* A `render` function, which outputs the updated UI and injects it in the DOM
* A `dispose` unmounting function, which cleans up the DOM when the widget unmounts

Both functions require access to the DOM element (for injection and clean up).You can provide it with a factory function.

<CodeGroup>
  ```js JavaScript (Preact) theme={"system"}
  import { render as preactRender } from 'preact';
  import { html } from 'htm/preact';

  function createNegativeRefinementListRenderer({ container }) {
    const containerNode =
      typeof container === 'string'
        ? document.querySelector(container)
        : container;

    return {
      render({ items, refine, canRefine }) {
        preactRender(
          html`
            <div
              class="${cx(
                'ais-NegativeRefinementList',
                !canRefine && 'ais-NegativeRefinementList--noRefinement'
              )}"
            >
              <ul class="ais-NegativeRefinementList-list">
                ${items
                  .map(
                    (item) => html`<li
                      class="${cx(
                        'ais-NegativeRefinementList-item',
                        item.isExcluded &&
                          'ais-NegativeRefinementList-item--selected'
                      )}"
                    >
                      <label class="ais-NegativeRefinementList-label">
                        <input
                          checked="${item.isExcluded}"
                          type="checkbox"
                          class="ais-NegativeRefinementList-checkbox"
                          value="${item.name}"
                          onChange="${(event) => {
                            event.preventDefault();
                            refine(item.name);
                          }}"
                        />
                        <span class="ais-NegativeRefinementList-labelText"
                          >${item.name}</span
                        >
                        <span class="ais-NegativeRefinementList-count"
                          >${item.count}</span
                        > </label
                      >$
                    </li>`
                  )
                  .join('')}
              </ul>
            </div>
          `,
          containerNode
        );
      },
      dispose() {
        preactRender(null, containerNode);
      },
    };
  }

  function cx(...classNames) {
    return classNames.filter(Boolean).join(' ');
  }
  ```

  ```js JavaScript (DOM API) theme={"system"}
  function createNegativeRefinementListRenderer({ container }) {
    const containerNode =
      typeof container === 'string'
        ? document.querySelector(container)
        : container;

    return {
      render({ items, refine, canRefine }) {
        containerNode.innerHTML = `
          <div class="${cx(
            'ais-NegativeRefinementList',
            !canRefine && 'ais-NegativeRefinementList--noRefinement'
          )}">
            <ul class="ais-NegativeRefinementList-list">${items
              .map(
                (item) => `
                  <li class="${cx(
                    'ais-NegativeRefinementList-item',
                    item.isExcluded && 'ais-NegativeRefinementList-item--selected'
                  )}">
                    <label class="ais-NegativeRefinementList-label">
                      <input
                        checked="${item.isExcluded}"
                        type="checkbox"
                        class="ais-NegativeRefinementList-checkbox"
                        value="${item.name}"
                      />
                      <span class="ais-NegativeRefinementList-labelText">${
                        item.name
                      }</span>
                      <span class="ais-NegativeRefinementList-count">${
                        item.count
                      }</span>
                    </label>$
                  </li>
                `
              )
              .join('')}
            </ul>
          </div>
        `;

        [...containerNode.querySelectorAll(input)].forEach((element) => {
          element.addEventListener('change', (event) => {
            event.preventDefault();
            refine(event.currentTarget.value);
          });
        });
      },
      dispose() {
        containerNode.innerHTML = '';
      },
    };
  }

  function cx(...classNames) {
    return classNames.filter(Boolean).join(' ');
  }
  ```

  ```tsx TypeScript (Preact) theme={"system"}
  function createNegativeRefinementListRenderer({ container }) {
    const containerNode =
      typeof container === 'string'
        ? document.querySelector(container)
        : container;

    return {
      render({ items, refine, canRefine }) {
        containerNode.innerHTML = `
          <div class="${cx(
            'ais-NegativeRefinementList',
            !canRefine && 'ais-NegativeRefinementList--noRefinement'
          )}">
            <ul class="ais-NegativeRefinementList-list">${items
              .map(
                (item) => `
                  <li class="${cx(
                    'ais-NegativeRefinementList-item',
                    item.isExcluded && 'ais-NegativeRefinementList-item--selected'
                  )}">
                    <label class="ais-NegativeRefinementList-label">
                      <input
                        checked="${item.isExcluded}"
                        type="checkbox"
                        class="ais-NegativeRefinementList-checkbox"
                        value="${item.name}"
                      />
                      <span class="ais-NegativeRefinementList-labelText">${
                        item.name
                      }</span>
                      <span class="ais-NegativeRefinementList-count">${
                        item.count
                      }</span>
                    </label>$
                  </li>
                `
              )
              .join('')}
            </ul>
          </div>
        `;

        [...containerNode.querySelectorAll(input)].forEach((element) => {
          element.addEventListener('change', (event) => {
            event.preventDefault();
            refine(event.currentTarget.value);
          });
        });
      },
      dispose() {
        containerNode.innerHTML = '';
      },
    };
  }

  function cx(...classNames) {
    return classNames.filter(Boolean).join(' ');
  }
  ```

  ```tsx TypeScript (DOM API) theme={"system"}
  import type { Renderer, Unmounter } from 'instantsearch.js';

  type NegativeRefinementListWidgetParams = {
    container: string | HTMLElement;
  };

  type NegativeRefinementListRendererFactory = ({
    container,
  }: NegativeRefinementListWidgetParams) => {
    render: Renderer<
      NegativeRefinementListRenderState,
      Partial<NegativeRefinementListWidgetParams>
    >;
    dispose: Unmounter;
  };

  const createNegativeRefinementListRenderer: NegativeRefinementListRendererFactory =
    ({ container }) => {
    const containerNode =
      typeof container === 'string'
        ? document.querySelector(container)
        : container;

    return {
      render({ items, refine, canRefine }) {
        containerNode.innerHTML = `
          <div class="${cx(
            'ais-NegativeRefinementList',
            !canRefine && 'ais-NegativeRefinementList--noRefinement'
          )}">
            <ul class="ais-NegativeRefinementList-list">${items
              .map(
                (item) => `
                  <li class="${cx(
                    'ais-NegativeRefinementList-item',
                    item.isExcluded && 'ais-NegativeRefinementList-item--selected'
                  )}">
                    <label class="ais-NegativeRefinementList-label">
                      <input
                        checked="${item.isExcluded}"
                        type="checkbox"
                        class="ais-NegativeRefinementList-checkbox"
                        value="${item.name}"
                      />
                      <span class="ais-NegativeRefinementList-labelText">${
                        item.name
                      }</span>
                      <span class="ais-NegativeRefinementList-count">${
                        item.count
                      }</span>
                    </label>$
                  </li>
                `
              )
              .join('')}
            </ul>
          </div>
        `;

        [...containerNode.querySelectorAll(input)].forEach((element) => {
          element.addEventListener('change', (event) => {
            event.preventDefault();
            refine(event.currentTarget.value);
          });
        });
      },
      dispose() {
        containerNode.innerHTML = '';
      },
    };
  }

  function cx(
    ...classNames: Array<string | number | boolean | undefined | null>
  ) {
    return classNames.filter(Boolean).join(' ');
  }
  ```
</CodeGroup>

You can now create the widget (consisting of a connector and associated UI rendering).

<CodeGroup>
  ```js JavaScript theme={"system"}
  function negativeRefinementList(params) {
    const { container, attribute, ...connectorParams } = params;
    const { render, dispose } = createNegativeRefinementListRenderer({
      container,
    });

    const createWidget = connectNegativeRefinementList(render, dispose);

    return {
      ...createWidget(connectorParams),
      // This is helpful for debugging purposes and allows to differentiate
      // between the connector and the widget.
      $$widgetType: 'myOrganization.negativeRefinementList',
    };
  }
  ```

  ```ts TypeScript theme={"system"}
  import { WidgetFactory, WidgetRenderState } from 'instantsearch.js';

  type NegativeRefinementListWidgetDescription = {
    $$type: 'myOrganization.negativeRefinementList',
    renderState: NegativeRefinementListRenderState,
    indexRenderState: {
      negativeRefinementList: {
        [attribute: string]: WidgetRenderState<
          NegativeRefinementListRenderState,
          NegativeRefinementListConnectorParams
        >,
      },
    },
    indexUiState: {
      negativeRefinementList: {
        [attribute: string]: string[],
      },
    },
  };

  type NegativeRefinementListWidget = WidgetFactory<
    NegativeRefinementListWidgetDescription & {
      $$widgetType: 'myOrganization.negativeRefinementList',
    },
    NegativeRefinementListConnectorParams,
    NegativeRefinementListWidgetParams
  >;

  function negativeRefinementList(params: NegativeRefinementListParams) {
    const { container, attribute, ...connectorParams } = params;
    const { render, dispose } = createNegativeRefinementListRenderer({
      container,
    });

    const createWidget = connectNegativeRefinementList(render, dispose);

    return {
      ...createWidget(connectorParams),
      // This is helpful for debugging purposes and allows to differentiate
      // between the connector and the widget.
      $$widgetType: 'myOrganization.negativeRefinementList',
    };
  }
  ```
</CodeGroup>

<Info>
  InstantSearch widgets use a [standardized class naming convention](#expose-standard-classes).
</Info>

The widget is now [usable in an InstantSearch app](#use-the-custom-widget).
Still, if you want to reuse or distribute the widget, you can [further tweak the API](#make-the-widget-reusable) to use the same standards as the built-in InstantSearch widgets.

## Make the widget reusable

You might want to reuse your widget within your app, share it across multiple projects, or even publish it on [npm](https://www.npmjs.com/) for others to enjoy.
To do so, **you can provide APIs to allow customization** while abstracting the complexity away.

InstantSearch exposes consistent APIs. You can follow the same guidelines and conventions in your own widgets and connectors.

### Expose standard classes

Widgets expose a class on each DOM element to help you style them.

Built-in InstantSearch widgets use the [SUITCSS](https://github.com/suitcss/suit/blob/master/doc/naming-conventions.md#components) component syntax:

```txt theme={"system"}
[<namespace>-]<ComponentName>[-descendantName][--modifierName]
```

* **Every class starts with the `ais-` namespace** (for Algolia InstantSearch). This helps target all InstantSearch elements with selectors like `[class^="ais-"]`.
* **Every class has a component name mapped to the widget name.** In the example on this page, the widget uses the `NegativeRefinementList` component name. Component names are always in [Pascal case](https://en.wiktionary.org/wiki/Pascal_case).
* **Each element is identified with a descendant name.** In the example on this page, each item of the widget uses the `-item` descendant name. Descendant names are always in camel case.
* If an element has multiple states, **identify each state with a "modifier".** In the example on this page, the selected item of the widget uses the `--selected` modifier. Modifiers are always in camel case and prefixed with two hyphens. You should include the modified class on the element in addition to the base component class (for example, `ais-NegativeRefinementList-item` and `ais-NegativeRefinementList-item--selected`).

<Info>
  See these conventions [in action](#render-a-custom-user-interface).
</Info>

If you're using a CSS linter like [Stylelint](https://stylelint.io/),
you can validate your InstantSearch classes with the following regular expression:

```txt theme={"system"}
^ais-(?:[A-Z][a-z]+)+(?:-[a-z]+(?:[A-Z][a-z]+)*)?(?:--[a-z]+(?:[A-Z][a-z]+)*)?$
```

### Pass custom classes

Widgets expose [standardized class names](#expose-standard-classes) to let users write custom CSS but you could open the styling API further to allow passing classes directly on each element.
This lets users of class-based CSS frameworks like [Bootstrap](https://getbootstrap.com/) or [Tailwind CSS](https://tailwindcss.com/) use them without friction or workarounds.

In built-in widgets, the convention is to provide a prop that takes an object of named classes.

<CodeGroup>
  ```js JavaScript (Preact) theme={"system"}
  function createNegativeRefinementListRenderer({ container, cssClasses = {} }) {
    // …

    return {
      render({ items, refine, canRefine }) {
        preactRender(
          html`
            <div
              class="${cx(
                'ais-NegativeRefinementList',
                cssClasses.root,
                !canRefine &&
                  cx(
                    'ais-NegativeRefinementList--noRefinement',
                    cssClasses.noRefinementRoot
                  )
              )}"
            >
              <ul class="${cx(
                'ais-NegativeRefinementList-list',
                cssClasses.list
              )}">
                ${items
                  .map(
                    (item) => html`<li
                      class="${cx(
                        'ais-NegativeRefinementList-item',
                        cssClasses.item,
                        item.isExcluded &&
                          cx(
                            'ais-NegativeRefinementList-item--selected',
                            cssClasses.selectedItem
                          )
                      )}"
                    >
                      <label class="${cx(
                        'ais-NegativeRefinementList-label',
                        cssClasses.label
                      )}">
                        <input
                          checked="${item.isExcluded}"
                          type="checkbox"
                          class="${cx(
                            'ais-NegativeRefinementList-checkbox',
                            cssClasses.checkbox
                          )}"
                          value="${item.name}"
                          onChange="${(event) => {
                            event.preventDefault();
                            refine(item.name);
                          }}"
                        />
                        <span class="${cx(
                          'ais-NegativeRefinementList-labelText',
                          cssClasses.labelText
                        )}"
                          >${item.name}</span
                        >
                        <span class="${cx(
                          'ais-NegativeRefinementList-count',
                          cssClasses.count
                        )}"
                          >${item.count}</span
                        > </label
                      >$
                    </li>`
                  )
                  .join('')}
              </ul>
            </div>
          `,
          containerNode
        );

        // …
      },
      // …
    };
  }
  ```

  ```js JavaScript (DOM API) theme={"system"}
  function createNegativeRefinementListRenderer({ container, cssClasses = {} }) {
    // …

    return {
      render({ items, refine, canRefine }) {
        containerNode.innerHTML = `
          <div class="${cx(
            'ais-NegativeRefinementList',
            cssClasses.root,
            !canRefine &&
              cx(
                'ais-NegativeRefinementList--noRefinement',
                cssClasses.noRefinementRoot
              )
          )}">
            <ul class="${cx('ais-NegativeRefinementList-list', cssClasses.list)}">
            ${items
              .map(
                (item) => `
              <li class="${cx(
                'ais-NegativeRefinementList-item',
                cssClasses.item,
                item.isExcluded &&
                  cx(
                    'ais-NegativeRefinementList-item--selected',
                    cssClasses.selectedItem
                  )
              )}">
                <label class="${cx(
                  'ais-NegativeRefinementList-label',
                  cssClasses.label
                )}">
                  <input
                    checked="${item.isExcluded}"
                    type="checkbox"
                    class="${cx(
                      'ais-NegativeRefinementList-checkbox',
                      cssClasses.checkbox
                    )}"
                    value="${item.name}"
                  />
                  <span class="${cx(
                    'ais-NegativeRefinementList-labelText',
                    cssClasses.labelText
                  )}">${item.name}</span>
                  <span class="${cx(
                    'ais-NegativeRefinementList-count',
                    cssClasses.count
                  )}">${item.count}</span>
                </label>$
              </li>
            `
              )
              .join('')}
            </ul>
          </div>
        `;

        // …
      },
      // …
    };
  }
  ```

  ```ts TypeScript (Preact) theme={"system"}
  type NegativeRefinementListClassNames = {
    root: string,
    noRefinementRoot: string,
    list: string,
    item: string,
    selectedItem: string,
    label: string,
    checkbox: string,
    labelText: string,
    count: string,
  };

  type NegativeRefinementListWidgetParams = {
    container: string | HTMLElement,
    cssClasses?: Partial<NegativeRefinementListClassNames>,
  };

  type NegativeRefinementListRendererFactory = {
    // …
  };

  const createNegativeRefinementListRenderer: NegativeRefinementListRendererFactory =
    ({ container, cssClasses = {} }) => {
      // …

      return {
        render({ items, refine, canRefine }) {
          preactRender(
            html`
              <div
                class="${cx(
                  'ais-NegativeRefinementList',
                  cssClasses.root,
                  !canRefine &&
                    cx(
                      'ais-NegativeRefinementList--noRefinement',
                      cssClasses.noRefinementRoot
                    )
                )}"
              >
                <ul
                  class="${cx(
                    'ais-NegativeRefinementList-list',
                    cssClasses.list
                  )}"
                >
                  ${items
                    .map(
                      (item) => html`<li
                        class="${cx(
                          'ais-NegativeRefinementList-item',
                          cssClasses.item,
                          item.isExcluded &&
                            cx(
                              'ais-NegativeRefinementList-item--selected',
                              cssClasses.selectedItem
                            )
                        )}"
                      >
                        <label
                          class="${cx(
                            'ais-NegativeRefinementList-label',
                            cssClasses.label
                          )}"
                        >
                          <input
                            checked="${item.isExcluded}"
                            type="checkbox"
                            class="${cx(
                              'ais-NegativeRefinementList-checkbox',
                              cssClasses.checkbox
                            )}"
                            value="${item.name}"
                            onChange="${(event) => {
                              event.preventDefault();
                              refine(item.name);
                            }}"
                          />
                          <span
                            class="${cx(
                              'ais-NegativeRefinementList-labelText',
                              cssClasses.labelText
                            )}"
                            >${item.name}</span
                          >
                          <span
                            class="${cx(
                              'ais-NegativeRefinementList-count',
                              cssClasses.count
                            )}"
                            >${item.count}</span
                          > </label
                        >$
                      </li>`
                    )
                    .join('')}
                </ul>
              </div>
            `,
            containerNode
          );

          // …
        },
        // …
      };
    };
  ```

  ```ts TypeScript (DOM API) theme={"system"}
  type NegativeRefinementListClassNames = {
    root: string,
    noRefinementRoot: string,
    list: string,
    item: string,
    selectedItem: string,
    label: string,
    checkbox: string,
    labelText: string,
    count: string,
  };

  type NegativeRefinementListWidgetParams = {
    container: string | HTMLElement,
    cssClasses?: Partial<NegativeRefinementListClassNames>,
  };

  type NegativeRefinementListRendererFactory = {
    // …
  };

  const createNegativeRefinementListRenderer: NegativeRefinementListRendererFactory =
    ({ container, cssClasses = {} }) => {
      // …

      return {
        render({ items, refine, canRefine }) {
          preactRender(
            html`
              <div
                class="${cx(
                  'ais-NegativeRefinementList',
                  cssClasses.root,
                  !canRefine &&
                    cx(
                      'ais-NegativeRefinementList--noRefinement',
                      cssClasses.noRefinementRoot
                    )
                )}"
              >
                <ul
                  class="${cx(
                    'ais-NegativeRefinementList-list',
                    cssClasses.list
                  )}"
                >
                  ${items
                    .map(
                      (item) => html`<li
                        class="${cx(
                          'ais-NegativeRefinementList-item',
                          cssClasses.item,
                          item.isExcluded &&
                            cx(
                              'ais-NegativeRefinementList-item--selected',
                              cssClasses.selectedItem
                            )
                        )}"
                      >
                        <label
                          class="${cx(
                            'ais-NegativeRefinementList-label',
                            cssClasses.label
                          )}"
                        >
                          <input
                            checked="${item.isExcluded}"
                            type="checkbox"
                            class="${cx(
                              'ais-NegativeRefinementList-checkbox',
                              cssClasses.checkbox
                            )}"
                            value="${item.name}"
                            onChange="${(event) => {
                              event.preventDefault();
                              refine(item.name);
                            }}"
                          />
                          <span
                            class="${cx(
                              'ais-NegativeRefinementList-labelText',
                              cssClasses.labelText
                            )}"
                            >${item.name}</span
                          >
                          <span
                            class="${cx(
                              'ais-NegativeRefinementList-count',
                              cssClasses.count
                            )}"
                            >${item.count}</span
                          > </label
                        >$
                      </li>`
                    )
                    .join('')}
                </ul>
              </div>
            `,
            containerNode
          );

          // …
        },
        // …
      };
    };
  ```
</CodeGroup>

### Customize the UI

**UI customizability is an important aspect of building reusable InstantSearch widgets.**
If you need to internationalize your app, control the markup, or change icons, you shouldn't have to opt out of using widgets and resort to using connectors just to tweak the UI.

#### Templates

If users need to control the markup, they shouldn't have to immediately reach out to connectors.
Instead, you can let them template parts of the UI.
For example, users may want to change the rendering of each item to display a custom checkbox that requires additional markup.

In InstantSearch.js, widgets expose a `templates` option.
Templates are dictionaries to customize the UI elements, text, and support internationalization.
For more information, see [Templating your UI](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js/#templating-your-ui).

<Note>
  Behind the scenes, InstantSearch.js uses Preact to render widgets and lets widget users [return templates as virtual DOM elements](/doc/guides/building-search-ui/widgets/customize-an-existing-widget/js/#templating-your-ui)).
  Consider using Preact and exposing the `html` function in your templates for a consistent experience with built-in widgets.
</Note>

<CodeGroup>
  ```js JavaScript (Preact) theme={"system"}
  import { render as preactRender } from 'preact';
  import { html } from 'htm/preact';

  function createNegativeRefinementListRenderer({
    container,
    cssClasses = {},
    templates: providedTemplates = {},
  }) {
    // …

    const templates = {
      listItem({ item, cssClasses, html }) {
        return html`
          <li
            class="${cx(
              'ais-NegativeRefinementList-item',
              cssClasses.item,
              item.isExcluded &&
                cx(
                  'ais-NegativeRefinementList-item--selected',
                  cssClasses.selectedItem
                )
            )}"
          >
            <!-- … -->
          </li>
        `;
      },
      ...providedTemplates,
    };

    return {
      render({ items, refine, canRefine }) {
        preactRender(
          html`
            <div class="${cx('ais-NegativeRefinementList' /* … */)}">
              <ul class="${cx('ais-NegativeRefinementList-list' /* … */)}">
                ${items
                  .map((item) => templates.listItem({ item, cssClasses, html }))
                  .join('')}
              </ul>
            </div>
          `,
          containerNode
        );
      },
      // …
    };
  }
  ```

  ```js JavaScript (DOM API) theme={"system"}
  function createNegativeRefinementListRenderer({
    container,
    cssClasses = {},
    templates: providedTemplates = {},
  }) {
    // …

    const templates = {
      listItem({ item, cssClasses }) {
        return `
          <li
            class="${cx(
              'ais-NegativeRefinementList-item',
              cssClasses.item,
              item.isExcluded &&
                cx(
                  'ais-NegativeRefinementList-item--selected',
                  cssClasses.selectedItem
                )
            )}"
          >
            <!-- … -->
          </li>
        `;
      },
      ...providedTemplates,
    };

    return {
      render({ items, refine, canRefine }) {
        // …

        containerNode.querySelector('ul').innerHTML = items
          .map((item) => templates.listItem({ item, cssClasses }))
          .join('');

        // …
      },
      // …
    };
  }
  ```

  ```ts TypeScript (Preact) theme={"system"}
  import { render as preactRender } from 'preact';
  import { html } from 'htm/preact';

  import type { Renderer, Unmounter } from 'instantsearch.js';
  import type { VNode } from 'preact';

  type NegativeRefinementListClassNames = {
    // …
  };

  type NegativeRefinementListTemplates = {
    listItem(
      data: Partial<NegativeRefinementListClassNames> & {
        item: NegativeRefinementListRenderState['items'],
        html: typeof html,
      }
    ): VNode | VNode[] | string,
  };

  type NegativeRefinementListWidgetParams = {
    container: string | HTMLElement,
    cssClasses?: Partial<NegativeRefinementListClassNames>,
    templates?: Partial<NegativeRefinementListTemplates>,
  };

  type NegativeRefinementListRendererFactory = {
    // …
  };

  const createNegativeRefinementListRenderer: NegativeRefinementListRendererFactory =
    ({ container, cssClasses = {}, templates: providedTemplates = {} }) => {
      // …

      const templates: NegativeRefinementListTemplates = {
        listItem({ item, cssClasses, html }) {
          return html`
            <li
              class="${cx(
                'ais-NegativeRefinementList-item',
                cssClasses.item,
                item.isExcluded &&
                  cx(
                    'ais-NegativeRefinementList-item--selected',
                    cssClasses.selectedItem
                  )
              )}"
            >
              <!-- … -->
            </li>
          `;
        },
        ...providedTemplates,
      };

      return {
        render({ items, refine, canRefine }) {
          preactRender(
            html`
              <div class="${cx('ais-NegativeRefinementList' /* … */)}">
                <ul class="${cx('ais-NegativeRefinementList-list' /* … */)}">
                  ${items
                    .map((item) => templates.listItem({ item, cssClasses, html }))
                    .join('')}
                </ul>
              </div>
            `,
            containerNode
          );
        },
        // …
      };
    };
  ```

  ```ts TypeScript (DOM API) theme={"system"}
  import { render as preactRender } from 'preact';
  import { html } from 'htm/preact';

  import type { Renderer, Unmounter } from 'instantsearch.js';
  import type { VNode } from 'preact';

  type NegativeRefinementListClassNames = {
    // …
  };

  type NegativeRefinementListTemplates = {
    listItem(
      data: Partial<NegativeRefinementListClassNames> & {
        item: NegativeRefinementListRenderState['items'],
        html: typeof html,
      }
    ): VNode | VNode[] | string,
  };

  type NegativeRefinementListWidgetParams = {
    container: string | HTMLElement,
    cssClasses?: Partial<NegativeRefinementListClassNames>,
    templates?: Partial<NegativeRefinementListTemplates>,
  };

  type NegativeRefinementListRendererFactory = {
    // …
  };

  const createNegativeRefinementListRenderer: NegativeRefinementListRendererFactory =
    ({ container, cssClasses = {}, templates: providedTemplates = {} }) => {
      // …

      const templates: NegativeRefinementListTemplates = {
        listItem({ item, cssClasses, html }) {
          return html`
            <li
              class="${cx(
                'ais-NegativeRefinementList-item',
                cssClasses.item,
                item.isExcluded &&
                  cx(
                    'ais-NegativeRefinementList-item--selected',
                    cssClasses.selectedItem
                  )
              )}"
            >
              <!-- … -->
            </li>
          `;
        },
        ...providedTemplates,
      };

      return {
        render({ items, refine, canRefine }) {
          preactRender(
            html`
              <div class="${cx('ais-NegativeRefinementList' /* … */)}">
                <ul class="${cx('ais-NegativeRefinementList-list' /* … */)}">
                  ${items
                    .map((item) => templates.listItem({ item, cssClasses, html }))
                    .join('')}
                </ul>
              </div>
            `,
            containerNode
          );
        },
        // …
      };
    };
  ```
</CodeGroup>

### Expose standard options

Each widget and connector exposes options specific to its use case.
Some options are available across multiple widgets and connectors to address common issues and offer a consistent experience.

In addition to root props, classes, translations, and templates,
you can expose the following options on widgets and connectors when relevant.

#### Providing a container

In InstantSearch.js, widgets are rendered within a container:
a DOM element to inject the widget into.
Your [render function](#render-a-custom-user-interface) should accept a container CSS selector as a string (such as `#negative-refinement-list`) or a direct reference to the element, as an `HTMLElement`.

#### Target a record attribute

When a widget needs to target a specific record attribute, you should expose an `attribute` option in the connector.
This is the case for most refinement widgets like [`RefinementList`](/doc/api-reference/widgets/refinement-list/js) so you can select the specific attribute to refine.

The `attribute` option typically accepts a string.
For deeply nested objects,
you can accept dot-separated values like `brand.name` or an array of strings like `["brand", "name"]`.

#### Include or exclude attributes

When a widget manipulates selected refinements, you may want to let users select what attributes to include or exclude.
This is the case with widgets like [`CurrentRefinements`](/doc/api-reference/widgets/current-refinements/js) or [`ClearRefinements`](/doc/api-reference/widgets/clear-refinements/js) so you can hand-pick exactly what attributes to manipulate.

#### Limit items

When a widget manipulates items, notably facet refinements, you may want to let users limit how many of them to retrieve.
Widgets like [`RefinementList`](/doc/api-reference/widgets/refinement-list/js) or [`Menu`](/doc/api-reference/widgets/menu/js) expose a `limit` option,
which is directly forwarded to the Algolia search engine with the [`maxValuesPerFacet`](/doc/api-reference/api-parameters/maxValuesPerFacet) search parameter.

Such widgets usually expose two extra options to let users toggle more facets: `showMore`, a boolean option to enable the feature, and `showMoreLimit`, to define the maximum number of items to display if the widget is showing more items.

#### Transform items

When a widget manipulates items like hits or facets,
users may want to change them before rendering.
To do so, expose a `transformItems` option for transforming,
removing, or reordering items on the connector.

The `transformItems` option is a function that receives the items and should return a new array of the same shape. The default value is an identity function.

## Use the custom widget

You can use a custom widget like any widget provided by the library.
It takes the parameters to forward to the connectors, HTML props for the root element, and an object for classes.

```js JavaScript theme={"system"}
import { liteClient as algoliasearch } from 'algoliasearch/lite';
import instantsearch from 'instantsearch.js';

import { negativeRefinementList } from './negativeRefinementList';

const searchClient = algoliasearch('undefined', 'undefined');

const search = instantsearch({
  indexName: 'instant_search',
  searchClient,
});

search.addWidgets([
  negativeRefinementList({
    // Connector parameters
    attribute: 'brand',
    // Container to inject into
    container: '#negative-refinement-list',
    // Custom CSS classes
    cssClasses: {
      root: 'MyCustomNegativeRefinementList',
      item: 'MyCustomNegativeRefinementListItem',
    },
  }),
]);

search.start();
```

This is what the widget looks like in an app:

<img src="https://mintcdn.com/algolia/WOi5v-PGZrMZ2rOj/images/build-search-ui/negative-refinement-list.gif?s=99f6ecca35adcba5615c12f256e75fdd" alt="Screenshot of a search interface with a 'BRANDS' filter on the left and a list of products with prices on the right." width="690" height="388" data-path="images/build-search-ui/negative-refinement-list.gif" />

## Next steps

You now have a good starting point to take full control of your InstantSearch experience.
You can further improve it by:

* Improving the widget's API with useful parameters like [`connectRefinementList`](/doc/api-reference/widgets/refinement-list/js#instance-options).
* Open sourcing your custom widget on [GitHub](https://github.com/) and publish it on [npm](https://www.npmjs.com/) (if you do, [let Algolia know.](https://github.com/algolia/instantsearch/discussions/new?category=show-and-tell)).
