Theming

Background

Stream's UI SDK makes it easy for developers to add custom styles and attributes to widgets. Starting with the design-refresh release, Stream uses StreamTheme — a Flutter ThemeExtension — instead of a dedicated wrapper widget.

StreamTheme is read via Theme.of(context) like any other ThemeExtension. You typically pass a customized instance through MaterialApp.theme.extensions (or MaterialApp.darkTheme.extensions); if you don't, StreamChat resolves a default for you (see below).

Setting Up StreamTheme

For the default look, you don't need to wire anything — just drop StreamChat into your tree. On build, StreamChat reads StreamTheme.of(context), falling back to a light or dark default based on the surrounding Theme.of(context).brightness, and appends it to the ambient Theme so every descendant Stream widget can resolve it via the standard theme lookup.

MaterialApp(
  home: StreamChat(
    client: client,
    child: const MyHomePage(),
  ),
)

Customizing StreamTheme

To customize, construct a StreamTheme and pass it through MaterialApp.theme.extensions (and darkTheme.extensions for dark mode). StreamChat picks it up via StreamTheme.of(context) and propagates it to descendants.

The most common customization is the color scheme. Supply your own brand and chrome swatches to StreamColorScheme.light() or StreamColorScheme.dark() and pass the result to StreamTheme. Those two swatches drive the accent, text, background, and border tokens automatically inside the factory, so the palette stays cohesive.

Always build the color scheme through StreamColorScheme.light() / StreamColorScheme.dark(), not copyWith. copyWith overrides a single field without re-running the derivation, so changing brand or chrome through it leaves the dependent tokens on their defaults.

MaterialApp(
  theme: ThemeData(
    brightness: Brightness.light,
    extensions: [
      StreamTheme(
        brightness: Brightness.light,
        colorScheme: StreamColorScheme.light(
          brand: StreamColorSwatch.fromColor(Colors.indigo),
          chrome: StreamColorSwatch.fromColor(Colors.blueGrey),
        ),
        avatarTheme: const StreamAvatarThemeData(
          // Customize avatar defaults...
        ),
      ),
    ],
  ),
  darkTheme: ThemeData(
    brightness: Brightness.dark,
    extensions: [
      StreamTheme(
        brightness: Brightness.dark,
        colorScheme: StreamColorScheme.dark(
          brand: StreamColorSwatch.fromColor(
            Colors.indigo,
            brightness: Brightness.dark,
          ),
          chrome: StreamColorSwatch.fromColor(
            Colors.blueGrey,
            brightness: Brightness.dark,
          ),
        ),
      ),
    ],
  ),
  home: StreamChat(
    client: client,
    child: const MyHomePage(),
  ),
)

Reading the Theme in Widgets

There are three ways to read theme values. Pick whichever reads cleanest at the call site.

1. The aggregate themes — StreamTheme.of(context) and StreamChatTheme.of(context)

StreamTheme.of(context) returns the core aggregate (primitives, semantic tokens, and core component themes). StreamChatTheme.of(context) returns the chat aggregate (chat-specific component themes like messageListViewTheme, channelListItemTheme, threadListTileTheme). Reach for one of these when you need several values at once.

final theme = StreamTheme.of(context);
final color = theme.colorScheme.accentPrimary;
final pad = theme.spacing.md;

2. Per-component StreamFooTheme.of(context)

Every component theme is also an InheritedTheme with its own static .of that merges any nearest-ancestor subtree override (StreamButtonTheme(data: ..., child: ...)) with the aggregate value. Use this form inside any widget that might be wrapped in a scoped theme override — it picks up the override automatically. Works for both core and chat component themes.

final buttonTheme = StreamButtonTheme.of(context);            // core
final listTheme   = StreamMessageListViewTheme.of(context);   // chat

3. BuildContext extensions (core only)

stream_core_flutter exposes a getter on BuildContext for every value in StreamThemecontext.streamTheme, context.streamColorScheme, context.streamSpacing, context.streamButtonTheme, and so on. The component-theme getters are equivalent to calling the matching StreamFooTheme.of(context), so they pick up subtree overrides the same way. Chat component themes don't have extensions; use their static .of(context) accessor.

Container(
  color: context.streamColorScheme.backgroundPrimary,
  padding: EdgeInsets.all(context.streamSpacing.md),
)

Brand Color

The most basic customization you can do is to change the brand color. Set the brand and chrome swatches on StreamColorScheme when creating your StreamTheme. UI elements such as the send button, active borders, outgoing message bubbles, and text links automatically inherit the new brand color. The chrome swatch controls the neutral palette — timestamps, placeholders, and borders — so giving it a tint that complements your brand keeps the entire UI cohesive.

StreamColorSwatch

Both brand and chrome are a StreamColorSwatch which extends Flutter's ColorSwatch and represents a full palette of shades derived from a single base color. The factory StreamColorSwatch.fromColor generates the complete range automatically using HSL color space:

  • Shade 0 — lightest (white in light mode)
  • Shade 500 — the exact color you supply
  • Shade 1000 — darkest (black in light mode)

In light mode the scale runs light-to-dark (lower numbers are lighter). For dark mode, pass brightness: Brightness.dark and the scale inverts — shade 0 becomes the darkest and shade 1000 the lightest — so the palette integrates naturally with dark backgrounds.

For example, switching the brand and chrome to red in both light and dark mode:

MaterialApp(
  theme: ThemeData(
    extensions: [
      StreamTheme(
        brightness: Brightness.light,
        colorScheme: StreamColorScheme.light(
          brand: StreamColorSwatch.fromColor(const Color(0xFFE91E63)),
          chrome: StreamColorSwatch.fromColor(const Color(0xFFC9A8A8)),
        ),
      ),
    ],
  ),
  darkTheme: ThemeData(
    brightness: Brightness.dark,
    extensions: [
      StreamTheme(
        brightness: Brightness.dark,
        colorScheme: StreamColorScheme.dark(
          brand: StreamColorSwatch.fromColor(
            const Color(0xFFE91E63),
            brightness: Brightness.dark,
          ),
          chrome: StreamColorSwatch.fromColor(
            const Color(0xFFC9A8A8),
            brightness: Brightness.dark,
          ),
        ),
      ),
    ],
  ),
  themeMode: ThemeMode.system,
  home: MyHomePage(),
)
BeforeAfter

Color Tokens

StreamColorScheme defines the semantic color palette used throughout the Stream SDK. All tokens are accessible via StreamTheme.of(context).colorScheme.

Brand and Chrome

brand and chrome are StreamColorSwatch objects — multi-shade palettes that serve as the source of truth for all derived semantic tokens. You typically override these two instead of individual tokens, and the SDK derives the rest automatically.

TokenDescription
brandThe primary brand color swatch with shades from 50 to 900. Drives accentPrimary, textLink, borderActive, and focus states. Defaults to Stream blue.
chromeThe neutral chrome color swatch with shades from 0 (white) to 1000 (black). Drives most text, background, and border tokens. Defaults to a neutral gray scale.

Each swatch exposes named shades via shade50, shade100, …, shade900 (and shade0 / shade1000 for chrome). Pass a custom swatch when creating StreamColorScheme.light() or StreamColorScheme.dark():

StreamColorScheme.light(
  brand: StreamColorSwatch.fromColor(Colors.indigo),
  chrome: StreamColorSwatch.fromColor(Colors.blueGrey),
)

See the Brand Color section above for a working example overriding brand and chrome together, with a before/after screenshot.

Accent

TokenDescription
accentPrimaryThe main brand color. Used for interactive elements, buttons, links, and primary actions. Override this to apply your brand color across the SDK.
accentSuccessIndicates a positive or completed state. Used for confirmations and success feedback.
accentWarningIndicates a cautionary state. Used for warnings and non-critical alerts.
accentErrorIndicates a failure or destructive state. Used for failed messages, validation errors, and deletions.
accentNeutralA mid-tone gray for de-emphasized UI elements.

Background — Surface

TokenDescription
backgroundAppThe outermost application background. Sits behind all surfaces and is generally not overridden directly.
backgroundSurfaceBackground for sectioned content areas. Used for grouped containers and distinct content regions.
backgroundSurfaceSubtleA slightly receded background. Used for secondary containers or to create soft visual separation.
backgroundSurfaceCardBackground for contained, card-style elements. Matches the surface in light mode but lifts slightly in dark mode to maintain visual separation.
backgroundSurfaceStrongA more prominent background. Used for elements that need to stand out from the main surface.
backgroundInverseThe opposite of the primary surface. Used for tooltips, snackbars, and high-contrast floating elements.
backgroundOnAccentBackground for elements placed on an accent-colored surface. Ensures legibility against brand colors.
backgroundHighlightA tint for drawing attention to content. Used for highlights and pinned messages.
backgroundOverlayLightA light semi-transparent layer. Used to lighten surfaces and for hover states on dark backgrounds.
backgroundOverlayDarkA dark semi-transparent layer. Used for image overlays.
backgroundScrimA heavy semi-transparent layer. Used behind sheets, drawers, and modals to separate them from content.
backgroundDisabledBackground for non-interactive elements. Flattens the element visually to signal unavailability.

Background — State

TokenDescription
backgroundHoverA subtle overlay applied on hover. Provides feedback on interactive elements on pointer devices.
backgroundPressedA slightly stronger overlay applied during an active press or tap. Provides tactile feedback.
backgroundSelectedIndicates an active or selected state. Used for selected messages, active list items, and controls.

Text

TokenDescription
textPrimaryMain body text. Used for message content, titles, and any text that carries primary meaning.
textSecondarySupporting metadata text. Used for timestamps, subtitles, and secondary labels.
textTertiaryDe-emphasized text. Used for hints, placeholders, and lowest-priority supporting information.
textOnInverseText on inverse-colored surfaces. Flips between light and dark to maintain legibility when the background inverts.
textOnAccentText on accent-colored surfaces. Stays white in both light and dark mode since the accent background does not invert.
textDisabledText for non-interactive or unavailable states. Communicates that an element cannot be interacted with.
textLinkHyperlinks and inline actions. Uses the brand color to signal interactivity within text content.

Border — Core

TokenDescription
borderDefaultStandard border for surfaces and containers. Used for input fields, cards, and dividers on neutral backgrounds.
borderSubtleA lighter border for minimal separation. Used where a full-strength border would feel too heavy.
borderStrongAn emphatic border for elements that need clear definition. Used for focused containers and prominent dividers.
borderOnAccentBorder on accent-colored surfaces. Stays white in both light and dark mode since the accent background does not invert.
borderOnInverseBorder on inverse-colored surfaces. Stays legible when the background flips between light and dark mode.
borderOnSurfaceBorder for elements placed on a surface background.
borderOpacitySubtleA very light transparent border. Used as a frame treatment on images and media attachments.
borderOpacityStrongA stronger transparent border for elements on colored or dark backgrounds. Used for waveform bars and similar treatments.

Border — Utility

TokenDescription
borderFocusFocus ring applied to interactive elements when focused via keyboard or accessibility tools.
borderDisabledBorder for non-interactive elements. Matches the disabled surface to visually flatten the element.
borderDisabledOnSurfaceBorder for disabled elements on elevated surfaces. Stays visually distinct from the surface without drawing attention.
borderHoverBorder overlay applied on hover. Used for interactive containers on pointer devices.
borderPressedBorder overlay applied during an active press or tap.
borderActiveBorder indicating the active or focused state of an input or control.
borderErrorBorder indicating a validation error or failure state.
borderWarningBorder indicating a cautionary or warning state.
borderSuccessBorder indicating a successful or confirmed state.
borderSelectedBorder indicating a selected state.

Avatar

The avatar palette is a list of StreamAvatarColorPair objects, each with a backgroundColor and foregroundColor. Colors are assigned deterministically based on the user's name or ID.

PropertyDescription
backgroundColorBackground color for the avatar circle.
foregroundColorColor for the avatar initials or icon.

Elevation

The Stream design system uses a single elevation scale (04) to express vertical hierarchy. Higher levels sit visually closer to the user. Each level pairs two things: a surface color (read from the color scheme) and a drop shadow (rendered by Flutter's Material(elevation:), mapped to the same dp value Material uses).

Elevation tokens in light and dark mode

LevelMaterial dpSurface color tokenUsage
00backgroundElevation0Base surfaces — screen background, main content plane. No shadow.
11backgroundElevation1Subtle separation within content — small contained components, the message list, channel list.
23backgroundElevation2Raised surfaces — sticky headers, contained toolbars, badge counts.
36backgroundElevation3Floating, non-blocking overlays — context menus, reaction picker, floating composer, snackbars.
46–8(uses surface tokens)Blocking overlays and modal surfaces — sheets. Combine with backgroundScrim behind the sheet.

In light mode, levels 03 all resolve to white and the depth cue is the shadow alone. In dark mode, the surface tokens step progressively lighter so depth is communicated by background tint as well as shadow.

To apply a level to a Stream component, set its theme's elevation field — e.g. StreamSheetThemeData.elevation, StreamContextMenuThemeData.elevation, StreamReactionPickerThemeData.elevation. The integer flows straight into the underlying Material widget. For a custom widget, wrap it in Material(elevation: N) with the matching integer and read the surface color from the matching backgroundElevationN on context.streamColorScheme.

See Material 3 elevation for the underlying shadow algorithm.

Icon Assets

StreamIcons holds the IconData for every icon used across Stream widgets. Each icon is a standard Flutter IconData, so you can substitute any icon from Material Icons, Cupertino Icons, or your own icon font.

Pass a custom StreamIcons to StreamTheme via the icons parameter:

MaterialApp(
  theme: ThemeData(
    extensions: [
      StreamTheme(
        brightness: Brightness.light,
        icons: const StreamIcons(
          send: Icons.reply_rounded,
        ),
      ),
    ],
  ),
  home: MyHomePage(),
)
BeforeAfter

If the same icon is used in multiple places, replacing it in StreamIcons updates every occurrence across all Stream widgets at once.

You can also read icons from anywhere in the widget tree:

final sendIcon = context.streamIcons.send;

Two-Layer Theme Architecture

Stream Chat uses two complementary theme layers:

  • StreamTheme (design-system tokens) — shared across all Stream products. Controls color scheme, typography, avatar sizing, badges, reaction picker appearance, and other low-level primitives. Provided as a ThemeExtension on MaterialApp.theme.
  • StreamChatThemeData (chat-specific themes) — controls styling for chat components like message bubbles, channel list items, message input, polls, and galleries. Passed to StreamChat.themeData.

Both are optional — sensible defaults are applied automatically.

Per-Component Theme Objects

Each component has its own theme data class. Depending on which layer it belongs to, you configure it differently:

Design-system themes (via StreamTheme):

ComponentTheme Class
Message itemsStreamMessageItemThemeData
Reaction pickerStreamReactionPickerThemeData
AvatarsStreamAvatarThemeData
BadgesStreamBadgeNotificationThemeData
Text inputsStreamTextInputThemeData

Chat-specific themes (via StreamChatThemeData):

ComponentTheme Class
Channel list itemsStreamChannelListItemThemeData
Channel headerStreamAppBarThemeData
PollsStreamPollCreatorThemeData, StreamPollInteractorThemeData
Thread listStreamThreadListTileThemeData
Voice recordingStreamVoiceRecordingAttachmentThemeData

Example — customizing channel list items globally:

MaterialApp(
  theme: ThemeData(
    extensions: [
      StreamTheme.light(),
    ],
  ),
  home: StreamChat(
    client: client,
    themeData: StreamChatThemeData(
      channelListItemTheme: StreamChannelListItemThemeData(
        titleStyle: const TextStyle(fontWeight: FontWeight.bold),
        subtitleStyle: const TextStyle(color: Colors.grey),
        timestampStyle: const TextStyle(fontSize: 12),
      ),
    ),
    child: const MyHomePage(),
  ),
)

Subtree Theme Overrides

StreamTheme and the per-component theme classes are InheritedWidgets — they follow the same nearest-ancestor-wins rule as Flutter's built-in Theme. Place the root StreamTheme once via MaterialApp.theme.extensions, then drop a per-component theme widget anywhere in the tree to scope an override to that subtree. The nearest ancestor wins, so a nested StreamChannelListItemTheme (for example, around a single StreamChannelListView) overrides the global value without affecting the rest of the app.

StreamChannelListItemTheme(
  data: StreamChannelListItemThemeData(
    titleStyle: const TextStyle(color: Colors.blue),
  ),
  child: StreamChannelListView(controller: controller),
)

Light and Dark Mode

Pass different StreamTheme instances to MaterialApp.theme and MaterialApp.darkTheme to support both modes:

MaterialApp(
  theme: ThemeData(
    extensions: [StreamTheme.light()],
  ),
  darkTheme: ThemeData(
    brightness: Brightness.dark,
    extensions: [StreamTheme.dark()],
  ),
  themeMode: ThemeMode.system,
  home: MyHomePage(),
)

Global Configuration

For global configuration options, use StreamChatConfigurationData passed to StreamChat.configData. This controls behavioral and structural settings that are independent of theming:

StreamChat(
  client: client,
  configData: StreamChatConfigurationData(
    reactionIconResolver: const MyReactionIconResolver(),
    enforceUniqueReactions: true,
    draftMessagesEnabled: true,
    imageCDN: const StreamImageCDN(),
    attachmentBuilders: [
      MyCustomAttachmentBuilder(),
      ...StreamAttachmentWidgetBuilder.defaultBuilders(message: message),
    ],
  ),
  child: MyHomePage(),
)
PropertyDescription
reactionIconResolverMaps reaction types to emoji/widgets. Defaults to DefaultReactionIconResolver
enforceUniqueReactionsWhether a new reaction replaces the existing one. Defaults to true
draftMessagesEnabledEnables draft message support. Defaults to false
imageCDNImage CDN for generating resized URLs and cache keys. Defaults to StreamImageCDN
attachmentBuildersCustom attachment renderers prepended to the defaults
reactionTypenull by default; StreamMessageReactions falls back to StreamReactionsType.segmented.
reactionPositionnull by default; falls back to StreamReactionsPosition.header. header overlaps the bubble edge; footer sits flush below it.
messagePreviewFormatterFormatter for message previews in channel lists