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

# Customize the chat results carousel

> Customize how the chat widget renders Algolia search results and recommendations in a carousel with InstantSearch.js.

export const customLabel_0 = "in beta"

export const FlavorSwitcher = ({current, baseHref = "", options = [], label = "InstantSearch framework"}) => {
  if (options.length === 0) {
    return <div className="not-prose" role="alert" style={{
      margin: "0.25rem 0 1.5rem",
      padding: "0.75rem",
      border: "1px solid #f59e0b",
      borderRadius: "0.625rem",
      color: "inherit",
      fontSize: "0.875rem"
    }}>
        FlavorSwitcher requires at least one option.
      </div>;
  }
  const selected = options.find(option => option.value === current) ?? options[0];
  return <div className="not-prose mint-flavor-switcher">
      <style>{`
        .mint-flavor-switcher {
          --mfs-bg: #ffffff;
          --mfs-bg-hover: #f4f4f5;
          --mfs-bg-current: #eef2ff;
          --mfs-border: #d4d4d8;
          --mfs-fg: #18181b;
          --mfs-muted: #71717a;
          --mfs-accent: #4f46e5;
          position: relative;
          width: min(100%, 19rem);
          margin: 0.25rem 0 1.5rem;
          color: var(--mfs-fg);
          font-size: 0.875rem;
          line-height: 1.25rem;
        }

        .dark .mint-flavor-switcher {
          --mfs-bg: #18181b;
          --mfs-bg-hover: #27272a;
          --mfs-bg-current: #272747;
          --mfs-border: #3f3f46;
          --mfs-fg: #fafafa;
          --mfs-muted: #a1a1aa;
          --mfs-accent: #a5b4fc;
        }

        .mint-flavor-switcher details {
          position: relative;
        }

        .mint-flavor-switcher summary {
          display: flex;
          min-height: 2.75rem;
          box-sizing: border-box;
          align-items: center;
          justify-content: space-between;
          gap: 0.75rem;
          padding: 0.625rem 0.75rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          color: var(--mfs-fg);
          cursor: pointer;
          font-weight: 600;
          list-style: none;
          transition: border-color 150ms ease, box-shadow 150ms ease;
        }

        .mint-flavor-switcher summary::-webkit-details-marker {
          display: none;
        }

        .mint-flavor-switcher summary:hover {
          border-color: var(--mfs-accent);
        }

        .mint-flavor-switcher summary:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: 2px;
        }

        .mint-flavor-switcher__label {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }

        .mint-flavor-switcher__chevron {
          flex: none;
          transition: transform 150ms ease;
        }

        .mint-flavor-switcher details[open] .mint-flavor-switcher__chevron {
          transform: rotate(180deg);
        }

        .mint-flavor-switcher__menu {
          position: absolute;
          z-index: 50;
          top: calc(100% + 0.375rem);
          left: 0;
          width: 100%;
          box-sizing: border-box;
          margin: 0;
          padding: 0.375rem;
          border: 1px solid var(--mfs-border);
          border-radius: 0.625rem;
          background: var(--mfs-bg);
          box-shadow: 0 12px 30px rgb(0 0 0 / 16%);
          list-style: none;
        }

        .mint-flavor-switcher__menu li {
          margin: 0;
          padding: 0;
        }

        .mint-flavor-switcher__option {
          display: grid;
          gap: 0.125rem;
          padding: 0.625rem 0.75rem;
          border-radius: 0.4rem;
          color: var(--mfs-fg);
          text-decoration: none;
        }

        .mint-flavor-switcher__option:hover {
          background: var(--mfs-bg-hover);
        }

        .mint-flavor-switcher__option:focus-visible {
          outline: 2px solid var(--mfs-accent);
          outline-offset: -2px;
        }

        .mint-flavor-switcher__option[aria-current="page"] {
          background: var(--mfs-bg-current);
          color: var(--mfs-accent);
        }

        .mint-flavor-switcher__name {
          font-weight: 600;
        }

        .mint-flavor-switcher__description {
          color: var(--mfs-muted);
          font-size: 0.8125rem;
        }

        @media (prefers-reduced-motion: reduce) {
          .mint-flavor-switcher summary,
          .mint-flavor-switcher__chevron {
            transition: none;
          }
        }
      `}</style>

      <details>
        <summary aria-label={`${label}: ${selected.label}`}>
          <span className="mint-flavor-switcher__label">{selected.label}</span>
          <svg className="mint-flavor-switcher__chevron" width="18" height="18" 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>
        </summary>

        <ul className="mint-flavor-switcher__menu" aria-label={label}>
          {options.map(option => {
    const isCurrent = option.value === selected.value;
    const href = option.href ?? `${baseHref.replace(/\/$/, "")}/${encodeURIComponent(option.value)}`;
    return <li key={option.value}>
                <a className="mint-flavor-switcher__option" href={href} aria-current={isCurrent ? "page" : undefined}>
                  <span className="mint-flavor-switcher__name">
                    {option.label}
                  </span>
                  {option.description ? <span className="mint-flavor-switcher__description">
                      {option.description}
                    </span> : null}
                </a>
              </li>;
  })}
        </ul>
      </details>
    </div>;
};

<div className="mint-flavor-switcher-slot not-prose">
  <FlavorSwitcher
    current="js"
    baseHref="/doc/guides/building-search-ui/going-further/chat-customization/results-carousel"
    options={[
{ value: "js", label: "JavaScript", description: "InstantSearch.js" },
{ value: "react", label: "React", description: "React InstantSearch" },
]}
  />
</div>

<Callout icon="flask-conical" color="#14b8a6">
  This widget is **{customLabel_0 || "experimental"}** and is subject to change in minor versions.
</Callout>

When the agent searches your index, the [`chat`](/doc/api-reference/widgets/chat/js) widget renders the results in a built-in carousel. You can customize it at three levels, from the least to the most involved:

1. Restyle the default carousel with CSS.
2. Change the markup of each result card with the [`item`](/doc/api-reference/widgets/chat/js#param-templates) template.
3. Replace the entire carousel layout by overriding the search tool.

## Restyle the default carousel with CSS

If the default structure works and you only want a different look, target the carousel's CSS classes. The search-results carousel uses classes prefixed with `ais-ChatToolSearchIndexCarousel` (for example, `ais-ChatToolSearchIndexCarouselHeader`). See the [styling guide](/doc/guides/building-search-ui/styling/js) for the full approach.

## Customize each result card

Use the [`item`](/doc/api-reference/widgets/chat/js#param-templates) template to change what each card in the carousel looks like. The same template is used by both the search-results and recommendations carousels. It receives a single record:

```js JavaScript icon=code theme={"system"}
import algoliasearch from "algoliasearch/lite";
import instantsearch from "instantsearch.js";
import { chat } from "instantsearch.js/es/widgets";

const search = instantsearch({
  indexName: "instant_search",
  searchClient: algoliasearch("YourApplicationID", "YourSearchOnlyAPIKey"),
});

search.addWidgets([
  chat({
    container: "#chat",
    agentId: "YOUR_AGENT_ID",
    templates: {
      item(hit, { html, components }) {
        return html`
          <article class="product-card">
            <img src="${hit.image}" alt="${hit.name}" />
            <h3>${components.Highlight({ attribute: "name", hit })}</h3>
            <span class="product-card-price">$${hit.price}</span>
          </article>
        `;
      },
    },
  }),
]);

search.start();
```

For most stores, this is enough: you swap in your own product-card markup and the carousel behavior (header, scroll buttons, "View all") stays as-is.

## Replace the entire carousel layout

To change more than the cards—such as the header, the scroll controls, or the wrapper—override the tool that renders the carousel. Import the tool type and use it as a key in the [`tools`](/doc/api-reference/widgets/chat/js#param-tools) option:

* `SearchIndexToolType` renders the search-results carousel.
* `RecommendToolType` renders the recommendations carousel.

Both are exported from `instantsearch.js/es/widgets/chat/chat`. Whatever you pass under a tool key fully replaces the built-in rendering for that tool.

```js JavaScript icon=code expandable theme={"system"}
import { chat, SearchIndexToolType } from "instantsearch.js/es/widgets/chat/chat";

search.addWidgets([
  chat({
    container: "#chat",
    agentId: "YOUR_AGENT_ID",
    tools: {
      [SearchIndexToolType]: {
        templates: {
          layout({ message, sendEvent }, { html }) {
            const items = message.output?.hits || [];

            if (items.length === 0) {
              return html`<p>No results found.</p>`;
            }

            return html`
              <div class="MyCarousel">
                <div class="MyCarousel-header">
                  ${items.length} of ${message.output?.nbHits} results
                </div>
                <div class="MyCarousel-track">
                  ${items.map(
                    (item) => html`
                      <article
                        class="MyCarousel-item"
                        onClick=${() =>
                          sendEvent("click", item, "Product clicked from chat")}
                      >
                        <img src="${item.image}" alt="${item.name}" />
                        <h3>${item.name}</h3>
                        <span>$${item.price}</span>
                      </article>
                    `,
                  )}
                </div>
              </div>
            `;
          },
        },
      },
    },
  }),
]);
```

The `layout` template receives:

* `message`. The tool call message. The result is on `message.output` (`hits`, `nbHits`, `queryID`); the agent's parameters are on `message.input`.
* `sendEvent`. Sends `click` or `conversion` events for the result, using the [`insights`](/doc/api-reference/widgets/insights/js) middleware.
* `applyFilters` and `onClose`. Apply filters to the InstantSearch UI state, or dismiss the tool's UI.

<Note>
  Overriding a tool changes only how the result is **displayed** in the chat. The tool itself—its name, when the agent calls it, and its parameters—is configured on the agent in the [Agent Studio dashboard](/doc/guides/algolia-ai/agent-studio/how-to/dashboard#client-side-tools).
</Note>

## Related

* [chat widget reference](/doc/api-reference/widgets/chat/js)
* [Show starter prompts on the chat welcome screen](/doc/guides/building-search-ui/going-further/chat-customization/welcome-screen/js)
* [insights middleware](/doc/api-reference/widgets/insights/js)
* [Agent Studio](/doc/guides/algolia-ai/agent-studio)
