# Channel Search

Channel search finds channels based on user input. Depending on your needs, it can find existing conversations, start new ones, or do both.

There are two ways to use the built-in search functionality:

1. by enabling search in the `ChannelList` component with the
   [`showChannelSearch`](/chat/docs/sdk/react/components/core-components/channel_list#showchannelsearch/) prop,
2. or by using the [`ChannelSearch`](/chat/docs/sdk/react/components/utility-components/channel_search/) component directly.

## Best Practices

- Decide early whether search should live inside `ChannelList` or be standalone.
- Scope results to relevant users/channels to avoid noise.
- Keep search result items lightweight for fast input response.
- Preserve keyboard navigation and focus states in custom items.
- Use client queries only when you need full control over ranking.

In both cases, [`ChannelSearch`](/chat/docs/sdk/react/components/utility-components/channel_search/) handles the UI logic.

In the first case, if you're using the search functionality of the
`ChannelList` component, the `ChannelSearch` is rendered by the `ChannelList`.
You can still pass props to the underlying `ChannelSearch` through the
[`additionalChannelSearchProps`](/chat/docs/sdk/react/components/core-components/channel_list#showchannelsearch/).

For example, enable search in `ChannelList` with the
[`showChannelSearch`](/chat/docs/sdk/react/components/core-components/channel_list#showchannelsearch/) prop), and configure the search results to include both channels
and users by passing settings in the [`additionalChannelSearchProps`](/chat/docs/sdk/react/components/core-components/channel_list#showchannelsearch/):

```tsx
<ChannelList
  filters={filters}
  sort={sort}
  options={options}
  showChannelSearch
  additionalChannelSearchProps={{ searchForChannels: true }}
/>
```

In the second case, if you're using the [`ChannelSearch`](/chat/docs/sdk/react/components/utility-components/channel_search/) component directly,
you can pass settings directly as props of the `ChannelSearch` component:

```tsx
<ChannelSearch searchForChannels />
```

## Component Anatomy

The [`ChannelSearch`](/chat/docs/sdk/react/components/utility-components/channel_search/) component consists of the search bar (including the search
input), the results header, and the results list (consisting of individual search
results items).

![](/data/docs/chat-sdk/react/v13-latest/_assets/channel-search-anatomy.png)

Each of these components can be overridden by passing custom components in the
[`ChannelSearch` props](/chat/docs/sdk/react/components/utility-components/channel_search#Props/):

```tsx
<ChannelSearch
  SearchBar={CustomSearchBar}
  SearchInput={CustomInput}
  SearchResultsHeader={CustomHeader}
  SearchResultsList={CustomList}
  SearchResultItem={CustomItem}
/>
```

If you're using the search functionality of the `ChannelList` components, you
can pass the same custom components to the [`additionalChannelSearchProps`](/chat/docs/sdk/react/components/core-components/channel_list#showchannelsearch/):

```tsx
<ChannelList
  filters={filters}
  sort={sort}
  options={options}
  showChannelSearch
  additionalChannelSearchProps={{
    SearchBar: CustomSearchBar,
    SearchInput: CustomInput,
    SearchResultsHeader: CustomHeader,
    SearchResultsList: CustomList,
    SearchResultItem: CustomItem,
  }}
/>
```

Next, we’ll look at a few customization options. You can also explore the built-in [customization options](/chat/docs/sdk/react/components/utility-components/channel_search/) that don’t require custom components.

## Overriding the Search Result Item

You can override the way each search result item is presented by providing a
custom `SearchResultItem`.

```tsx
<ChannelSearch SearchResultItem={CustomSearchResultItem} />
```

Or:

```tsx
<ChannelList
  additionalChannelSearchProps={{
    SearchResultItem: CustomSearchResultItem,
  }}
/>
// Don't forget to provide filter and sort options as well!
```

This component receives a search result item as a prop, which can be either a
`UserResponse` or a `Channel` (if the [`searchForChannels`](/chat/docs/sdk/react/components/utility-components/channel_search#searchforchannels/) option is enabled).

Your custom component should:

1. Display both channel and user search result items.
2. Provide visual feedback for an item focused with the arrow keys. We can do this by
   looking at the `focusUser` prop which contains the index of the currently
   selected item.
3. When clicked, it should invoke the `selectResult` callback.

<codetabs>

<tabs-item value="js" label="React">

```tsx
const CustomSearchResultItem = ({
  result,
  index,
  focusedUser,
  selectResult,
}) => {
  const isChannel = result.cid;

  return (
    <button
      className={`search-result-item ${index === focusedUser ? "search-result-item_focused" : ""}`}
      onClick={() => selectResult(result)}
    >
      {isChannel ? (
        <>
          <span className="search-result-item__icon">#️⃣</span>
          {result.data?.name}
        </>
      ) : (
        <>
          <span className="search-result-item__icon">👤</span>
          {result.name ?? result.id}
        </>
      )}
    </button>
  );
};
```

</tabs-item>

<tabs-item value="css" label="CSS">

```css
.search-result-item {
  font: inherit;
  border: 0;
  background: none;
  padding: 10px 20px 10px 50px;
  text-align: left;
}

.search-result-item:not(:last-child) {
  border-bottom: 1px solid #dbdde1;
}

.search-result-item_focused {
  background: #dbdde1;
}

.search-result-item__icon {
  display: inline-block;
  width: 30px;
  margin-left: -30px;
}
```

</tabs-item>

</codetabs>

![](/data/docs/chat-sdk/react/v13-latest/_assets/channel-search-item.png)

## Implementing Search from Scratch

You don't have to rely on the components provided in the SDK to implement
search. For ultimate customization, it’s not too difficult to implement search
from scratch. You’ll manage state yourself and use the low-level client
methods to query for results, but the upside is that you can manipulate the
results however you like.

See our client documentation to learn how to query for [channels](/chat/docs/react/query_channels/),
[users](/chat/docs/react/update_users/), or [messages](/chat/docs/react/search/). As a quick reference, here are the queries we will
be using:

```js
// Query at most 5 messaging channels where current user is a member,
// by channel name:
const channels = await client.queryChannels(
  {
    type: "messaging",
    name: { $autocomplete: query },
    members: { $in: [userId] },
  },
  { last_message_at: -1, updated_at: -1 },
  { limit: 5 },
);
```

```js
// Query at most 5 users by name or id (filter out own user if you would like to avoid showing it in the results):
const { users } = await client.queryUsers(
  {
    $or: [{ id: { $autocomplete: query } }, { name: { $autocomplete: query } }],
  },
  { id: 1, name: 1 },
  { limit: 5 },
);
```

```js
// Query at most 5 messages from the messaging channels where current user
// is a member, by message text:
const { results } = await client.search(
  { type: "messaging", members: { $in: [userId] } },
  query,
  {
    limit: 5,
  },
);
const messages = results.map((item) => item.message);
```

Next, let's add some simple text input and some buttons to search for channels,
users, or messages:

<codetabs>

<tabs-item value="js" label="React">

```tsx
const CustomSearch = () => {
  const [query, setQuery] = useState("");

  return (
    <div className="search">
      <input
        type="search"
        className="search-input"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      {query && (
        <div className="search-actions">
          <button type="button" className="search-button">
            #️⃣ Find "{query}" channels
          </button>
          <button type="button" className="search-button">
            👤 Find "{query}" users
          </button>
          <button type="button" className="search-button">
            💬 Look up "{query}" in messages
          </button>
        </div>
      )}
    </div>
  );
};
```

</tabs-item>

<tabs-item value="css" label="CSS">

```css
.search-input {
  width: 100%;
  border: 0;
  border-radius: 10px;
  background: #00000014;
  font: inherit;
  padding: 10px 15px;
}

.search-input::-webkit-search-cancel-button {
  appearance: none;
}

.search-actions {
  display: flex;
  flex-direction: column;
  margin: 10px 0 20px;
}

.search-button {
  background: #00000014;
  border: 0;
  border-bottom: 1px solid #dbdde1;
  padding: 10px 15px;
  cursor: pointer;
}

.search-button:first-child {
  border-radius: 10px 10px 0 0;
}

.search-button:last-child {
  border-radius: 0 0 10px 10px;
  border-bottom: 0;
}

.search-button:hover {
  background: #dbdde1;
}
```

</tabs-item>

</codetabs>

![](/data/docs/chat-sdk/react/v13-latest/_assets/channel-search.png)

So far, our `CustomSearch` component doesn't do anything. Let's wire things up
by adding click event listeners to the search buttons.

<details>
<summary><strong>A note about race conditions</strong></summary>

One thing we should be aware of is race conditions: we should either abort
or discard the results of the previous request when making a new one, or prevent a
user from making multiple requests at once. Better yet, use a query
library like [TanStack Query](https://tanstack.com/query/latest) or
[SWR](https://swr.vercel.app/) to make requests.

In this example, we will use a helper function that will protect us
from race conditions:

```js
function useSearchQuery() {
  const [results, setResults] = useState(null);
  const [pending, setPending] = useState(false);
  const pendingRequestAbortController = useRef(null);

  const startNextRequestWithSignal = () => {
    pendingRequestAbortController.current?.abort();
    pendingRequestAbortController.current = new AbortController();
    return pendingRequestAbortController.current.signal;
  };

  const querySearchResults = async (fether) => {
    setPending(true);
    const signal = startNextRequestWithSignal();
    const results = await fether();

    if (!signal.aborted) {
      setResults(results);
      setPending(false);
    }
  };

  return { results, pending, querySearchResults };
}
```

</details>

<admonition type="warning">

If you're implementing the "search as you type" user experience,
remember to debounce or throttle your search requests. Otherwise, you can quickly
hit rate limits.

</admonition>

```tsx
import { useChatContext } from "stream-chat-react";

const CustomSearch = () => {
  const { client } = useChatContext();
  const [query, setQuery] = useState("");
  const { results, pending, querySearchResults } = useSearchQuery();

  const handleChannelSearchClick = () => {
    querySearchResults(async () => {
      const channels = await client.queryChannels(
        {
          type: "messaging",
          name: { $autocomplete: query },
          members: { $in: [userId] },
        },
        { last_message_at: -1, updated_at: -1 },
        { limit: 5 },
      );

      return {
        entity: "channel",
        items: channels,
      };
    });
  };

  const handleUserSearchClick = () => {
    querySearchResults(async () => {
      const { users } = await client.queryUsers(
        {
          $or: [
            { id: { $autocomplete: query } },
            { name: { $autocomplete: query } },
          ],
        },
        { id: 1, name: 1 },
        { limit: 5 },
      );

      return {
        entity: "user",
        items: users,
      };
    });
  };

  const handleMessageSearchClick = () => {
    querySearchResults(async () => {
      const { results } = await client.search(
        { type: "messaging", members: { $in: [userId] } },
        query,
        { limit: 5 },
      );

      return {
        entity: "message",
        items: results.map((item) => item.message),
      };
    });
  };

  return (
    <div className="search">
      <input
        type="search"
        className="search-input"
        value={query}
        onChange={(event) => setQuery(event.target.value)}
      />
      {query && (
        <div className="search-actions">
          <button
            type="button"
            className="search-button"
            onClick={handleChannelSearchClick}
          >
            #️⃣ Find <strong>"{query}"</strong> channels
          </button>
          <button
            type="button"
            className="search-button"
            onClick={handleUserSearchClick}
          >
            👤 Find <strong>"{query}"</strong> users
          </button>
          <button
            type="button"
            className="search-button"
            onClick={handleMessageSearchClick}
          >
            💬 Look up <strong>"{query}"</strong> in messages
          </button>
        </div>
      )}
    </div>
  );
};
```

Finally, we need to display the search results to the user. You can use components
like `ChannelPreview` that come with the SDK, or you can create your own. Let's
create very simple preview components for channels, users, and messages:

<codetabs>

<tabs-item value="js" label="React">

```tsx
const ChannelSearchResultPreview = ({ channel }) => (
  <li className="search-results__item">
    <div className="search-results__icon">#️⃣</div>
    {channel.data?.name}
  </li>
);

const UserSearchResultPreview = ({ user }) => (
  <li className="search-results__item">
    <div className="search-results__icon">👤</div>
    {user.name ?? user.id}
  </li>
);

const MessageSearchResultPreview = ({ message }) => (
  <li className="search-results__item">
    <div className="search-results__icon">💬</div>
    {message.text}
  </li>
);

const SearchResultsPreview = ({ results }) => {
  if (results.items.length === 0) {
    return <div class="search-results">🤷‍♂️ No results</div>;
  }

  return (
    <ul className="search-results">
      {results.entity === "channel" &&
        results.items.map((item) => (
          <ChannelSearchResultPreview key={item.cid} channel={item} />
        ))}
      {results.entity === "user" &&
        results.items.map((item) => (
          <UserSearchResultPreview key={item.id} user={item} />
        ))}
      {results.entity === "message" &&
        results.items.map((item) => (
          <MessageSearchResultPreview key={item.id} message={item} />
        ))}
    </ul>
  );
};
```

</tabs-item>

<tabs-item value="css" label="CSS">

```css
.search-results {
  list-style: none;
  padding: 0;
  margin: 0;
}

.search-results__item {
  padding-left: 30px;
}

.search-results__item:not(:last-child) {
  margin-bottom: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid #dbdde1;
}

.search-results__icon {
  display: inline-block;
  width: 30px;
  margin-left: -30px;
}
```

</tabs-item>

</codetabs>

![](/data/docs/chat-sdk/react/v13-latest/_assets/channel-search-channels.png)

![](/data/docs/chat-sdk/react/v13-latest/_assets/channel-search-users.png)

![](/data/docs/chat-sdk/react/v13-latest/_assets/channel-search-messages.png)

What happens when you click on a search result depends on your desired user
experience. If you click on a channel, it makes sense to set the channel as
active. When clicking on a user, you may want to create or open a channel with
a one-on-one conversation with the user. When clicking on a message, it's
probably expected that a relevant channel will be set as active and scrolled to the
message.

<codetabs>

<tabs-item value="channel" label="Channel Preview">

```tsx
import { useChatContext } from "stream-chat-react";

const ChannelSearchResultPreview = ({ channel }) => {
  const { setActiveChannel } = useChatContext();

  return (
    <li
      className="search-results__item"
      onClick={() => setActiveChannel(channel)}
    >
      <div className="search-results__icon">#️⃣</div>
      {channel.data?.name}
    </li>
  );
};
```

</tabs-item>

<tabs-item value="user" label="User Preview">

```tsx
import { useChatContext } from "stream-chat-react";

const UserSearchResultPreview = ({ user }) => {
  const { client, setActiveChannel } = useChatContext();

  const handleClick = async () => {
    const channel = client.channel("messaging", { members: [userId, user.id] });
    await channel.watch();
    setActiveChannel(channel);
  };

  return (
    <li className="search-results__item" onClick={handleClick}>
      <div className="search-results__icon">👤</div>
      {user.name ?? user.id}
    </li>
  );
};
```

</tabs-item>

<tabs-item value="message" label="Message Preview">

```tsx
import { useChatContext } from "stream-chat-react";

const MessageSearchResultPreview = ({ message }) => {
  const history = useHistory(); // bring your own router of choice
  const { client, setActiveChannel } = useChatContext();

  const handleClick = async () => {
    if (message.channel) {
      const channel = client.channel(message.channel.type, message.channel.id);
      setActiveChannel(channel);
      await channel.state.loadMessageIntoState(message.id);
      history.replace(`${window.location.pathname}#${message.id}`);
    }
  };

  return (
    <li className="search-results__item" onClick={handleClick}>
      <div className="search-results__icon">💬</div>
      {message.text}
    </li>
  );
};

// Somewhere in your application code:
const location = useLocation();
const messageId = useMemo(() => new URL(location).hash.slice(1), [location]);
<MessageList highlightedMessageId={messageId} />;
```

</tabs-item>

</codetabs>

And that's it! Here's the complete code:

<codetabs>

<tabs-item value="js" label="React">

```tsx
import { useChatContext } from "stream-chat-react";

const CustomSearch = () => {
  const { client } = useChatContext();
  const [query, setQuery] = useState("");
  const { results, pending, querySearchResults } = useSearchQuery();
  // Use your favorite query library here 👆

  const handleChannelSearchClick = async () => {
    querySearchResults(async () => {
      const channels = await client.queryChannels(
        {
          type: "messaging",
          name: { $autocomplete: query },
          members: { $in: [userId] },
        },
        { last_message_at: -1, updated_at: -1 },
        { limit: 5 },
      );

      return {
        entity: "channel",
        items: channels,
      };
    });
  };

  const handleUserSearchClick = async () => {
    querySearchResults(async () => {
      const { users } = await client.queryUsers(
        {
          $or: [
            { id: { $autocomplete: query } },
            { name: { $autocomplete: query } },
          ],
        },
        { id: 1, name: 1 },
        { limit: 5 },
      );

      return {
        entity: "user",
        items: users,
      };
    });
  };

  const handleMessageSearchClick = async () => {
    querySearchResults(async () => {
      const { results } = await client.search(
        { type: "messaging", members: { $in: [userId] } },
        query,
        { limit: 5 },
      );

      return {
        entity: "message",
        items: results.map((item) => item.message),
      };
    });
  };

  return (
    <div className="search">
      <input
        type="search"
        className="search-input"
        value={query}
        placeholder="Search"
        onChange={(event) => setQuery(event.target.value)}
      />
      {query && (
        <div className="search-actions">
          <button
            type="button"
            className="search-button"
            onClick={handleChannelSearchClick}
          >
            #️⃣ Find <strong>"{query}"</strong> channels
          </button>
          <button
            type="button"
            className="search-button"
            onClick={handleUserSearchClick}
          >
            👤 Find <strong>"{query}"</strong> users
          </button>
          <button
            type="button"
            className="search-button"
            onClick={handleMessageSearchClick}
          >
            💬 Look up <strong>"{query}"</strong> in messages
          </button>
        </div>
      )}

      {pending && <>Searching...</>}
      {results && <SearchResultsPreview results={results} />}
    </div>
  );
};

const ChannelSearchResultPreview = ({ channel }) => {
  const { setActiveChannel } = useChatContext();

  return (
    <li
      className="search-results__item"
      onClick={() => setActiveChannel(channel)}
    >
      <div className="search-results__icon">#️⃣</div>
      {channel.data?.name}
    </li>
  );
};

const UserSearchResultPreview = ({ user }) => {
  const { client, setActiveChannel } = useChatContext();

  const handleClick = async () => {
    const channel = client.channel("messaging", { members: [userId, user.id] });
    await channel.watch();
    setActiveChannel(channel);
  };

  return (
    <li className="search-results__item" onClick={handleClick}>
      <div className="search-results__icon">👤</div>
      {user.name ?? user.id}
    </li>
  );
};

const MessageSearchResultPreview = ({ message }) => {
  const history = useHistory(); // bring your own router of choice
  const { client, setActiveChannel } = useChatContext();

  const handleClick = async () => {
    if (message.channel) {
      const channel = client.channel(message.channel.type, message.channel.id);
      setActiveChannel(channel);
      await channel.state.loadMessageIntoState(message.id);
      history.replace(`${window.location.pathname}#${message.id}`);
    }
  };

  return (
    <li className="search-results__item" onClick={handleClick}>
      <div className="search-results__icon">💬</div>
      {message.text}
    </li>
  );
};

const SearchResultsPreview = ({ results }) => {
  if (results.items.length === 0) {
    return <>No results</>;
  }

  return (
    <ul className="search-results">
      {results.entity === "channel" &&
        results.items.map((item) => (
          <ChannelSearchResultPreview key={item.cid} channel={item} />
        ))}
      {results.entity === "user" &&
        results.items.map((item) => (
          <UserSearchResultPreview key={item.id} user={item} />
        ))}
      {results.entity === "message" &&
        results.items.map((item) => (
          <MessageSearchResultPreview key={item.id} message={item} />
        ))}
    </ul>
  );
};
```

</tabs-item>

<tabs-item value="css" label="CSS">

```css
.search-input {
  width: 100%;
  border: 0;
  border-radius: 10px;
  background: #00000014;
  font: inherit;
  padding: 10px 15px;
}

.search-input::-webkit-search-cancel-button {
  appearance: none;
}

.search-actions {
  display: flex;
  flex-direction: column;
  margin: 10px 0 20px;
}

.search-button {
  background: #00000014;
  border: 0;
  border-bottom: 1px solid #dbdde1;
  padding: 10px 15px;
  cursor: pointer;
}

.search-button:first-child {
  border-radius: 10px 10px 0 0;
}

.search-button:last-child {
  border-radius: 0 0 10px 10px;
  border-bottom: 0;
}

.search-button:hover {
  background: #dbdde1;
}

.search-results {
  list-style: none;
  padding: 0;
  margin: 0;
}

.search-results__item {
  padding-left: 30px;
}

.search-results__item:not(:last-child) {
  margin-bottom: 10px;
  padding-bottom: 10px;
  border-bottom: 1px solid #dbdde1;
}

.search-results__icon {
  display: inline-block;
  width: 30px;
  margin-left: -30px;
}
```

</tabs-item>

</codetabs>


---

This page was last updated at 2026-03-13T13:15:41.163Z.

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/guides/customization/channel_search/](https://getstream.io/chat/docs/sdk/react/guides/customization/channel_search/).