> ## 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.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Customize the inbox

> Theme the inbox, handle clicks and actions, or replace parts of the UI.

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

The inbox ships with a default look and is fully themeable. <Doc href="/docs/in-app/add-an-inbox">Add an inbox</Doc> covers embedding it first.

## Theme

Theme the inbox with `lightTheme` / `darkTheme` (a `CourierInboxTheme`), and set `mode` to `"light"`, `"dark"`, or `"system"`. The default is `"system"`, which follows the user's OS setting.

<Frame caption="A Courier Inbox with a custom theme.">
  <img src="https://mintcdn.com/courier-4f1f25dc/9rcgucLA9fBnJt_U/assets/inbox-theme.webp?fit=max&auto=format&n=9rcgucLA9fBnJt_U&q=85&s=38d817f4fa2390404ab22ade2d22f612" alt="The Courier Inbox with a custom purple theme applied to its unread count badge, unread dots, and action buttons" className="mx-auto" width="3152" height="1776" data-path="assets/inbox-theme.webp" />
</Frame>

<CodeGroup>
  ```jsx React highlight={3-4,6} theme={null}
  import { CourierInbox } from "@trycourier/courier-react";  // or "@trycourier/courier-react-17"

  const lightTheme = { /* CourierInboxTheme */ };
  const darkTheme = { /* CourierInboxTheme */ };

  <CourierInbox lightTheme={lightTheme} darkTheme={darkTheme} mode="system" />;
  ```

  ```html Web Components highlight={7-8} theme={null}
  <courier-inbox id="inbox"></courier-inbox>

  <script type="module">
    import { Courier } from "@trycourier/courier-ui-inbox";

    const inbox = document.getElementById("inbox");
    inbox.setLightTheme({ /* CourierInboxTheme */ });
    inbox.setDarkTheme({ /* CourierInboxTheme */ });
  </script>
  ```

  ```vue Vue highlight={4-5,9} theme={null}
  <script setup lang="ts">
  import { CourierInbox } from "@trycourier/courier-vue";

  const lightTheme = { /* CourierInboxTheme */ };
  const darkTheme = { /* CourierInboxTheme */ };
  </script>

  <template>
    <CourierInbox :lightTheme="lightTheme" :darkTheme="darkTheme" mode="system" />
  </template>
  ```

  ```ts Angular highlight={8,11-12} theme={null}
  import { Component } from "@angular/core";
  import { CourierInboxComponent, CourierInboxTheme } from "@trycourier/courier-angular";

  @Component({
    selector: "app-inbox",
    standalone: true,
    imports: [CourierInboxComponent],
    template: `<courier-inbox [lightTheme]="lightTheme" [darkTheme]="darkTheme" mode="system"></courier-inbox>`,
  })
  export class InboxComponent {
    lightTheme: CourierInboxTheme = { /* ... */ };
    darkTheme: CourierInboxTheme = { /* ... */ };
  }
  ```

  ```swift iOS highlight={3-4,9-10} theme={null}
  // UIKit
  let inbox = CourierInbox(
    lightTheme: CourierInboxTheme(/* ... */),
    darkTheme: CourierInboxTheme(/* ... */)
  )

  // SwiftUI
  CourierInboxView(
    lightTheme: CourierInboxTheme(/* ... */),
    darkTheme: CourierInboxTheme(/* ... */)
  )
  ```

  ```kotlin Android highlight={3-4,8-9} theme={null}
  // Jetpack Compose
  CourierInbox(
    lightTheme = CourierInboxTheme(/* ... */),
    darkTheme = CourierInboxTheme(/* ... */)
  )

  // Views: set the properties on the CourierInbox view
  inbox.lightTheme = CourierInboxTheme(/* ... */)
  inbox.darkTheme = CourierInboxTheme(/* ... */)
  ```

  ```dart Flutter highlight={2-3} theme={null}
  CourierInbox(
    lightTheme: CourierInboxTheme(/* ... */),
    darkTheme: CourierInboxTheme(/* ... */),
  );
  ```

  ```jsx React Native theme={null}
  <CourierInboxView theme={{ light: { /* ... */ }, dark: { /* ... */ } }} />;
  ```
</CodeGroup>

On React, Vue, and Angular the theme is a prop. On iOS, Android, and Flutter it is a constructor argument. On the web component, call `setLightTheme` / `setDarkTheme`, or set the `light-theme` / `dark-theme` attributes with a JSON string.

**Full theme reference.** The web theme object (`CourierInboxTheme`) is identical across React, Vue, Angular, and the Web Components, so it is documented once in <Doc href="/docs/in-app/customize-the-inbox#courierinboxtheme-reference">the reference below</Doc>. The native SDKs have their own: <Doc href="/docs/sdk-libraries/ios#inbox-theme">iOS</Doc>, <Doc href="/docs/sdk-libraries/android#inbox-theme">Android</Doc>, <Doc href="/docs/sdk-libraries/flutter#inbox-theme">Flutter</Doc>, and <Doc href="/docs/sdk-libraries/react-native#inbox-theme">React Native</Doc>.

### Start from a default theme

The web SDKs export the two themes they ship, `defaultLightTheme` and `defaultDarkTheme`,
plus `mergeTheme` to layer your overrides onto one of them.

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

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

// mergeTheme takes the MODE, not a theme object
const theme = mergeTheme("light", overrides);
```

**`mergeTheme` takes a mode string, `"light"` or `"dark"`, as its first argument.** It picks
the matching default itself. Passing `defaultLightTheme` there type-checks against nothing
useful and merges onto the dark defaults, which shows up as a theme that looks right in one
mode and wrong in the other.

Merging is not required. A theme passed straight to `lightTheme` already merges over the
defaults field by field, so reach for `mergeTheme` only when you need the resolved object,
such as reading a default colour to compute another.

### CourierInboxTheme reference

Every property is optional, and the object is identical on every web SDK.

```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;
      };
    };
  };
};
```

## Handle interaction

Respond to interactions with your own callbacks instead of the SDK's default navigation. Every platform exposes the same two interactions:

* **Message click** fires when a user taps a message. Receives the `message` and its `index`.
* **Action click** fires when a user taps a message's action button. Receives the tapped `action` alongside the `message` and `index`.

<CodeGroup>
  ```jsx React theme={null}
  <CourierInbox
    onMessageClick={({ message, index }) => openInApp(message)}
    onMessageActionClick={({ message, action, index }) => handleAction(message, action)}
  />
  ```

  ```js Web Components theme={null}
  const inbox = document.getElementById("inbox");

  inbox.onMessageClick(({ message, index }) => openInApp(message));
  inbox.onMessageActionClick(({ message, action, index }) => handleAction(message, action));

  // Or listen for the equivalent DOM events (detail carries the same payload):
  // inbox.addEventListener("message-click", (e) => openInApp(e.detail.message));
  // inbox.addEventListener("message-action-click", (e) => handleAction(e.detail.message, e.detail.action));
  ```

  ```vue Vue theme={null}
  <template>
    <CourierInbox
      :onMessageClick="({ message, index }) => openInApp(message)"
      :onMessageActionClick="({ message, action, index }) => handleAction(message, action)"
    />
  </template>
  ```

  ```html Angular theme={null}
  <courier-inbox
    (messageClick)="openInApp($event.message)"
    (messageActionClick)="handleAction($event.message, $event.action)">
  </courier-inbox>
  ```

  ```swift iOS theme={null}
  // UIKit (CourierInbox) and SwiftUI (CourierInboxView) take the same callbacks
  CourierInboxView(
    didClickInboxMessageAtIndex: { message, index in openInApp(message) },
    didClickInboxActionForMessageAtIndex: { action, message, index in handleAction(message, action) }
  )
  ```

  ```kotlin Android theme={null}
  inbox.setOnClickMessageListener { message, index -> openInApp(message) }
  inbox.setOnClickActionListener { action, message, index -> handleAction(message, action) }
  ```

  ```dart Flutter theme={null}
  CourierInbox(
    onMessageClick: (message, index) => openInApp(message),
    onActionClick: (action, message, index) => handleAction(message, action),
  );
  ```

  ```jsx React Native theme={null}
  <CourierInboxView
    onClickInboxMessageAtIndex={(message, index) => openInApp(message)}
    onClickInboxActionForMessageAtIndex={(action, message, index) => handleAction(message, action)}
  />
  ```
</CodeGroup>

The callback names differ by platform but the payloads line up. React, Vue, and Angular deliver these as props (`onMessage*`) or outputs (`messageClick`). The payload is a single object: `{ message, index }`, or `{ message, action, index }` for actions. The web component exposes matching `onMessage*` methods, or `message-*` DOM events with the same payload on `event.detail`. On iOS, Android, Flutter, and React Native they are constructor arguments, view parameters, or setters, with positional arguments. **Action click passes the `action` first**, then the `message` and `index`.

### Long press

Every platform reports a long press, web included. It fires only on devices that send
touch events, so a desktop pointer never triggers it and you still need a click handler
for the same intent.

<CodeGroup>
  ```jsx React theme={null}
  <CourierInbox onMessageLongPress={({ message, index }) => showActions(message)} />
  ```

  ```js Web Components theme={null}
  const inbox = document.getElementById("inbox");

  inbox.onMessageLongPress(({ message, index }) => showActions(message));
  ```

  ```vue Vue theme={null}
  <template>
    <CourierInbox :onMessageLongPress="({ message, index }) => showActions(message)" />
  </template>
  ```

  ```html Angular theme={null}
  <courier-inbox (messageLongPress)="showActions($event.message)"></courier-inbox>
  ```

  ```swift iOS theme={null}
  CourierInboxView(
    didLongPressInboxMessageAtIndex: { message, index in message.markAsArchived() }
  )
  ```

  ```kotlin Android theme={null}
  // Jetpack Compose takes it as an argument
  CourierInbox(
    onLongPressMessageListener = { message, index -> message.markAsArchived() }
  )

  // On the XML view, it is a setter
  inbox.setOnLongPressMessageListener { message, index -> message.markAsArchived() }
  ```
</CodeGroup>

The payload matches message click: an object on the web SDKs, positional arguments on
mobile. Both popup menu components take the same handler.

**On the web, long press is gated on the message menu being enabled.** The gesture opens
the per-message action menu, and the handler runs as part of that. A theme that sets
`inbox.list.item.menu.enabled` to `false` suppresses the gesture entirely, so the callback
never fires and nothing reports why. Two theme fields tune it:

| Field                                              | Default | What it does                                                                      |
| -------------------------------------------------- | ------- | --------------------------------------------------------------------------------- |
| `inbox.list.item.menu.longPress.displayDuration`   | `4000`  | How long the menu stays open, in milliseconds.                                    |
| `inbox.list.item.menu.longPress.vibrationDuration` | `50`    | Haptic length, in milliseconds. Ignored where `navigator.vibrate` is unavailable. |

The hold threshold is 650ms and is not themeable. A recognized long press suppresses the
click that would otherwise follow it, so the two handlers never both run.

### Scroll

iOS and Android report the list's scroll offset, so you can shrink a header or hide a
badge as the reader moves. The web SDKs have no equivalent: listen on the scrolling
element yourself.

<CodeGroup>
  ```swift iOS theme={null}
  CourierInboxView(
    didScrollInbox: { scrollView in header.collapse(scrollView.contentOffset.y) }
  )
  ```

  ```kotlin Android theme={null}
  // Jetpack Compose takes it as an argument
  CourierInbox(
    onScrollInboxListener = { offsetInDp -> header.collapse(offsetInDp) }
  )

  // On the XML view, it is a setter
  inbox.setOnScrollInboxListener { offsetInDp -> header.collapse(offsetInDp) }
  ```
</CodeGroup>

The offset is a `UIScrollView` on iOS and a dp value on Android.

## Custom UI

Theming changes how the built-in inbox looks. To change what it renders, see <Doc href="/docs/in-app/build-a-custom-inbox">Build a custom inbox UI</Doc>. It covers both levels: replacing individual slots such as the list item or header, and reading the message data and actions to build the UI yourself.

## FAQ

<AccordionGroup>
  <Accordion title="How do I match the inbox to my brand?">
    Supply a `lightTheme` and `darkTheme` as `CourierInboxTheme` values and set colors, typography, and spacing there. See [Theme](#theme) and your platform's SDK reference for the full field list.
  </Accordion>

  <Accordion title="Can I force light or dark instead of following the OS?">
    Set `mode` to `"light"` or `"dark"`. Leave it as `"system"` (the default) to follow the user's OS setting. See [Theme](#theme).
  </Accordion>

  <Accordion title="Can I render my own message rows?">
    See [Custom UI](#custom-ui): replace parts of the built-in inbox on the web SDKs, or build the whole UI yourself on any platform.
  </Accordion>

  <Accordion title="What is the difference between replacing parts and building my own UI?">
    <Doc href="/docs/in-app/build-a-custom-inbox#replace-parts-of-the-inbox">Replacing parts</Doc> keeps the built-in inbox and swaps individual slots, on the web SDKs only. <Doc href="/docs/in-app/build-a-custom-inbox#build-your-own-ui">Building your own UI</Doc> replaces the whole thing and works on every platform, including mobile.
  </Accordion>

  <Accordion title="Can I replace parts of the inbox on mobile?">
    Not yet, that is a web-SDK feature. On mobile, theme the built-in component or <Doc href="/docs/in-app/build-a-custom-inbox#build-your-own-ui">build your own UI</Doc> from the listener.
  </Accordion>

  <Accordion title="Do I still get real-time sync with a custom UI?">
    The SDK keeps the feed synced and calls your renderer or listener on every change. See <Doc href="/docs/in-app/how-the-inbox-behaves">How the inbox behaves</Doc>.
  </Accordion>
</AccordionGroup>
