Custom Channel Background

Basic Custom Background

For a static background, wrap MessageList and MessageInput with ImageBackground.

Best Practices

  • Keep message list backgrounds transparent so wallpapers show through without overlay artifacts.
  • Ensure text contrast remains accessible against bright or busy images.
  • Cache or preload background images to avoid layout jank when entering a channel.
  • Persist per-channel settings so returning users see consistent visuals.
  • Provide a safe default background when user-selected assets fail to load.

167857632 C0bc9d67 0a84 4cf5 9d75 305e3bcd1f3d

Also adjust the theme so the list background is transparent.

import {
  Channel,
  MessageInput,
  MessageList,
  ThemeProvider,
} from "stream-chat-react-native";
import { ImageBackground } from "react-native";

export const theme = {
  messageList: {
    container: {
      backgroundColor: "transparent",
    },
  },
};

const IMAGE_URI =
  "https://images.unsplash.com/photo-1549125764-91425ca48850?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8NjF8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60";

const ChannelScreen = ({ channel }) => (
  <ThemeProvider style={theme}>
    <Channel channel={channel}>
      <ImageBackground
        style={{ flex: 1 }}
        source={{
          uri: IMAGE_URI,
        }}
      >
        <MessageList />
        <MessageInput />
      </ImageBackground>
    </Channel>
  </ThemeProvider>
);

Custom Background With Selection Screen

Next, add a button that opens a screen where the user selects a background image per channel.

168006715 Acae4b85 00cb 4b45 A127 Aa8f94c13895

168006193 8fd4ad85 7553 4956 A7c6 5d6979e15ee4

Chat screen with customize background buttonWallpaper overview screen with background image options

Store and manage channel preferences

To persist preferences (currently just the background URI), store data using react-native-mmkv, a key-value storage framework. Follow the installation steps before continuing.

Start by creating ChannelBackgroundView. It renders the background by reading from the key-value store. Store an object to keep it extensible (for example, add dimming or background color later).

import type { ViewProps } from "react-native";
import { useMMKVObject } from "react-native-mmkv";

type ChannelPreferences = {
  imageUri: string;
};

const DEFAULT_BACKGROUND_URI = "https://i.redd.it/3jfjc53fsyb61.jpg";

const ChannelBackgroundView = ({
  channelId,
  ...props
}: {
  channelId: string;
} & ViewProps) => {
  const [channelPreferences] = useMMKVObject<ChannelPreferences>(channelId);
  const uri = channelPreferences?.imageUri || DEFAULT_BACKGROUND_URI;

  return <ImageBackground {...props} source={{ uri }} />;
};

Use it in ChannelScreen: replace the static ImageBackground with ChannelBackgroundView and pass channelId.

const ChannelScreen = ({ channel }) => {
  return (
    <ThemeProvider style={theme}>
      <Channel channel={channel}>
        <ChannelBackgroundView channelId={channel?.id} style={{ flex: 1 }}>
          <MessageList />
          <MessageInput />
        </ChannelBackgroundView>
      </Channel>
    </ThemeProvider>
  );
};

Wallpaper overview screen

Add a screen where the user selects a wallpaper from a predefined list.

import { StackNavigationProp } from "@react-navigation/stack";
import { RouteProp } from "@react-navigation/native";
import { useMMKVObject } from "react-native-mmkv";
import { View, SafeAreaView, Pressable, Image, StyleSheet } from "react-native";

const WallpaperOverviewScreen = ({
  navigation: { navigate },
  route: {
    params: { channelId },
  },
}: WallpaperOverviewScreenProps) => {
  const [_, setChannelPreferences] =
    useMMKVObject<ChannelPreferences>(channelId);
  return (
    <SafeAreaView
      style={{
        flex: 1,
        justifyContent: "center",
      }}
    >
      <View style={styles.container}>
        {BRIGHT_IMAGES?.map(({ imageUri = "" }, i) => {
          const handleOnPress = () => {
            setChannelPreferences({ imageUri });
            navigate("Channel");
          };
          return (
            <Pressable
              key={i}
              onPress={handleOnPress}
              style={{
                margin: 1,
                width: GRID_ITEM_WIDTH,
              }}
            >
              <Image style={styles.image} source={{ uri: imageUri }} />
            </Pressable>
          );
        })}
      </View>
    </SafeAreaView>
  );
};

type StackNavigatorParamList = {
  WallpaperOverviewScreen: {
    channelId: string;
  };
};

type WallpaperOverviewScreenProps = {
  navigation: StackNavigationProp<
    StackNavigatorParamList,
    "WallpaperOverviewScreen"
  >;
  route: RouteProp<StackNavigatorParamList, "WallpaperOverviewScreen">;
};

type ChannelPreferences = {
  imageUri: string;
};

const GRID_ITEM_WIDTH = "32.7%";

// Some random images that will get you started
const BRIGHT_IMAGES = [
  "https://images.unsplash.com/photo-1549125764-91425ca48850?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8NjF8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1549241520-425e3dfc01cb?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8ODB8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1554226321-24fdcddd5a55?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MjE5fHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1550006490-9f0656b79e9d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8ODl8fHxlbnwwfHx8fA%3D%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1551506448-074afa034c05?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MTEzfHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1553114835-6f7674d3c2c0?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MTMyfHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1553075712-453f7213c24f?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MTMzfHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1551917951-148edcd8ea8d?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MTU3fHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1553969923-bbf0cac2666b?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MjA3fHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1553194642-29b272a173b9?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MTcwfHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1553356084-58ef4a67b2a7?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MTcxfHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
  "https://images.unsplash.com/photo-1553526777-5ffa3b3248d8?ixlib=rb-1.2.1&ixid=MnwxMjA3fDB8MHxleHBsb3JlLWZlZWR8MTk4fHx8ZW58MHx8fHw%3D&auto=format&fit=crop&w=800&q=60",
].map((imageUri) => ({
  imageUri,
}));

const styles = StyleSheet.create({
  container: {
    flexDirection: "row",
    flex: 1,
    alignContent: "stretch",
    flexWrap: "wrap",
    padding: 6,
  },
  image: {
    flex: 1,
    width: "100%",
  },
});

Be aware of the fact that channel preferences were implemented with MMKV, a key-value storage framework. There are alternative approaches to achieving the same goal, such as saving the channel preferences as custom data on Stream's channel object.

Add a configuration button

We will now add a button that will take the user from the Channel screen to our new WallpaperOverview screen.

import { useNavigation } from "@react-navigation/native";
import {
  Channel,
  MessageInput,
  MessageList,
  ThemeProvider,
} from "stream-chat-react-native";
import { Pressable, Text, StyleSheet } from "react-native";

const ChannelScreen = ({ channel }) => {
  const { navigate } = useNavigation();
  const handleMenuOnPress = () =>
    navigate("WallpaperOverviewScreen", { channelId: channel?.id });

  return (
    <ThemeProvider style={theme}>
      <Channel channel={channel}>
        <ChannelBackgroundView channelId={channel?.id} style={{ flex: 1 }}>
          <Pressable style={styles.menuButton} onPress={handleMenuOnPress}>
            <Text>🎨</Text>
          </Pressable>
          <MessageList />
          <MessageInput />
        </ChannelBackgroundView>
      </Channel>
    </ThemeProvider>
  );
};

const styles = StyleSheet.create({
  menuButton: {
    position: "absolute",
    right: 0,
    top: 0,
    backgroundColor: "rgba(255,87,56,0.65)",
    borderRadius: 36,
    padding: 16,
    margin: 16,
    alignItems: "center",
    zIndex: 10,
  },
});

export const theme = {
  messageList: {
    container: {
      backgroundColor: "transparent",
    },
  },
};

Optional: Connect all screens by navigation

If applicable to your use case, add our screens to a Navigation Stack by doing the following:

import { createNativeStackNavigator } from "react-native-screens/native-stack";
import { NavigationContainer } from "@react-navigation/native";

const Stack = createNativeStackNavigator();

export default () => {
  return (
    <SafeAreaProvider>
      <ThemeProvider style={theme}>
        <NavigationContainer>
          <Stack.Navigator initialRouteName="Channel">
            <Stack.Screen
              component={ChannelScreen}
              name="Channel"
              options={noHeaderOptions}
            />
            <Stack.Screen
              component={WallpaperOverviewScreen}
              name="WallpaperOverviewScreen"
            />
          </Stack.Navigator>
        </NavigationContainer>
      </ThemeProvider>
    </SafeAreaProvider>
  );
};