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

# Courier React SDK

> Embed a customizable in-app notification center, toasts, and notification preferences in your React app. Courier React components seamlessly integrate with email, SMS, push, and more.

<img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-react-banner.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=e6726bfe5e69eb7c8e2c2c319483b086" alt="Courier React Inbox" width="2080" height="1000" data-path="assets/sdks/courier-inbox-react-banner.png" />

The Courier React SDK provides ready-made components and programmatic hooks for building notification experiences:

* `<CourierInbox />` — full-featured inbox for displaying and managing messages
* `<CourierInboxPopupMenu />` — popup menu version of the inbox
* `<CourierToast />` — toast notifications for time-sensitive alerts
* `<CourierPreferences />` — notification preferences center for managing topic subscriptions and delivery
* `useCourier()` — hook for programmatic access and custom UIs

<Tip>
  See these components in action with the [interactive Inbox demo](https://www.courier.com/inbox-demo) — no setup required.
</Tip>

## Installation

Available on
<Link href="https://github.com/trycourier/courier-web/tree/main/%40trycourier/courier-react"><Icon icon="github" iconType="solid" /> GitHub</Link>
and <Link href="https://www.npmjs.com/package/@trycourier/courier-react"><Icon icon="npm" iconType="solid" /> npm</Link>.

Courier publishes two React packages: `@trycourier/courier-react` for React 18+ and `@trycourier/courier-react-17` for React 17.

<CodeGroup>
  ```bash React 18+ theme={null}
  npm install @trycourier/courier-react
  ```

  ```bash React 17 theme={null}
  npm install @trycourier/courier-react-17
  ```
</CodeGroup>

<Tip>
  This is the latest version of the Courier React SDK, recommended for new and existing apps.

  If you're coming from an earlier version of the Courier React SDK, check out the
  [v8 migration guide](/sdk-libraries/courier-react-v8-migration-guide) for what's changed,
  how to upgrade your app, and links to documentation for past versions of the React SDK.
</Tip>

<Note>
  Not using React? Check out the
  [@trycourier/courier-ui-inbox](/sdk-libraries/courier-ui-inbox-web/) and
  [@trycourier/courier-ui-toast](/sdk-libraries/courier-ui-inbox-web#toast-web-components)
  packages instead, which provide Web Components for any JavaScript project.
</Note>

## Authentication

To use the SDK, you need to generate a JWT (JSON Web Token) for your user. **This JWT should always be generated by your backend server, never in client-side code.**

<Steps>
  <Step title="Your client calls your backend">
    When your app needs to authenticate a user, your client
    should make a request to your own backend (ex. `GET https://your-awesome-app.com/api/generate-courier-jwt`).
  </Step>

  <Step title="Your backend calls Courier">
    In your backend endpoint, use your [Courier API Key](/platform/workspaces/environments-api-keys.mdx) to call the [Courier Issue Token Endpoint](/api-reference/authentication/create-a-jwt) and generate a JWT for the user.
  </Step>

  <Step title="Your backend returns the JWT to your client">
    Having received the JWT from Courier, your backend should return it to your client and pass it to the Courier SDK.
  </Step>
</Steps>

<Note>
  For a step-by-step walkthrough of authentication and token generation, see our <Link href="/tutorials/inbox/how-to-send-jwt"> JWT authentication tutorial</Link>.
</Note>

### Development testing with cURL

To quickly test JWT generation for development only, you can call the [Issue Token Endpoint](/api-reference/authentication/create-a-jwt) directly.

<Warning>
  Do not call the Issue Token API from client-side code. Always keep your Courier API keys secure.
</Warning>

```bash theme={null}
curl -X POST https://api.courier.com/auth/issue-token \
  -H 'Authorization: Bearer $YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "scope": "user_id:$YOUR_USER_ID inbox:read:messages inbox:write:events",
    "expires_in": "1 day"
  }'
```

## Quick Start

Get up and running with Courier React in minutes. This minimal example shows how to add the inbox component to your app.

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import { CourierInbox, useCourier } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      // Generate a JWT for your user on your backend server
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";

      // Authenticate the user
      courier.shared.signIn({
        userId: $YOUR_USER_ID,
        jwt: jwt,
      });
    }, []);

    return <CourierInbox />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import { CourierInbox, useCourier } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      // Generate a JWT for your user on your backend server
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";

      // Authenticate the user
      courier.shared.signIn({
        userId: $YOUR_USER_ID,
        jwt: jwt,
      });
    }, []);

    return <CourierInbox />;
  }
  ```
</CodeGroup>

<Tip>
  Follow the [inbox implementation tutorial](/tutorials/inbox/how-to-implement-inbox) for detailed step-by-step guidance including backend JWT generation.
</Tip>

## Inbox Component

### `<CourierInbox />`

<Frame caption="Default CourierInbox component">
  <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-react-preview.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=83e9f4165bdc1a9d3ef87b20e38511f2" alt="Preview of the default CourierInbox component" width="1376" height="1218" data-path="assets/sdks/courier-inbox-react-preview.png" />
</Frame>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import { CourierInbox, useCourier } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return <CourierInbox />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import { CourierInbox, useCourier } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return <CourierInbox />;
  }
  ```
</CodeGroup>

<Note>
  If you're using [tenants](/platform/tenants/tenants-overview), scope requests to a particular
  tenant by passing its ID to `signIn`:

  ```js theme={null}
  courier.shared.signIn({ userId: "my-user-id", jwt, tenantId: "my-tenant-id" });
  ```

  For the full reference of sign in parameters, see the [Courier JS docs](/sdk-libraries/courier-js-web#courierclient-options).
</Note>

<Tip>
  **Getting Started**: Follow the step-by-step [inbox implementation tutorial](/tutorials/inbox/how-to-implement-inbox), or see a complete working example in our [React Inbox sample app](https://github.com/trycourier/courier-samples/tree/main/web/react/inbox).
</Tip>

***

### `<CourierInboxPopupMenu />`

<Frame caption="Default CourierInboxPopupMenu component">
  <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-popupmenu.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=46706ec548f8cbba7e0820ab41355945" alt="Preview of the default CourierInboxPopupMenu component" width="1210" height="1346" data-path="assets/sdks/courier-inbox-popupmenu.png" />
</Frame>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import { CourierInboxPopupMenu, useCourier } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return (
      <div style={{ padding: "24px" }}>
        <CourierInboxPopupMenu />
      </div>
    );
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import { CourierInboxPopupMenu, useCourier } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return (
      <div style={{ padding: "24px" }}>
        <CourierInboxPopupMenu />
      </div>
    );
  }
  ```
</CodeGroup>

***

## Tabs and Feeds

Tabs and feeds organize and filter messages in the inbox. A **feed** is a container that groups related **tabs** together. Each tab applies filters to show relevant messages.

<Note>
  If there is only one feed, the feed selection dropdown is hidden. If a feed has only one tab, the tab bar is hidden and the unread count appears next to the feed.
</Note>

Filter options for each tab:

| Filter Property | Type                 | Description                                                         |
| :-------------- | :------------------- | :------------------------------------------------------------------ |
| `tags`          | `string[]`           | Messages that have any of the specified tags                        |
| `archived`      | `boolean`            | Whether to include archived messages (defaults to `false` if unset) |
| `status`        | `'read' \| 'unread'` | Filter by read/unread status                                        |

<Frame caption="CourierInbox with many tabs">
  <img src="https://mintcdn.com/courier-4f1f25dc/wrizTlDlT-IGTzZg/assets/sdks/courier-react-tabs.png?fit=max&auto=format&n=wrizTlDlT-IGTzZg&q=85&s=5496c204ed302a8c80e11a9c90aa2670" alt="Preview of CourierInbox with tabs" width="1404" height="1244" data-path="assets/sdks/courier-react-tabs.png" />
</Frame>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import {
    CourierInbox,
    useCourier,
    type CourierInboxFeed,
  } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    const feeds: CourierInboxFeed[] = [
      {
        feedId: 'notifications',
        title: 'Notifications',
        tabs: [
          { datasetId: 'all-notifications', title: 'All', filter: {} },
          { datasetId: 'unread-notifications', title: 'Unread', filter: { status: 'unread' } },
          { datasetId: 'important', title: 'Important', filter: { tags: ['important'] } },
          { datasetId: 'archived', title: 'Archived', filter: { archived: true } }
        ]
      }
    ];

    return <CourierInbox feeds={feeds} />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import {
    CourierInbox,
    useCourier,
    type CourierInboxFeed,
  } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    const feeds: CourierInboxFeed[] = [
      {
        feedId: 'notifications',
        title: 'Notifications',
        tabs: [
          { datasetId: 'all-notifications', title: 'All', filter: {} },
          { datasetId: 'unread-notifications', title: 'Unread', filter: { status: 'unread' } },
          { datasetId: 'important', title: 'Important', filter: { tags: ['important'] } },
          { datasetId: 'archived', title: 'Archived', filter: { archived: true } }
        ]
      }
    ];

    return <CourierInbox feeds={feeds} />;
  }
  ```
</CodeGroup>

You can also define **multiple feeds** to organize messages into different categories. Each feed appears as a selectable option in the inbox header.

<Frame caption="CourierInbox with many feeds">
  <img src="https://mintcdn.com/courier-4f1f25dc/pC9XFZDnjT-hDjXv/assets/sdks/courier-react-feeds.png?fit=max&auto=format&n=pC9XFZDnjT-hDjXv&q=85&s=93e49653f674daed0976a6a93f5907d3" alt="Preview of CourierInbox with feeds" width="1376" height="1218" data-path="assets/sdks/courier-react-feeds.png" />
</Frame>

<Expandable title="Multiple feeds example">
  ```jsx theme={null}
  const feeds: CourierInboxFeed[] = [
    {
      feedId: 'all',
      title: 'All',
      tabs: [{ datasetId: 'all', title: 'All', filter: {} }]
    },
    {
      feedId: 'jobs',
      title: 'Jobs',
      tabs: [{ datasetId: 'jobs', title: 'Jobs', filter: { tags: ['job'] } }]
    },
    {
      feedId: 'my-posts',
      title: 'My Posts',
      tabs: [
        { datasetId: 'all-my-posts', title: 'All', filter: {} },
        { datasetId: 'comments', title: 'Comments', filter: { tags: ['comment'] } },
        { datasetId: 'reactions', title: 'Reactions', filter: { tags: ['reaction'] } },
        { datasetId: 'reposts', title: 'Reposts', filter: { tags: ['repost'] } }
      ]
    },
    {
      feedId: 'mentions',
      title: 'Mentions',
      tabs: [{ datasetId: 'mentions', title: 'Mentions', filter: { tags: ['mention'] } }]
    }
  ];
  ```
</Expandable>

***

### Handle Clicks and Presses

| Callback Prop          | Type Signature                                            |
| :--------------------- | :-------------------------------------------------------- |
| `onMessageClick`       | `(props: CourierInboxListItemFactoryProps) => void`       |
| `onMessageActionClick` | `(props: CourierInboxListItemActionFactoryProps) => void` |
| `onMessageLongPress`   | `(props: CourierInboxListItemFactoryProps) => void`       |

<Tip>
  `onMessageLongPress` is only applicable on devices that support touch events.
</Tip>

<CodeGroup>
  ```jsx React 18+ lines highlight=14-31 theme={null}
  import { useEffect } from "react";
  import {
    CourierInbox,
    useCourier,
    type CourierInboxListItemFactoryProps,
    type CourierInboxListItemActionFactoryProps
  } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return (
      <CourierInbox
        onMessageClick={({ message, index }: CourierInboxListItemFactoryProps) => {
          alert(`Message clicked at index ${index}:\n${JSON.stringify(message, null, 2)}`);
        }}
        onMessageActionClick={({ message, action, index }: CourierInboxListItemActionFactoryProps) => {
          alert(`Action clicked: ${JSON.stringify(action, null, 2)}`);
        }}
        onMessageLongPress={({ message, index }: CourierInboxListItemFactoryProps) => {
          alert(`Message long pressed at index ${index}`);
        }}
      />
    );
  }
  ```

  ```jsx React 17 lines highlight=14-31 theme={null}
  import { useEffect } from "react";
  import {
    CourierInbox,
    useCourier,
    type CourierInboxListItemFactoryProps,
    type CourierInboxListItemActionFactoryProps
  } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return (
      <CourierInbox
        onMessageClick={({ message, index }: CourierInboxListItemFactoryProps) => {
          alert(`Message clicked at index ${index}:\n${JSON.stringify(message, null, 2)}`);
        }}
        onMessageActionClick={({ message, action, index }: CourierInboxListItemActionFactoryProps) => {
          alert(`Action clicked: ${JSON.stringify(action, null, 2)}`);
        }}
        onMessageLongPress={({ message, index }: CourierInboxListItemFactoryProps) => {
          alert(`Message long pressed at index ${index}`);
        }}
      />
    );
  }
  ```
</CodeGroup>

***

### Styles and Theming

Customize the inbox to match your app with a theme object. You can customize fonts, icons, text, and more.

<Frame caption="Courier React Inbox with a custom unread indicator style">
  <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-theme.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=69a5d3d6a364a05202eaa29eb5d09ac6" alt="Courier React Inbox with a custom unread indicator style" width="1376" height="1218" data-path="assets/sdks/courier-inbox-theme.png" />
</Frame>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { CourierInbox, type CourierInboxTheme } from "@trycourier/courier-react";

  export default function App() {
    // Authentication code...

    const theme: CourierInboxTheme = {
      inbox: {
        header: {
          filters: { unreadIndicator: { backgroundColor: "#8B5CF6" } },
        },
        list: {
          item: { unreadIndicatorColor: "#8B5CF6" },
        },
      },
    };

    return <CourierInbox lightTheme={theme} darkTheme={theme} mode="light" />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { CourierInbox, type CourierInboxTheme } from "@trycourier/courier-react-17";

  export default function App() {
    // Authentication code...

    const theme: CourierInboxTheme = {
      inbox: {
        header: {
          filters: { unreadIndicator: { backgroundColor: "#8B5CF6" } },
        },
        list: {
          item: { unreadIndicatorColor: "#8B5CF6" },
        },
      },
    };

    return <CourierInbox lightTheme={theme} darkTheme={theme} mode="light" />;
  }
  ```
</CodeGroup>

Theme utilities: `defaultLightTheme` / `defaultDarkTheme` provide the default inbox themes, and `mergeTheme(baseTheme, overrideTheme)` merges two themes with the override taking precedence.

```jsx theme={null}
import { defaultLightTheme, mergeTheme, type CourierInboxTheme } from "@trycourier/courier-react";

const customTheme: CourierInboxTheme = {
  inbox: { list: { item: { unreadIndicatorColor: "#8B5CF6" } } },
};

const mergedTheme = mergeTheme(defaultLightTheme, customTheme);
```

The full `CourierInboxTheme` type is below. Every property is optional; only override what you need. It covers the popup trigger button, the inbox window (header, feeds, tabs, actions), the message list (items, scrollbar, menus), and loading/empty/error states.

<Accordion title="CourierInboxTheme Reference">
  ```typescript theme={null}
  export type CourierInboxTheme = {
    popup?: {
      button?: {
        icon?: { color?: string; svg?: string };
        backgroundColor?: string;
        hoverBackgroundColor?: string;
        activeBackgroundColor?: string;
        unreadDotIndicator?: {
          backgroundColor?: string;
          borderRadius?: string;
          height?: string;
          width?: string;
        };
      };
      window?: {
        backgroundColor?: string;
        borderRadius?: string;
        border?: string;
        shadow?: string;
        animation?: {
          transition?: string;
          initialTransform?: string;
          visibleTransform?: string;
        };
      };
    };
    inbox?: {
      header?: {
        backgroundColor?: string;
        shadow?: string;
        border?: string;
        feeds?: {
          button?: {
            selectedFeedIconColor?: string;
            font?: { family?: string; weight?: string; size?: string; color?: string };
            changeFeedIcon?: { color?: string; svg?: string };
            unreadCountIndicator?: {
              font?: { family?: string; weight?: string; size?: string; color?: string };
              backgroundColor?: string;
              borderRadius?: string;
              padding?: string;
            };
            hoverBackgroundColor?: string;
            activeBackgroundColor?: string;
            transition?: string;
          };
          menu?: {
            backgroundColor?: string;
            border?: string;
            borderRadius?: string;
            shadow?: string;
            animation?: {
              transition?: string;
              initialTransform?: string;
              visibleTransform?: string;
            };
            list?: {
              font?: { family?: string; weight?: string; size?: string; color?: string };
              selectedIcon?: { color?: string; svg?: string };
              hoverBackgroundColor?: string;
              activeBackgroundColor?: string;
              divider?: string;
            };
          };
          tabs?: {
            borderRadius?: string | {
              topLeft?: string;
              topRight?: string;
              bottomLeft?: string;
              bottomRight?: string;
            };
            transition?: string;
            default?: {
              backgroundColor?: string;
              hoverBackgroundColor?: string;
              activeBackgroundColor?: string;
              font?: { family?: string; weight?: string; size?: string; color?: string };
              indicatorColor?: string;
              indicatorHeight?: string;
              unreadIndicator?: {
                font?: { family?: string; weight?: string; size?: string; color?: string };
                backgroundColor?: string;
                borderRadius?: string;
                padding?: string;
              };
            };
            selected?: {
              backgroundColor?: string;
              hoverBackgroundColor?: string;
              activeBackgroundColor?: string;
              font?: { family?: string; weight?: string; size?: string; color?: string };
              indicatorColor?: string;
              indicatorHeight?: string;
              unreadIndicator?: {
                font?: { family?: string; weight?: string; size?: string; color?: string };
                backgroundColor?: string;
                borderRadius?: string;
                padding?: string;
              };
            };
          };
        };
        tabs?: {
          borderRadius?: string | {
            topLeft?: string;
            topRight?: string;
            bottomLeft?: string;
            bottomRight?: string;
          };
          transition?: string;
          default?: {
            backgroundColor?: string;
            hoverBackgroundColor?: string;
            activeBackgroundColor?: string;
            font?: { family?: string; weight?: string; size?: string; color?: string };
            indicatorColor?: string;
            indicatorHeight?: string;
            unreadIndicator?: {
              font?: { family?: string; weight?: string; size?: string; color?: string };
              backgroundColor?: string;
              borderRadius?: string;
              padding?: string;
            };
          };
          selected?: {
            backgroundColor?: string;
            hoverBackgroundColor?: string;
            activeBackgroundColor?: string;
            font?: { family?: string; weight?: string; size?: string; color?: string };
            indicatorColor?: string;
            indicatorHeight?: string;
            unreadIndicator?: {
              font?: { family?: string; weight?: string; size?: string; color?: string };
              backgroundColor?: string;
              borderRadius?: string;
              padding?: string;
            };
          };
        };
        actions?: {
          button?: {
            icon?: { color?: string; svg?: string };
            backgroundColor?: string;
            hoverBackgroundColor?: string;
            activeBackgroundColor?: string;
          };
          markAllRead?: { icon?: { color?: string; svg?: string }; text?: string };
          archiveAll?: { icon?: { color?: string; svg?: string }; text?: string };
          archiveRead?: { icon?: { color?: string; svg?: string }; text?: string };
          animation?: {
            transition?: string;
            initialTransform?: string;
            visibleTransform?: string;
          };
          menu?: {
            backgroundColor?: string;
            border?: string;
            borderRadius?: string;
            shadow?: string;
            animation?: {
              transition?: string;
              initialTransform?: string;
              visibleTransform?: string;
            };
            list?: {
              font?: { family?: string; weight?: string; size?: string; color?: string };
              selectedIcon?: { color?: string; svg?: string };
              hoverBackgroundColor?: string;
              activeBackgroundColor?: string;
              divider?: string;
            };
          };
        };
      };
      list?: {
        backgroundColor?: string;
        scrollbar?: {
          trackBackgroundColor?: string;
          thumbColor?: string;
          thumbHoverColor?: string;
          width?: string;
          height?: string;
          borderRadius?: string;
        };
        item?: {
          unreadIndicatorColor?: string;
          backgroundColor?: string;
          hoverBackgroundColor?: string;
          activeBackgroundColor?: string;
          transition?: string;
          title?: { family?: string; weight?: string; size?: string; color?: string };
          subtitle?: { family?: string; weight?: string; size?: string; color?: string };
          time?: { family?: string; weight?: string; size?: string; color?: string };
          archiveIcon?: { color?: string; svg?: string };
          divider?: string;
          actions?: {
            backgroundColor?: string;
            hoverBackgroundColor?: string;
            activeBackgroundColor?: string;
            border?: string;
            borderRadius?: string;
            shadow?: string;
            font?: { family?: string; weight?: string; size?: string; color?: string };
          };
          menu?: {
            enabled?: boolean;
            backgroundColor?: string;
            border?: string;
            borderRadius?: string;
            shadow?: string;
            animation?: {
              transition?: string;
              initialTransform?: string;
              visibleTransform?: string;
            };
            longPress?: { displayDuration?: number; vibrationDuration?: number };
            item?: {
              hoverBackgroundColor?: string;
              activeBackgroundColor?: string;
              borderRadius?: string;
              read?: { color?: string; svg?: string };
              unread?: { color?: string; svg?: string };
              archive?: { color?: string; svg?: string };
              unarchive?: { color?: string; svg?: string };
            };
          };
        };
      };
      loading?: {
        animation?: {
          barColor?: string;
          barHeight?: string;
          barBorderRadius?: string;
          duration?: string;
        };
        divider?: string;
      };
      empty?: {
        title?: {
          font?: { family?: string; weight?: string; size?: string; color?: string };
          text?: string;
        };
        button?: {
          font?: { family?: string; weight?: string; size?: string; color?: string };
          text?: string;
          shadow?: string;
          border?: string;
          borderRadius?: string;
          backgroundColor?: string;
          hoverBackgroundColor?: string;
          activeBackgroundColor?: string;
        };
      };
      error?: {
        title?: {
          font?: { family?: string; weight?: string; size?: string; color?: string };
          text?: string;
        };
        button?: {
          font?: { family?: string; weight?: string; size?: string; color?: string };
          text?: string;
          shadow?: string;
          border?: string;
          borderRadius?: string;
          backgroundColor?: string;
          hoverBackgroundColor?: string;
          activeBackgroundColor?: string;
        };
      };
    };
  };
  ```
</Accordion>

### Popup alignment and dimensions

<Frame caption="Customizing the popup menu's alignment, position, and dimensions">
  <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-popupmenu-position.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=38df5e6240fda84069c666ebe681d189" alt="Customizing the popup menu's alignment, position, and dimensions" width="1096" height="1266" data-path="assets/sdks/courier-inbox-popupmenu-position.png" />
</Frame>

| Vertical Alignment | Options                                              |
| :----------------- | :--------------------------------------------------- |
| Top                | `"top-right"`, `"top-center"`, `"top-left"`          |
| Center             | `"center-right"`, `"center-center"`, `"center-left"` |
| Bottom             | `"bottom-right"`, `"bottom-center"`, `"bottom-left"` |

```jsx theme={null}
<CourierInboxPopupMenu
  popupAlignment="top-left"
  popupWidth="340px"
  popupHeight="400px"
  top="44px"
  left="44px"
/>
```

#### Fixed height

`<CourierInbox />` has a default height of `auto`. Set a fixed height with the `height` prop:

```jsx theme={null}
<CourierInbox height="50vh" />
```

***

### Custom Elements

You can customize individual parts of the inbox by passing render props. Each render prop receives typed props and returns a `ReactNode`.

| Render Prop            | Type Signature                                                 |
| :--------------------- | :------------------------------------------------------------- |
| `renderListItem`       | `(props: CourierInboxListItemFactoryProps) => ReactNode`       |
| `renderHeader`         | `(props: CourierInboxHeaderFactoryProps) => ReactNode`         |
| `renderMenuButton`     | `(props: CourierInboxMenuButtonFactoryProps) => ReactNode`     |
| `renderLoadingState`   | `(props: CourierInboxStateLoadingFactoryProps) => ReactNode`   |
| `renderEmptyState`     | `(props: CourierInboxStateEmptyFactoryProps) => ReactNode`     |
| `renderErrorState`     | `(props: CourierInboxStateErrorFactoryProps) => ReactNode`     |
| `renderPaginationItem` | `(props: CourierInboxPaginationItemFactoryProps) => ReactNode` |

You can also use React refs (`CourierInboxElement`, `CourierInboxPopupMenuElement`) for programmatic access to component methods like `removeHeader()`.

<Tip>
  Subsequent pages of messages are loaded automatically when the user scrolls to the bottom of the inbox,
  so the pagination component may only be visible briefly.
</Tip>

<Expandable title="Custom list item example">
  ```jsx theme={null}
  import { CourierInbox, type CourierInboxListItemFactoryProps } from "@trycourier/courier-react";

  const CustomListItem = ({ message, index }: CourierInboxListItemFactoryProps) => (
    <pre style={{ padding: "24px", borderBottom: "1px solid #e0e0e0", margin: "0" }}>
      {JSON.stringify({ message, index }, null, 2)}
    </pre>
  );

  export default function App() {
    // Authentication code...
    return (
      <CourierInbox
        renderListItem={(props: CourierInboxListItemFactoryProps) => <CustomListItem {...props} />}
      />
    );
  }
  ```

  <Frame caption="Custom inbox message list item">
    <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-list-items.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=f6db4e077e25539782f0c521591a14d8" alt="Custom inbox message list item displaying the message object" width="1376" height="1218" data-path="assets/sdks/courier-inbox-list-items.png" />
  </Frame>
</Expandable>

<Expandable title="Custom header example">
  ```jsx theme={null}
  import { CourierInbox, type CourierInboxHeaderFactoryProps } from "@trycourier/courier-react";

  const CustomHeader = (props: CourierInboxHeaderFactoryProps) => {
    const selectedFeed = props.feeds.find(feed => feed.isSelected);
    const selectedTab = selectedFeed?.tabs.find(tab => tab.isSelected);

    return (
      <div style={{ background: "red", fontSize: "24px", padding: "24px", width: "100%" }}>
        Feed: {selectedFeed?.title ?? 'None'} | Tab: {selectedTab?.title ?? 'None'} | Unread: {selectedTab?.unreadCount ?? 0}
      </div>
    );
  };

  export default function App() {
    // Authentication code...
    return <CourierInbox renderHeader={(props) => <CustomHeader {...props} />} />;
  }
  ```

  <Frame caption="Custom inbox header">
    <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-custom-header.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=fd35615d6b4956e702842e92059a755d" alt="Custom inbox header" width="1376" height="1218" data-path="assets/sdks/courier-inbox-custom-header.png" />
  </Frame>
</Expandable>

<Expandable title="Custom popup menu button example">
  ```jsx theme={null}
  import { CourierInboxPopupMenu, type CourierInboxMenuButtonFactoryProps } from "@trycourier/courier-react";

  const CustomMenuButton = ({ unreadCount }: CourierInboxMenuButtonFactoryProps) => (
    <button>Open Inbox ({unreadCount} unread)</button>
  );

  export default function App() {
    // Authentication code...
    return (
      <CourierInboxPopupMenu renderMenuButton={(props) => <CustomMenuButton {...props} />} />
    );
  }
  ```

  <Frame caption="Custom inbox popup menu button">
    <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-menu-button.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=c791ccbc4a2f32d74f232a15807218ab" alt="Custom inbox popup menu button" width="1212" height="1342" data-path="assets/sdks/courier-inbox-menu-button.png" />
  </Frame>
</Expandable>

<Expandable title="Custom loading, empty, error, and pagination states">
  ```jsx theme={null}
  import { CourierInbox } from "@trycourier/courier-react";

  export default function App() {
    // Authentication code...
    return (
      <CourierInbox
        renderLoadingState={({ datasetId }) => (
          <div style={{ padding: "24px", textAlign: "center" }}>Loading {datasetId}...</div>
        )}
        renderEmptyState={({ datasetId }) => (
          <div style={{ padding: "24px", textAlign: "center" }}>No messages in {datasetId}</div>
        )}
        renderErrorState={({ datasetId, error }) => (
          <div style={{ padding: "24px", textAlign: "center" }}>Error: {error.message}</div>
        )}
        renderPaginationItem={({ datasetId }) => (
          <div style={{ padding: "24px", textAlign: "center" }}>Loading more...</div>
        )}
      />
    );
  }
  ```

  <Frame caption="Custom pagination state">
    <img src="https://mintcdn.com/courier-4f1f25dc/2GNhpTa50HDyTjlu/assets/sdks/courier-inbox-pagination.png?fit=max&auto=format&n=2GNhpTa50HDyTjlu&q=85&s=2825e20a34a942b2a27433c099bf47b1" alt="Custom pagination state" width="1096" height="1266" data-path="assets/sdks/courier-inbox-pagination.png" />
  </Frame>
</Expandable>

<Expandable title="Element ref example (removeHeader)">
  ```jsx theme={null}
  import { useEffect, useRef } from 'react';
  import { CourierInbox, useCourier, type CourierInboxElement } from '@trycourier/courier-react';

  export default function App() {
    const courier = useCourier();
    const inboxRef = useRef<CourierInboxElement>(null);

    useEffect(() => {
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt: "..." });
    }, []);

    useEffect(() => {
      if (inboxRef.current) {
        inboxRef.current.removeHeader();
      }
    }, [inboxRef.current]);

    return <CourierInbox ref={inboxRef} />;
  }
  ```
</Expandable>

## Toast Component

### `<CourierToast />`

<Frame caption="Courier Toast component">
  <img width="552px" src="https://mintcdn.com/courier-4f1f25dc/d_Nx8S3pul6-jZOx/assets/sdks/courier-toast-stack.png?fit=max&auto=format&n=d_Nx8S3pul6-jZOx&q=85&s=23b61aaa1b8a89cf34d5891fcf8076e7" data-path="assets/sdks/courier-toast-stack.png" />
</Frame>

Toasts are short-lived notifications that notify users and prompt them to take action.
The Toast component is connected to the feed of Courier Inbox messages.

<Tip>
  Toasts are synced with the Inbox message feed. You can use both components together to provide persistent and temporary notifications.
</Tip>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import { CourierToast, useCourier } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return <CourierToast />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import { CourierToast, useCourier } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return <CourierToast />;
  }
  ```
</CodeGroup>

<Tip>
  **Sample App**: See a complete working example in our [React Toast sample app](https://github.com/trycourier/courier-samples/tree/main/web/react/toast).
</Tip>

<Note>
  Some initialization for toasts is asynchronous. If your app displays toasts immediately when
  the component is mounted, consider using the `onReady` callback (see props table below) to wait
  until the component is fully initialized.
</Note>

***

### Handle Clicks

| Callback Prop            | Type Signature                                      |
| :----------------------- | :-------------------------------------------------- |
| `onToastItemClick`       | `(props: CourierToastItemClickEvent) => void`       |
| `onToastItemActionClick` | `(props: CourierToastItemActionClickEvent) => void` |

<Frame caption="Courier Toast with action buttons">
  <img width="552px" src="https://mintcdn.com/courier-4f1f25dc/d_Nx8S3pul6-jZOx/assets/sdks/courier-toast-action-buttons.png?fit=max&auto=format&n=d_Nx8S3pul6-jZOx&q=85&s=32dab2e3e0ad863fae60c4e65b881e4f" data-path="assets/sdks/courier-toast-action-buttons.png" />
</Frame>

If a message contains [actions](/platform/content/content-blocks/action-blocks), toast items
include a button for each. Use `onToastItemActionClick` to handle those clicks.

```jsx theme={null}
<CourierToast
  onToastItemClick={({ message }: CourierToastItemClickEvent) => {
    console.log("Toast clicked:", message);
  }}
  onToastItemActionClick={({ message, action }: CourierToastItemActionClickEvent) => {
    window.open(action.href);
  }}
/>
```

<Expandable title="Click event types">
  ```ts theme={null}
  type CourierToastItemClickEvent = {
    message: InboxMessage;
    toastItem: CourierToastItem | HTMLElement;
  };

  type CourierToastItemActionClickEvent = {
    message: InboxMessage;
    action: InboxAction;
  };
  ```
</Expandable>

***

### Styles and Theming

<Frame caption="Courier Toast with a custom theme">
  <img width="552px" src="https://mintcdn.com/courier-4f1f25dc/d_Nx8S3pul6-jZOx/assets/sdks/courier-toast-custom-theme.png?fit=max&auto=format&n=d_Nx8S3pul6-jZOx&q=85&s=766c15b8b329ce06f83f1a1c3a400904" data-path="assets/sdks/courier-toast-custom-theme.png" />
</Frame>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { CourierToast, type CourierToastTheme } from "@trycourier/courier-react";

  export default function App() {
    // Authentication code...

    const theme: CourierToastTheme = {
      toast: {
        item: {
          title: { color: "#6366f1", weight: "bold" },
          backgroundColor: "#edeefc",
          border: "1px solid #cdd1ff",
          borderRadius: "15px",
        },
      },
    };

    return <CourierToast lightTheme={theme} mode="light" />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { CourierToast, type CourierToastTheme } from "@trycourier/courier-react-17";

  export default function App() {
    // Authentication code...

    const theme: CourierToastTheme = {
      toast: {
        item: {
          title: { color: "#6366f1", weight: "bold" },
          backgroundColor: "#edeefc",
          border: "1px solid #cdd1ff",
          borderRadius: "15px",
        },
      },
    };

    return <CourierToast lightTheme={theme} mode="light" />;
  }
  ```
</CodeGroup>

Toast theme utilities: `defaultToastLightTheme` / `defaultToastDarkTheme` for defaults, and `mergeToastTheme(baseTheme, overrideTheme)` to merge.

<Expandable title="type CourierToastTheme">
  ```ts theme={null}
  type CourierToastTheme = {
    item?: {
      backgroundColor?: string;
      hoverBackgroundColor?: string;
      autoDismissBarColor?: string;
      title?: { family?: string; weight?: string; size?: string; color?: string; };
      body?: { family?: string; weight?: string; size?: string; color?: string; };
      icon?: { color?: string; svg?: string; };
      dismissIcon?: { color?: string; svg?: string; };
      shadow?: string;
      border?: string;
      borderRadius?: string;
      actions?: {
        backgroundColor?: string;
        hoverBackgroundColor?: string;
        activeBackgroundColor?: string;
        border?: string;
        borderRadius?: string;
        shadow?: string;
        font?: { family?: string; weight?: string; size?: string; color?: string; };
      };
    };
  };
  ```
</Expandable>

***

### Custom Elements

| Render Prop              | Type Signature                                                                         |
| :----------------------- | :------------------------------------------------------------------------------------- |
| `renderToastItemContent` | `(props: CourierToastItemFactoryProps) => ReactNode` — customize the content area only |
| `renderToastItem`        | `(props: CourierToastItemFactoryProps) => ReactNode` — fully replace each toast item   |

<Expandable title="Custom toast content example">
  ```jsx theme={null}
  import { CourierToast, type CourierToastItemFactoryProps } from "@trycourier/courier-react";

  const CustomToastContent = ({ message }: CourierToastItemFactoryProps) => (
    <div style={{ padding: "16px" }}>
      <strong style={{ display: "block", marginBottom: "4px" }}>{message.title}</strong>
      <p style={{ margin: 0, fontSize: "14px", color: "#6b7280" }}>{message.body}</p>
    </div>
  );

  export default function App() {
    // Authentication code...
    return <CourierToast renderToastItemContent={(props) => <CustomToastContent {...props} />} />;
  }
  ```

  <Frame caption="Toast with custom item content">
    <img width="552px" src="https://mintcdn.com/courier-4f1f25dc/d_Nx8S3pul6-jZOx/assets/sdks/courier-toast-custom-item-content.png?fit=max&auto=format&n=d_Nx8S3pul6-jZOx&q=85&s=0e7dc6855e4f25900aebbc58b1ce3bab" data-path="assets/sdks/courier-toast-custom-item-content.png" />
  </Frame>
</Expandable>

<Expandable title="Fully custom toast item example">
  ```jsx theme={null}
  import { CourierToast, type CourierToastItemFactoryProps } from "@trycourier/courier-react";

  const CustomToastItem = ({ message, dismiss }: CourierToastItemFactoryProps) => (
    <div style={{ display: "flex", alignItems: "center", gap: "12px", marginBottom: "8px" }}>
      <div style={{ flex: 1, padding: "16px", background: "#f6f6fe", border: "1px solid #c6c2ff", borderRadius: "8px" }}>
        <strong style={{ display: "block", marginBottom: "4px" }}>{message.title}</strong>
        <p style={{ margin: 0, fontSize: "14px" }}>{message.body}</p>
      </div>
      <div style={{ display: "flex", flexDirection: "column", gap: "8px", minWidth: "100px" }}>
        {message.actions?.map((action, index) => (
          <button key={index} onClick={() => window.open(action.href)}
            style={{ padding: "8px 12px", background: "#f6f6fe", border: "1px solid #c6c2ff", borderRadius: "8px" }}>
            {action.content}
          </button>
        ))}
      </div>
    </div>
  );

  export default function App() {
    // Authentication code...
    return <CourierToast renderToastItem={(props) => <CustomToastItem {...props} />} />;
  }
  ```

  <Frame caption="Toast with a fully custom item">
    <img width="552px" src="https://mintcdn.com/courier-4f1f25dc/d_Nx8S3pul6-jZOx/assets/sdks/courier-toast-custom-item.png?fit=max&auto=format&n=d_Nx8S3pul6-jZOx&q=85&s=7892a3935731468e0dd4c8c04c1909ab" data-path="assets/sdks/courier-toast-custom-item.png" />
  </Frame>
</Expandable>

***

### CourierToast Props

| Prop Name                | Type                                         | Default                                                                          | Description                                                                                 |
| :----------------------- | :------------------------------------------- | :------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------ |
| `style`                  | `CSSProperties`                              | `{ position: "fixed", width: "380px", top: "30px", right: "30px", zIndex: 999 }` | Styles applied to the toast component.                                                      |
| `lightTheme`             | `CourierToastTheme`                          | `undefined`                                                                      | Theme for light mode.                                                                       |
| `darkTheme`              | `CourierToastTheme`                          | `undefined`                                                                      | Theme for dark mode.                                                                        |
| `mode`                   | `"light" \| "dark" \| "system"`              | `"system"`                                                                       | Theme mode.                                                                                 |
| `autoDismiss`            | `boolean`                                    | `false`                                                                          | Enable auto-dismiss with countdown bar.                                                     |
| `autoDismissTimeoutMs`   | `number`                                     | `5000`                                                                           | Timeout in ms before auto-dismiss.                                                          |
| `dismissButton`          | `"visible" \| "hidden" \| "hover" \| "auto"` | `"auto"`                                                                         | Dismiss button visibility. `"auto"` shows always if autoDismiss is false, on hover if true. |
| `onToastItemClick`       | `fn`                                         | `undefined`                                                                      | Callback when a toast is clicked.                                                           |
| `onToastItemActionClick` | `fn`                                         | `undefined`                                                                      | Callback when a toast action button is clicked.                                             |
| `renderToastItem`        | `fn`                                         | `undefined`                                                                      | Custom render for entire toast items.                                                       |
| `renderToastItemContent` | `fn`                                         | `undefined`                                                                      | Custom render for toast item content only.                                                  |
| `onReady`                | `(ready: boolean) => void`                   | `undefined`                                                                      | Callback when the component is ready to receive messages.                                   |

<Frame caption="Toast component with auto-dismiss enabled">
  <img width="552px" src="https://mintcdn.com/courier-4f1f25dc/d_Nx8S3pul6-jZOx/assets/sdks/courier-toast-auto-dismiss.png?fit=max&auto=format&n=d_Nx8S3pul6-jZOx&q=85&s=c01306115adcab00fa7f0344eb7abc20" data-path="assets/sdks/courier-toast-auto-dismiss.png" />
</Frame>

Enabling `autoDismiss` adds a countdown bar to each toast and automatically removes it after the timeout. The countdown bar color is theme-able via `autoDismissBarColor` in `CourierToastTheme`.

```jsx theme={null}
<CourierToast autoDismiss={true} autoDismissTimeoutMs={7000} />
```

#### Using onReady

If toasts display immediately on mount or custom render functions don't apply correctly, use `onReady` to wait for full initialization before authenticating:

```jsx theme={null}
const [toastReady, setToastReady] = useState(false);

useEffect(() => {
  if (toastReady) {
    courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
  }
}, [toastReady]);

return <CourierToast onReady={setToastReady} renderToastItem={(props) => <CustomToast {...props} />} />;
```

## Preferences Component

### `<CourierPreferences />`

The Preferences component lets your users manage which topics they're subscribed to and how each topic is delivered (per-channel routing and digest schedules), directly inside your React app.

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import { CourierPreferences, useCourier } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return <CourierPreferences />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import { CourierPreferences, useCourier } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      const jwt = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
      courier.shared.signIn({ userId: $YOUR_USER_ID, jwt });
    }, []);

    return <CourierPreferences />;
  }
  ```
</CodeGroup>

<Note>
  Authentication requires a JWT that includes the `read:preferences` and `write:preferences` scopes. Always generate JWTs on your backend — see [Authentication](/sdk-libraries/courier-ui-inbox-web#authentication).
</Note>

***

### CourierPreferences Props

| Prop Name       | Type                            | Default     | Description                                                                 |
| :-------------- | :------------------------------ | :---------- | :-------------------------------------------------------------------------- |
| `style`         | `CSSProperties`                 | `undefined` | Styles applied to the preferences component.                                |
| `lightTheme`    | `CourierPreferencesTheme`       | `undefined` | Theme for light mode. Merged with defaults.                                 |
| `darkTheme`     | `CourierPreferencesTheme`       | `undefined` | Theme for dark mode. Merged with defaults.                                  |
| `mode`          | `"light" \| "dark" \| "system"` | `"system"`  | Theme mode.                                                                 |
| `title`         | `string`                        | `undefined` | Override the component's title text.                                        |
| `subtitle`      | `string`                        | `undefined` | Override the component's subtitle text.                                     |
| `brandId`       | `string`                        | `undefined` | Render preferences using a specific brand's styling.                        |
| `channelLabels` | `Record<string, string>`        | `undefined` | Rename how delivery channels appear in the UI (e.g. `{ email: "E-mail" }`). |
| `onError`       | `(error: Error) => void`        | `undefined` | Callback invoked when the component encounters an error.                    |

***

### Styles and Theming

Pass a `CourierPreferencesTheme` to `lightTheme` and/or `darkTheme`. Themes are merged with the defaults, so you only specify the values you want to override.

```jsx theme={null}
import { CourierPreferences, type CourierPreferencesTheme } from "@trycourier/courier-react";

const lightTheme: CourierPreferencesTheme = {
  primaryColor: "#8B5CF6",
  topic: {
    title: { family: "Poppins", size: "16px", weight: "500", color: "#1E1B2E" },
    toggle: { trackColor: "#D9D3EC", trackActiveColor: "#8B5CF6", thumbColor: "#FFFFFF", borderRadius: "12px" },
  },
  digest: {
    font: { family: "Poppins", size: "14px", weight: "400", color: "#6B6580" },
    radio: { ringColor: "#D9D3EC", checkedColor: "#8B5CF6" },
  },
  channelChip: {
    font: { family: "Poppins", size: "14px", weight: "400", color: "#6B6580" },
    checkbox: { checkedColor: "#8B5CF6" },
  },
};

export default function App() {
  // Authentication code...
  return <CourierPreferences lightTheme={lightTheme} mode="light" />;
}
```

Preferences theme utilities: `defaultPreferencesLightTheme` / `defaultPreferencesDarkTheme` for defaults, and `mergePreferencesTheme(mode, overrideTheme)` to merge. See the [full `CourierPreferencesTheme` reference](/sdk-libraries/courier-ui-inbox-web#preferences-web-components).

## useCourier Hook

The `useCourier()` hook provides programmatic access to Courier functionality for building custom UIs or integrating Courier features into existing components.

### When to use hooks vs components

* **Components** (`<CourierInbox />`, `<CourierToast />`): Quick integration with default UI
* **Hooks** (`useCourier()`): Custom UIs, programmatic control, advanced state management
* **Both together**: Use hooks for state management while components handle rendering

### Hook return value

```typescript theme={null}
{
  shared: Courier.shared,  // Direct access to Courier instance
  auth: {
    userId?: string,
    signIn: (props: CourierProps) => void,
    signOut: () => void
  },
  inbox: {
    // Methods
    load: (props?: { canUseCache?: boolean, datasetIds?: string[] }) => Promise<void>,
    fetchNextPageOfMessages: (props: { datasetId: string }) => Promise<InboxDataSet | null>,
    setPaginationLimit: (limit: number) => void,
    registerFeeds: (feeds: CourierInboxFeed[]) => void,
    listenForUpdates: () => Promise<void>,
    readMessage: (message: InboxMessage) => Promise<void>,
    unreadMessage: (message: InboxMessage) => Promise<void>,
    clickMessage: (message: InboxMessage) => Promise<void>,
    archiveMessage: (message: InboxMessage) => Promise<void>,
    unarchiveMessage: (message: InboxMessage) => Promise<void>,
    openMessage: (message: InboxMessage) => Promise<void>,
    readAllMessages: () => Promise<void>,
    // Reactive state
    feeds: Record<string, InboxDataSet>,
    totalUnreadCount?: number,
    error?: Error
  },
  toast: {
    addMessage: (message: InboxMessage) => void,
    removeMessage: (message: InboxMessage) => void,
    error?: Error
  }
}
```

**Complete example** — authentication, inbox setup, real-time updates, and displaying messages:

<Warning>
  You must call `inbox.listenForUpdates()` after authentication to enable real-time message updates. Without this, the inbox only shows messages from the initial load.
</Warning>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import { useCourier, type InboxMessage, defaultFeeds } from "@trycourier/courier-react";

  export default function App() {
    const { auth, inbox } = useCourier();

    useEffect(() => {
      auth.signIn({
        userId: $YOUR_USER_ID,
        jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      });
      loadInbox();
    }, []);

    async function loadInbox() {
      inbox.registerFeeds(defaultFeeds());
      await inbox.listenForUpdates();
      await inbox.load();
    }

    return (
      <div>
        <div>Total Unread: {inbox.totalUnreadCount ?? 0}</div>
        <ul>
          {inbox.feeds['all_messages']?.messages.map((message: InboxMessage) => (
            <li key={message.messageId} style={{
              backgroundColor: message.read ? 'transparent' : '#fee2e2',
              padding: '8px', marginBottom: '4px'
            }}>
              {message.title}
            </li>
          ))}
        </ul>
      </div>
    );
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import { useCourier, type InboxMessage, defaultFeeds } from "@trycourier/courier-react-17";

  export default function App() {
    const { auth, inbox } = useCourier();

    useEffect(() => {
      auth.signIn({
        userId: $YOUR_USER_ID,
        jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      });
      loadInbox();
    }, []);

    async function loadInbox() {
      inbox.registerFeeds(defaultFeeds());
      await inbox.listenForUpdates();
      await inbox.load();
    }

    return (
      <div>
        <div>Total Unread: {inbox.totalUnreadCount ?? 0}</div>
        <ul>
          {inbox.feeds['all_messages']?.messages.map((message: InboxMessage) => (
            <li key={message.messageId} style={{
              backgroundColor: message.read ? 'transparent' : '#fee2e2',
              padding: '8px', marginBottom: '4px'
            }}>
              {message.title}
            </li>
          ))}
        </ul>
      </div>
    );
  }
  ```
</CodeGroup>

### Inbox methods

| Method                                         | Description                                                                |
| :--------------------------------------------- | :------------------------------------------------------------------------- |
| `inbox.load(props?)`                           | Load messages. Optional `{ canUseCache, datasetIds }`.                     |
| `inbox.fetchNextPageOfMessages({ datasetId })` | Fetch next page. Returns `InboxDataSet \| null`.                           |
| `inbox.setPaginationLimit(limit)`              | Set messages per page.                                                     |
| `inbox.registerFeeds(feeds)`                   | Register feeds and tabs with the datastore.                                |
| `inbox.listenForUpdates()`                     | Start WebSocket connection for real-time updates. **Required after auth.** |
| `inbox.readMessage(message)`                   | Mark as read.                                                              |
| `inbox.unreadMessage(message)`                 | Mark as unread.                                                            |
| `inbox.archiveMessage(message)`                | Archive.                                                                   |
| `inbox.unarchiveMessage(message)`              | Unarchive.                                                                 |
| `inbox.clickMessage(message)`                  | Track click event (analytics).                                             |
| `inbox.openMessage(message)`                   | Mark as opened.                                                            |
| `inbox.readAllMessages()`                      | Mark all as read.                                                          |

### Toast methods

| Method                         | Description                            |
| :----------------------------- | :------------------------------------- |
| `toast.addMessage(message)`    | Add a message to the toast stack.      |
| `toast.removeMessage(message)` | Remove a message from the toast stack. |

**Error handling:** Check `inbox.error` and `toast.error` for error states:

```jsx theme={null}
const { inbox } = useCourier();
if (inbox.error) return <div>Error: {inbox.error.message}</div>;
```

<Tip>
  **Sample Apps**: See complete working examples in our [React Hooks sample app](https://github.com/trycourier/courier-samples/tree/main/web/react/hooks) and [React Inbox sample app](https://github.com/trycourier/courier-samples/tree/main/web/react/inbox).
</Tip>

## Advanced

### Next.js and SSR

Courier Inbox and Toast support Next.js but only render client-side. In Next.js 13+, add [`'use client'`](https://nextjs.org/docs/app/api-reference/directives/use-client) to the top of any file using Courier components.

```jsx theme={null}
"use client"

import { CourierInbox } from "@trycourier/courier-react";

export default function Page() {
  // Authentication code...
  return <CourierInbox />;
}
```

***

### Troubleshooting

<AccordionGroup>
  <Accordion title="Inbox not updating in real-time">
    Make sure you've called `inbox.listenForUpdates()` after authentication. This establishes the WebSocket connection required for real-time updates.

    ```jsx theme={null}
    await auth.signIn({ userId, jwt });
    inbox.registerFeeds(defaultFeeds());
    await inbox.listenForUpdates(); // Required for real-time
    await inbox.load();
    ```
  </Accordion>

  <Accordion title="Messages not loading">
    **Possible causes:**

    1. Not authenticated — ensure `signIn()` has been called
    2. Feeds not registered — call `registerFeeds()` before `load()`
    3. Network errors — check `inbox.error` for details
    4. JWT expired — generate a new token
  </Accordion>

  <Accordion title="Authentication errors">
    * **Invalid JWT**: Ensure the JWT is generated correctly on your backend
    * **Expired JWT**: JWTs have an expiration time; generate a new one
    * **Missing scopes**: Ensure your JWT includes `inbox:read:messages` and `inbox:write:events`
    * **Wrong user ID**: Verify the `userId` matches the user the JWT was issued for
  </Accordion>

  <Accordion title="TypeScript errors">
    Import types directly from the package:

    ```tsx theme={null}
    import { useCourier, type InboxMessage, type CourierInboxFeed } from '@trycourier/courier-react';
    ```
  </Accordion>

  <Accordion title="React 17 vs React 18 package mismatch">
    Ensure you're using the correct package:

    * React 18+: `@trycourier/courier-react`
    * React 17: `@trycourier/courier-react-17`
  </Accordion>
</AccordionGroup>

***

### Best Practices

* **JWT Security**: Always generate JWTs server-side. Cache on the client, refresh before expiration (standard: `'1d'`), include only necessary scopes.
* **Performance**: Use `canUseCache: true` (default) for cached data. Use `datasetIds` to load only needed datasets. Set appropriate `setPaginationLimit()` values. The hook's reactive state updates automatically; avoid triggering unnecessary re-renders.
* **Testing**: Mock `useCourier()` in tests to return test data:

```jsx theme={null}
jest.mock('@trycourier/courier-react', () => ({
  useCourier: () => ({
    inbox: {
      feeds: { 'all_messages': { messages: mockMessages } },
      totalUnreadCount: 5
    }
  })
}));
```

### EU and regional endpoints

Only needed if your workspace uses the [EU datacenter](/platform/workspaces/eu-datacenter). `@trycourier/courier-react` re-exports `EU_COURIER_API_URLS` and `getCourierApiUrlsForRegion` from `@trycourier/courier-js`.

<Note>
  Mint JWTs on a backend that calls the EU Issue Token host: `https://api.eu.courier.com/auth/issue-token`. Full detail: [Courier JS — EU and regional endpoints](/sdk-libraries/courier-js-web#eu-and-regional-endpoints).
</Note>

<CodeGroup>
  ```jsx React 18+ lines theme={null}
  import { useEffect } from "react";
  import { CourierInbox, getCourierApiUrlsForRegion, useCourier } from "@trycourier/courier-react";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      courier.shared.signIn({
        userId: $YOUR_USER_ID,
        jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        apiUrls: getCourierApiUrlsForRegion("eu"),
      });
    }, []);

    return <CourierInbox />;
  }
  ```

  ```jsx React 17 lines theme={null}
  import { useEffect } from "react";
  import { CourierInbox, getCourierApiUrlsForRegion, useCourier } from "@trycourier/courier-react-17";

  export default function App() {
    const courier = useCourier();

    useEffect(() => {
      courier.shared.signIn({
        userId: $YOUR_USER_ID,
        jwt: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        apiUrls: getCourierApiUrlsForRegion("eu"),
      });
    }, []);

    return <CourierInbox />;
  }
  ```
</CodeGroup>

You can use `apiUrls: EU_COURIER_API_URLS` instead of `getCourierApiUrlsForRegion("eu")` if you prefer the frozen preset.

## TypeScript Types

<Expandable title="type InboxMessage">
  ```typescript theme={null}
  type InboxMessage = {
    messageId: string;
    title?: string;
    body?: string;
    read?: string;     // ISO timestamp
    opened?: string;   // ISO timestamp
    archived?: string; // ISO timestamp
    tags?: string[];
    trackingIds?: { clickTrackingId?: string; openTrackingId?: string; };
    actions?: InboxAction[];
    data?: Record<string, unknown>;
  }
  ```
</Expandable>

<Expandable title="type InboxDataSet">
  ```typescript theme={null}
  type InboxDataSet = {
    id: string;
    messages: InboxMessage[];
    unreadCount: number;
    canPaginate: boolean;
    paginationCursor: string | null;
  }
  ```
</Expandable>

<Expandable title="type CourierInboxFeed">
  ```typescript theme={null}
  type CourierInboxFeed = {
    feedId: string;
    title: string;
    iconSVG?: string;
    tabs: CourierInboxTab[];
  }
  ```
</Expandable>

<Expandable title="type CourierInboxTab">
  ```typescript theme={null}
  type CourierInboxTab = {
    datasetId: string;
    title: string;
    filter: CourierInboxDatasetFilter;
  }
  ```
</Expandable>

<Expandable title="type CourierInboxDatasetFilter">
  ```typescript theme={null}
  type CourierInboxDatasetFilter = {
    tags?: string[];
    archived?: boolean;
    status?: 'read' | 'unread';
  }
  ```

  All filter properties are AND'd together. For example, `{ tags: ['important'], status: 'unread' }` shows messages that have the 'important' tag AND are unread.
</Expandable>

<Expandable title="type CourierToastItemFactoryProps">
  ```typescript theme={null}
  type CourierToastItemFactoryProps = {
    message: InboxMessage;
    autoDismiss: boolean;
    autoDismissTimeoutMs: number;
    dismiss: () => void;
  }
  ```
</Expandable>
