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

# Control when and where the chat loader shows

> Position the loader, override when it shows, and tune its timing in the InstantSearch.js chat widget.

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/loader-behavior"
    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>

The [`chat`](/doc/api-reference/widgets/chat/js) widget shows a loader while a chat turn is in progress.
You can control:

* Where it appears, with `loaderPosition` or CSS.
* When it appears, with `shouldShowLoader`.
* Its timing, with `loaderShowDelay` and `loaderMinDuration`.

To change what the loader says or how it looks, see [Customize the chat loader](/doc/guides/building-search-ui/going-further/chat-customization/loading-message/js).

## Render the loader inside the streaming message

By default, the loader appears in its own row after the last message.
Set [`loaderPosition`](/doc/api-reference/widgets/chat/js#param-loader-position) to `message-inline` to show it inside the streaming assistant message.
Before the assistant sends its first part, the loader appears in its own row.

```js JavaScript icon=code theme={"system"}
chat({
  container: "#chat",
  agentId: "YOUR_AGENT_ID",
  loaderPosition: "message-inline",
});
```

## Move the loader with CSS

You can use CSS to reposition the default row loader (`loaderPosition: "messages-end"`) within the chat panel.

Show it above the conversation:

```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
  order: -1;
}
```

Keep it in view while the conversation scrolls behind it:

```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
  position: sticky;
  bottom: 0;
}
```

Pin it to the panel instead of the scroll area:

```css CSS icon=paintbrush theme={"system"}
.ais-ChatMessages-content > .ais-ChatMessageLoader {
  position: absolute;
  bottom: var(--ais-spacing);
  left: var(--ais-spacing);
  right: var(--ais-spacing);
  z-index: 1;
}
```

The pinned loader positions itself against `.ais-ChatMessages`, outside the scroll container, so it stays put while messages scroll. It also overlaps the end of the conversation, so add matching `padding-bottom` to `.ais-ChatMessages-scroll`.

CSS can't move the loader outside the message list. To show progress in your header or next to the prompt input, render your own indicator there.

## Change when the loader shows

Use [`shouldShowLoader`](/doc/api-reference/widgets/chat/js#param-should-show-loader) to customize when the loader appears.
Relevant context properties include:

* `status`. The chat status: `submitted`, `streaming`, `ready`, or `error`.
* `phase`. The current phase of the turn: `submitted`, `tool`, `reasoning`, or `thinking`.
* `message`. The assistant message associated with the loader, when available.
* `messages`. The full conversation.
* `tools`. The tools available to the agent.
* `defaultValue`. Whether the widget would show the loader by default.

Return `defaultValue` for the cases you don't handle, so you adjust the built-in behavior instead of rebuilding it.

Keep the loader visible for the whole tool call, including while the tool streams its input:

```js JavaScript icon=code theme={"system"}
chat({
  // ...
  shouldShowLoader: ({ phase, defaultValue }) =>
    phase === "tool" || defaultValue,
});
```

Hide it for a tool that shows its own progress:

```js JavaScript icon=code theme={"system"}
chat({
  // ...
  shouldShowLoader: ({ message, defaultValue }) => {
    const lastPart = message?.parts.at(-1);

    if (lastPart?.type === "tool-YOUR_TOOL_NAME") {
      return false;
    }

    return defaultValue;
  },
});
```

<Note>
  Returning `false` for every turn removes the loader. Give users another way to tell that the agent is working, such as an indicator in your own layout.
</Note>

## Tune the loader timing

During a turn, the loader can hide and reappear as the chat moves between different phases.
Two options control these transitions:

* [`loaderShowDelay`](/doc/api-reference/widgets/chat/js#param-loader-show-delay). How long a renewed loading state must continue before the loader reappears. Defaults to 250 ms. The first time the loader appears in a turn, it isn't delayed.
* [`loaderMinDuration`](/doc/api-reference/widgets/chat/js#param-loader-min-duration). The minimum time the loader remains visible while the turn is running. Defaults to 200 ms. This prevents the loader from briefly appearing and disappearing. When the turn ends, the loader hides immediately.

```js JavaScript icon=code theme={"system"}
chat({
  // ...
  loaderShowDelay: 400,
  loaderMinDuration: 300,
});
```

Raise `loaderShowDelay` if your agent alternates between short tool calls and text.
Set it to `0` to show the loader immediately whenever a loading state starts.

## Support assistive technology and reduced motion

The message list has `aria-busy="true"` while the chat status is `submitted` or `streaming`,
so assistive technology can identify that the response is still being processed.

The default loader's entrance, spinner, and skeleton animations all stop under `prefers-reduced-motion: reduce`. Honor the same media query in your own loader.

## Related

* [chat widget reference](/doc/api-reference/widgets/chat/js)
* [Customize the chat loader](/doc/guides/building-search-ui/going-further/chat-customization/loading-message/js)
* [Style your widgets](/doc/guides/building-search-ui/styling/js)
