# Theming

The chat interface resolves its colours from three layers. Understanding the order is the whole of this page.

## Precedence

```text
Remote / dashboard theme      highest priority
        overrides
Local / SDK theme
        overrides
Native / built-in theme       final fallback
```

The merchant's dashboard configuration wins, because the merchant owns how their support experience looks. Your local theme fills in whatever the dashboard did not set. The built-in theme fills in whatever neither of those set, so **every colour always has a value**.

## Merging is per property, not per object

Layers are merged token by token, not swapped wholesale. Setting one colour locally does not discard the rest of the theme.

```text
Built-in:  headerBackground = purple,  chatBackground = off-white,  sentBubble = purple
Local:     headerBackground = green
Remote:    sentBubble = navy

Resolved:  headerBackground = green    (remote silent, local wins)
           sentBubble       = navy     (remote wins)
           chatBackground   = off-white (neither set it, built-in wins)
```

Nested structures (the background image block) merge the same way: a remote opacity does not erase a local image URL.

## The colour tokens

Every token names a visible part of the interface, and the same names are used by the dashboard, the web widget and all four mobile SDKs.

`chatBackground`, `headerBackground`, `headerPrimaryText`, `headerSecondaryText`, `headerIcon`, `closeButton`, `receivedBubble`, `receivedText`, `receivedTimestamp`, `sentBubble`, `sentText`, `sentTimestamp`, `systemMessageText`, `daySeparatorBackground`, `daySeparatorText`, `footerContainer`, `chatInputBackground`, `chatInputText`, `chatInputPlaceholder`, `chatInputBorder`, `sendButtonBackground`, `sendButtonIcon`, `attachmentButton`, `emojiButton`, `typingIndicatorBackground`, `typingIndicatorDot`, `unreadBadgeBackground`, `unreadBadgeText`, `scrollToBottomButtonBackground`, `scrollToBottomButtonIcon`, `onlineStatus`, `offlineStatus`, `error`, `success`, `warning`.

Values are CSS colours on the web and hex strings (`#RRGGBB` or `#AARRGGBB`) on mobile. An unusable value is ignored and the next layer down applies, so a bad colour never breaks the interface.

## Light and dark

`mode` picks which built-in palette forms the base. It follows the same precedence, with one rule: the dashboard's **auto** means "no opinion" and does not consume its turn, so your local `dark` still applies when the merchant has not pinned a mode. If nobody pins one, the viewer's system preference decides.

```text
Remote mode = light | dark   ->  wins
Remote mode = auto (or none) ->  your local mode wins
Neither                      ->  the device / browser setting
```

## Setting a local theme

```html
<!-- Web: script tag. A preset, or JSON for individual tokens. -->
<script src="https://widget.newinstance.cloud/embed.js"
  data-customer-token="TOKEN_FROM_YOUR_BACKEND"
  data-theme='{"mode":"dark","colors":{"sentBubble":"#16a34a"}}'
></script>
```

```js
NInstanceChat.init({
  customerToken: tokenFromYourBackend,
  theme: { mode: 'dark', colors: { sentBubble: '#16a34a' } },
});
```

```kotlin
LiveAndAiChatConfig(
    customerToken = tokenFromYourBackend,
    theme = ChatThemeOverride(sentBubble = "#16A34A", mode = ChatThemeOverride.Mode.DARK),
)
```

```swift
try LiveAndAiChatConfig(
    customerToken: tokenFromYourBackend,
    theme: ChatThemeOverride(mode: .dark, sentBubble: "#16A34A")
)
```

```ts
new NewinstanceChat({
  customerToken: tokenFromYourBackend,
  theme: { mode: 'dark', sentBubble: '#16A34A' },
});
```

```dart
NewinstanceChatConfig(
  customerToken: tokenFromYourBackend,
  theme: ChatThemeOverride(mode: 'dark', sentBubble: '#16A34A'),
);
```

The web widget also accepts `'dark'` / `'light'` as a bare string, and still accepts the older flat palette (`primaryColor`, `backgroundColor`, `textColor`, and so on), which is translated onto the tokens above.

## When configuration cannot be fetched

An invalid key, a disabled chat, an unreachable backend, a timeout, a malformed response: none of these leave the interface unstyled.

```text
Remote configuration loads      ->  remote + local + built-in
Remote configuration fails      ->  local + built-in
No local theme either           ->  built-in
```

A previously valid local theme is never destroyed by a failed fetch, the interface never sits blank or half-painted, and no authentication or configuration detail is shown to the customer. The failure is reported to you on the error channel as `CONFIG_FETCH_FAILED` (see **Authentication and identity -> Diagnosing problems**), which is a warning, not a fatal error.

A partly usable remote response is used partly: the tokens that parsed are applied, and the ones that did not fall through to your local theme and then the built-in palette.

## Authentication and theming are independent

They fail independently and are handled independently.

| Authentication | Remote theme | Result |
|---|---|---|
| Succeeds | Loads | Chat works, fully themed by the dashboard |
| Succeeds | Fails | Chat works, themed by your local theme and the built-in palette |
| Anonymous visitor | Loads | Chat works, fully themed; identity is irrelevant to theming |
| Fails | n/a | Chat does not open, and you get a coded diagnostic |

## One resolution, one interface

Theme precedence is resolved in exactly one place per SDK, and every part of the interface (header, message list, composer, day separators, buttons, badges) consumes that one resolved theme. There is no path by which the header can render the dashboard theme while the composer renders yours.
