> ## 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 preference center

> Theme the preferences component or build your own UI on the client methods behind it.

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

<Doc href="/docs/in-app/add-a-preference-center">Add a preference center</Doc> covers embedding it. This page is what you change once it renders.

## Theme

Theme the preferences center with `lightTheme` / `darkTheme` (a `CourierPreferencesTheme`). Set `mode` to `"light"`, `"dark"`, or `"system"`.

The default is `"system"`, which follows the user's OS setting.

<Frame caption="The Courier preferences center with a custom theme.">
  <img src="https://mintcdn.com/courier-4f1f25dc/Ia7FGvhsFU_5CtOa/assets/preferences-theme.webp?fit=max&auto=format&n=Ia7FGvhsFU_5CtOa&q=85&s=dd2922b0b7016216719db53e6cb53b1d" alt="The Courier preferences center with a custom purple theme applied to its topic toggles and channel checkboxes" className="mx-auto" width="3152" height="1776" data-path="assets/preferences-theme.webp" />
</Frame>

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

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

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

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

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

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

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

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

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

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

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

  ```swift iOS highlight={4-5,11-12} theme={null}
  // UIKit
  let preferences = CourierPreferences(
    mode: .channels(CourierUserPreferencesChannel.allCases),
    lightTheme: CourierPreferencesTheme(/* ... */),
    darkTheme: CourierPreferencesTheme(/* ... */)
  )

  // SwiftUI
  CourierPreferencesView(
    mode: .channels(CourierUserPreferencesChannel.allCases),
    lightTheme: CourierPreferencesTheme(/* ... */),
    darkTheme: CourierPreferencesTheme(/* ... */)
  )
  ```

  ```kotlin Android highlight={4-5} theme={null}
  // `mode` and the themes are properties on the view, not constructor arguments
  val preferences = findViewById<CourierPreferences>(R.id.courierPreferences)
  preferences.mode = CourierPreferences.Mode.Channels(CourierPreferenceChannel.allCases)
  preferences.lightTheme = CourierPreferencesTheme(/* ... */)
  preferences.darkTheme = CourierPreferencesTheme(/* ... */)
  ```

  ```dart Flutter highlight={3-4} theme={null}
  CourierPreferences(
    mode: ChannelsMode(channels: CourierUserPreferencesChannel.allCases),
    lightTheme: CourierPreferencesTheme(/* ... */),
    darkTheme: CourierPreferencesTheme(/* ... */),
  );
  ```

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

On the web the theme is a `CourierPreferencesTheme`, identical across React, Vue, Angular, and the Web Components, so it is documented once below. The native SDKs have their own: <Doc href="/docs/sdk-libraries/ios#preferences-theme">iOS</Doc>, <Doc href="/docs/sdk-libraries/android#preferences-theme">Android</Doc>, <Doc href="/docs/sdk-libraries/flutter#preferences-theme">Flutter</Doc>, and <Doc href="/docs/sdk-libraries/react-native#preferences-theme">React Native</Doc>.

### CourierPreferencesTheme reference

Every property is optional. Set `primaryColor` alone and it carries the accent through the toggles, radios, and chips, which is usually the whole change.

```ts theme={null}
type CourierPreferencesTheme = {
  primaryColor?: string;             // accent, defaults to the inbox blue
  title?: FontTheme;                 // the page heading
  subtitle?: FontTheme;              // the line under it
  container?: { font?: FontTheme };  // inherited by everything inside

  loading?: {
    animation?: {
      barColor?: string;
      barHeight?: string;
      barBorderRadius?: string;
      duration?: string;             // one shimmer cycle, e.g. '2s'
    };
  };

  // error and empty take the same shape
  error?: InfoStateTheme;
  empty?: InfoStateTheme;

  section?: {
    title?: FontTheme;
    description?: FontTheme;
    backgroundColor?: string;
  };

  topic?: {
    backgroundColor?: string;
    border?: string;
    borderRadius?: string;
    title?: FontTheme;
    description?: FontTheme;
    statusLabel?: FontTheme;
    toggle?: {
      trackColor?: string;
      trackActiveColor?: string;
      thumbColor?: string;
      borderRadius?: string;
    };
  };

  digest?: {
    font?: FontTheme;                // unselected schedule options
    selectedFont?: FontTheme;
    iconColor?: string;              // the trailing calendar icon
    radio?: {
      ringColor?: string;            // unselected ring
      checkedColor?: string;         // selected ring and inner dot
      font?: FontTheme;
      selectedFont?: FontTheme;
    };
  };

  channelChip?: {
    font?: FontTheme;
    selectedFont?: FontTheme;
    divider?: string;                // CSS border shorthand, above the chips
    checkbox?: {
      checkedColor?: string;         // border and fill when checked
      font?: FontTheme;
      selectedFont?: FontTheme;
    };
  };
};

type FontTheme = { family?: string; weight?: string; size?: string; color?: string };

type InfoStateTheme = {
  title?: { text?: string; font?: FontTheme };
  button?: {
    text?: string;                   // 'Retry' on the error state by default
    font?: FontTheme;
    backgroundColor?: string;
    hoverBackgroundColor?: string;
    border?: string;
    borderRadius?: string;
  };
};
```

**This is not the same shape as the inbox theme.** Preferences is a flat set of regions
(`section`, `topic`, `digest`, `channelChip`) with a single `primaryColor` on top, while
the inbox theme is a `popup` and `inbox` tree. A theme object written for one is not
partially valid for the other. It applies nothing.

Two fields exist but do nothing today: `loading.font`, kept for compatibility because the
loading state renders as a skeleton, and the deprecated `CourierPreferencesStatusTheme`.

## Custom UI

<Note>
  The preferences component has no render slots yet, so use the preferences client below instead, on any platform.
</Note>

To render preferences in your own UI instead of `CourierPreferences`, read and write topics through the preferences client. The same methods exist on every platform. Web frameworks expose them through `useCourier()` (React/Vue) or the injected service (Angular). Everywhere else uses `Courier.shared.client.preferences`:

<CodeGroup>
  ```jsx React theme={null}
  import { useCourier } from "@trycourier/courier-react";  // or "@trycourier/courier-react-17"

  const { preferences } = useCourier();

  // Read the user's topics, then write a change (client is set after signIn)
  const { items } = await preferences.getUserPreferences();

  await preferences.putUserPreferenceTopic({
    topicId: "TOPIC_ID",
    status: "OPTED_IN",
    hasCustomRouting: true,
    customRouting: ["inbox", "email"],
  });
  ```

  ```js Web Components theme={null}
  // Read the user's topics, then write a change (client is set after signIn)
  const { items } = await Courier.shared.client?.preferences.getUserPreferences();

  await Courier.shared.client?.preferences.putUserPreferenceTopic({
    topicId: "TOPIC_ID",
    status: "OPTED_IN",
    hasCustomRouting: true,
    customRouting: ["inbox", "email"],
  });
  ```

  ```ts Vue theme={null}
  import { useCourier } from "@trycourier/courier-vue";

  // `preferences` is plain (no .value)
  const { preferences } = useCourier();

  const { items } = await preferences.getUserPreferences();

  await preferences.putUserPreferenceTopic({
    topicId: "TOPIC_ID",
    status: "OPTED_IN",
    hasCustomRouting: true,
    customRouting: ["inbox", "email"],
  });
  ```

  ```ts Angular theme={null}
  import { inject } from "@angular/core";
  import { CourierService } from "@trycourier/courier-angular";

  const courier = inject(CourierService);

  const { items } = await courier.getUserPreferences();

  await courier.putUserPreferenceTopic({
    topicId: "TOPIC_ID",
    status: "OPTED_IN",
    hasCustomRouting: true,
    customRouting: ["inbox", "email"],
  });
  ```

  ```swift iOS theme={null}
  // `client` is available after signIn: Courier.shared.client
  let prefs = try await client.preferences.getUserPreferences(paginationCursor: nil)

  try await client.preferences.putUserPreferenceTopic(
    topicId: "TOPIC_ID",
    status: .optedIn,
    hasCustomRouting: true,
    customRouting: [.push]
  )
  ```

  ```kotlin Android theme={null}
  // `client` is available after signIn: Courier.shared.client
  val prefs = client.preferences.getUserPreferences(paginationCursor = null)

  client.preferences.putUserPreferenceTopic(
    topicId = "TOPIC_ID",
    status = CourierPreferenceStatus.OPTED_IN,
    hasCustomRouting = true,
    customRouting = listOf(CourierPreferenceChannel.PUSH)
  )
  ```

  ```dart Flutter theme={null}
  // Await the client, available after signIn
  final client = await Courier.shared.client;

  final prefs = await client.preferences.getUserPreferences(paginationCursor: null);

  await client.preferences.putUserPreferenceTopic(
    topicId: "TOPIC_ID",
    status: CourierUserPreferencesStatus.optedIn,
    hasCustomRouting: true,
    customRouting: [CourierUserPreferencesChannel.push],
  );
  ```

  ```typescript React Native theme={null}
  // Get the client, available after signIn
  const client = await Courier.shared.getClient();

  const prefs = await client.preferences.getUserPreferences({
    paginationCursor: undefined,
  });

  await client.preferences.putUserPreferenceTopic({
    topicId: "TOPIC_ID",
    status: CourierUserPreferencesStatus.OptedIn,
    hasCustomRouting: true,
    customRouting: [CourierUserPreferencesChannel.Push],
  });
  ```
</CodeGroup>

See your platform's SDK reference for the exact signatures.

## Methods behind the component

The component reads and writes through `courier.shared.preferences`, so a UI you build
yourself calls the same client. `useCourier()` returns the client in React.

| Method                                                                                          | Returns                                                                        |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| `getUserPreferences({ paginationCursor? })`                                                     | `CourierUserPreferences`, the user's topics and their statuses.                |
| `getUserPreferenceTopic({ topicId })`                                                           | `CourierUserPreferencesTopic` for one topic.                                   |
| `putUserPreferenceTopic({ topicId, status, hasCustomRouting, customRouting, digestSchedule? })` | The updated topic.                                                             |
| `getPreferencePage({ accountId?, brandId?, draft? })`                                           | `CourierPreferencePage`, the published section and topic layout, or the draft. |
| `getDigestSchedules({ topicId })`                                                               | The digest schedule options a topic offers.                                    |

**`putUserPreferenceTopic` replaces the topic's routing rather than merging into it.** Send
`hasCustomRouting` and the full `customRouting` array together, because omitting a channel
removes it.

```tsx theme={null}
const courier = useCourier();

const prefs = await courier.shared.preferences.getUserPreferences();
await courier.shared.preferences.putUserPreferenceTopic({
  topicId: "order-updates",
  status: "OPTED_IN",
  hasCustomRouting: true,
  customRouting: ["email"],
});
```
