Skip to main content

Branding & Styling Reference

The Inbox component is fully themeable through the appearance prop. This reference covers every level of customization — from dropping in a base theme to building dynamic, severity-aware, brand-perfect styles.
Inspiration: inbox.novu.co showcases pre-built variants like Notion and Reddit.

The appearance prop

type Appearance = {
  baseTheme?: BaseTheme;
  variables?: Variables;
  elements?: Record<string, string | StyleObject | ((ctx) => string)>;
  icons?: Record<string, () => ReactNode>;
};
KeyPurpose
baseThemeStart from a predefined theme (e.g. dark)
variablesGlobal design tokens (colors, fonts, radius, severity colors)
elementsPer-element styles — string, style object, or callback
iconsReplace built-in icons with your own React components
When both baseTheme and variables are set, variables win. Styles are auto-injected into <head>. If the Inbox is rendered inside a shadow DOM, styles are scoped to that shadow root.

Base themes

Novu currently ships a dark base theme:
import { Inbox } from "@novu/react";
import { dark } from "@novu/react/themes";

<Inbox
  applicationIdentifier="YOUR_NOVU_APP_ID"
  subscriberId="subscriber-123"
  appearance={{ baseTheme: dark }}
/>
Compose baseTheme + variables to start from a theme and tweak just a few tokens:
appearance={{
  baseTheme: dark,
  variables: { colorPrimary: "#FB4CA3", borderRadius: "12px" },
}}

Variables (design tokens)

VariableDescription
colorBackgroundInbox background
colorForegroundPrimary text color
colorPrimaryAccent color for interactive elements
colorPrimaryForegroundText on primary surfaces
colorSecondaryLess prominent surfaces
colorSecondaryForegroundText on secondary surfaces
colorCounterBackground of unread counters
colorCounterForegroundText inside counters
colorNeutralBorders and neutral surfaces
colorShadowShadow color
fontSizeBase font size
borderRadiusBorder radius applied across elements
colorSeverityHighHigh-severity accent
colorSeverityMediumMedium-severity accent
colorSeverityLowLow-severity accent
appearance={{
  variables: {
    colorBackground: "#0B0B0F",
    colorForeground: "#F4F4F6",
    colorPrimary: "#FB4CA3",
    colorPrimaryForeground: "#FFFFFF",
    colorSecondary: "#1A1A22",
    colorSecondaryForeground: "#A1A1AA",
    colorCounter: "#FB4CA3",
    colorCounterForeground: "#FFFFFF",
    colorNeutral: "#27272A",
    colorShadow: "rgba(0, 0, 0, 0.4)",
    fontSize: "14px",
    borderRadius: "10px",
    colorSeverityHigh: "#E5484D",
    colorSeverityMedium: "#F76808",
    colorSeverityLow: "#3E63DD",
  },
}}

Element-level styling

Each key in appearance.elements accepts:
  • String of class names — CSS, CSS Modules, Tailwind
  • Style object — inline CSS
  • Callback(context) => string returning class names, evaluated on every render with the relevant context (notification, unreadCount, preference, schedule)

Inline style object

appearance={{
  elements: {
    notificationSubject: { color: "#ff0000", fontWeight: 600 },
  },
}}

Tailwind classes

appearance={{
  elements: {
    bellIcon: "p-4 bg-white rounded-full",
    notification: "bg-white rounded-lg shadow-sm hover:shadow-md hover:bg-gray-50",
    notificationPrimaryAction__button: "bg-pink-500 text-white px-3 py-1 rounded",
  },
}}

CSS Modules

/* inbox.module.css */
.bellIcon {
  padding: 1rem;
  background-color: white;
  border-radius: 50%;
}
.bellIcon:hover { background-color: #f9fafb; }

.notification {
  background: white;
  border-radius: 0.5rem;
  box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
import styles from "./inbox.module.css";

appearance={{
  elements: {
    bellIcon: styles.bellIcon,
    notification: styles.notification,
  },
}}

Dynamic styling via callbacks

Callbacks receive contextual data and return class names. Use them for state-aware styles (unread count, notification severity, payload data):
appearance={{
  elements: {
    bellIcon: ({ unreadCount }) => {
      if (unreadCount.total > 10) {
        return "[--bell-gradient-start:var(--color-red-500)] [--bell-gradient-end:var(--color-red-500)]";
      }
      if (unreadCount.total > 0) {
        return "[--bell-gradient-start:var(--color-yellow-500)] [--bell-gradient-end:var(--color-yellow-500)]";
      }

      return "[--bell-gradient-start:var(--color-gray-500)] [--bell-gradient-end:var(--color-gray-500)]";
    },
    notification: ({ notification }) =>
      notification.data?.priority === "high" ? "bg-red-50 ring-1 ring-red-300" : "",
    severityHigh__notificationBar: ({ notification }) =>
      notification.read ? "opacity-50" : "opacity-100",
  },
}}

Common element keys

ElementKey
Notification containernotification
Notification subject textnotificationSubject
Notification body textnotificationBody
Notification image / iconnotificationImage
Notification datenotificationDate
Notification listnotificationList
Primary action buttonnotificationPrimaryAction__button
Secondary action buttonnotificationSecondaryAction__button
Archive buttonnotificationArchive__button
Snooze buttonnotificationSnooze__button
Mark unread buttonnotificationUnread__button
Bell iconbellIcon
Bell containerbellContainer
Bell dot (unread indicator)bellDot
Popover contentpopoverContent
Preferences buttonpreferences__button
Schedule containerscheduleContainer
Schedule headerscheduleHeader
Schedule body / tablescheduleBody, scheduleTable
To find any element key, inspect the DOM. Class names starting with nv- (visible just before a 🔔 emoji in DevTools) map to keys in elements. Drop the nv- prefix.

Callback context signatures

Element groupContext
bellIcon, bellContainer, bellDot, bellSeverityGlow, severity*__bellContainer{ unreadCount: { total: number; severity: Record<string, number> } }
notification, notification*, severity*__notification*, notificationDot{ notification: Notification }
notificationList, notificationListContainer{ notifications: Notification[] }
workflow*, channelsContainer, channelName{ preference: Preference }
channelContainer, channelLabelContainer, channelIconContainer, channelLabel, channelSwitchContainer, channel__icon{ preference?: Preference; preferenceGroup?: { name: string; preferences: Preference[] } }
preferencesGroup*{ preferenceGroup: { name: string; preferences: Preference[] } }
preferencesContainer{ preferences?: Preference[]; groups: Array<{ name: string; preferences: Preference[] }> }
schedule*, dayScheduleCopy*, timeSelect*{ schedule?: Schedule }

Severity styling

Notifications expose three severity levels: high, medium, low. Styling can be applied through variables (global colors) or elements (precise overrides).

Severity variables

VariableDescription
colorSeverityHighHigh severity color
colorSeverityMediumMedium severity color
colorSeverityLowLow severity color
appearance={{
  variables: {
    colorSeverityHigh: "#E5484D",
    colorSeverityMedium: "#F76808",
    colorSeverityLow: "#3E63DD",
  },
}}
Updating these automatically restyles both the notification accent bar and the bell glow.

Severity element keys

KeyDescription
severityHigh__bellContainerBell container for high severity
severityMedium__bellContainerBell container for medium severity
severityLow__bellContainerBell container for low severity
bellSeverityGlowBase bell glow style
severityGlowHigh__bellSeverityGlowGlow for high severity
severityGlowMedium__bellSeverityGlowGlow for medium severity
severityGlowLow__bellSeverityGlowGlow for low severity
severityHigh__notificationHigh severity notification row
severityMedium__notificationMedium severity notification row
severityLow__notificationLow severity notification row
notificationBarVertical bar on the left of a notification
severityHigh__notificationBarBar for high severity
severityMedium__notificationBarBar for medium severity
severityLow__notificationBarBar for low severity
appearance={{
  elements: {
    severityHigh__notificationBar: { backgroundColor: "red" },
    severityHigh__bellContainer: "ring-2 ring-red-500",
    severityGlowHigh__bellSeverityGlow: "bg-red-500/40 blur-md",
  },
}}
By default the bell takes the color of the highest-severity unread notification.

Custom icons

Replace built-in icons with anything that renders to a React node:
import { Inbox } from "@novu/react";
import {
  RiSettings3Fill,
  RiArrowDownLine,
  RiNotification3Fill,
} from "react-icons/ri";

<Inbox
  applicationIdentifier="YOUR_NOVU_APP_ID"
  subscriberId="subscriber-123"
  appearance={{
    icons: {
      bell: () => <RiNotification3Fill />,
      cogs: () => <RiSettings3Fill />,
      arrowDown: () => <RiArrowDownLine />,
    },
  }}
/>

Icon keys

KeyDescription
arrowDownDown arrow used in drop-downs and expandable sections
arrowDropDownDrop-down arrow in menus and selectors
arrowLeftLeft arrow used in pagination/navigation
arrowRightRight arrow used in pagination/navigation
bellNotification bell in the header
chatChat channel icon in preferences
checkCheckmark for selected items
clockDate/time display
cogsSettings/preferences icon
dotsThree-dot menu in notification items
emailEmail channel icon in preferences
inAppIn-app channel icon in preferences
markAsArchivedArchive notification
markAsArchivedReadArchive + read
markAsReadMark as read
markAsUnreadMark as unread
markAsUnarchivedUnarchive
pushPush channel icon in preferences
smsSMS channel icon in preferences
trashDelete
unreadUnread indicator
unsnoozeUnsnooze indicator
To find more keys, inspect the DOM for class names starting with nv- containing a 🖼️ emoji. The part after nv- is the icon key.

Responsive Inbox

<Inbox
  applicationIdentifier="YOUR_NOVU_APP_ID"
  subscriberId="subscriber-123"
  appearance={{ elements: { popoverContent: "novu-popover-content" } }}
/>
/* global.css */
.novu-popover-content { max-width: 500px; }

@media (max-width: 768px) { .novu-popover-content { max-width: 350px; } }
@media (max-width: 480px) { .novu-popover-content { max-width: 250px; } }
@media (max-width: 320px) { .novu-popover-content { max-width: 200px; } }
For a mobile drawer experience, use the Custom Popover pattern with shadcn’s <Drawer>.

Brand-presets cookbook

Notion-style (light, calm)

appearance={{
  variables: {
    colorBackground: "#FFFFFF",
    colorForeground: "#37352F",
    colorPrimary: "#2383E2",
    colorPrimaryForeground: "#FFFFFF",
    colorSecondary: "#F7F6F3",
    colorSecondaryForeground: "#787774",
    colorNeutral: "#E9E9E7",
    colorShadow: "rgba(15, 15, 15, 0.05)",
    fontSize: "14px",
    borderRadius: "6px",
  },
  elements: {
    notification: "hover:bg-[#F7F6F3] transition-colors",
    notificationSubject: { fontWeight: 600 },
  },
}}

Reddit-style (vibrant)

appearance={{
  variables: {
    colorPrimary: "#FF4500",
    colorPrimaryForeground: "#FFFFFF",
    colorBackground: "#FFFFFF",
    colorForeground: "#1A1A1B",
    colorSecondary: "#F6F7F8",
    colorCounter: "#FF4500",
    colorCounterForeground: "#FFFFFF",
    borderRadius: "16px",
  },
}}

Brand-locked dark

import { dark } from "@novu/react/themes";

appearance={{
  baseTheme: dark,
  variables: {
    colorPrimary: "#FB4CA3",
    colorPrimaryForeground: "#FFFFFF",
    borderRadius: "12px",
  },
}}