import {
OverlayProvider,
Chat,
Channel,
MessageList,
MessageInput,
} from "stream-chat-react-native";
const ChannelScreen = () => {
return (
<OverlayProvider>
<Chat client={client}>
<Channel channel={channel}>
<ChannelHeader />
<MessageList />
<MessageInput />
</Channel>
</Chat>
</OverlayProvider>
);
};Custom Poll Flow
This cookbook shows how to customize a Channel screen with the Poll component and use navigation instead of modals.
Best Practices
- Enable polls at the dashboard level before debugging UI behavior.
- Keep poll screens inside
Channelso they inherit the correct contexts. - Reuse default poll subcomponents and override only the pieces you need.
- Route poll navigation explicitly to avoid mixing modal and stack flows.
- Handle poll state updates through provided hooks instead of manual mutations.
Prerequisites
A Channel screen with a MessageList is required. Polls are tied to messages, so this must be in place first.
Polls are disabled by default. Enable them in the Stream Dashboard by toggling the Poll button.

This adds the Poll button to your attachment picker:

You also need a working chatClient. Examples use React Navigation, but any navigation library works.
The Initial UI
Without any customization, you should have something similar to the following:
The poll creation flow works out of the box with the default UI:



Customizing Poll Content
Customize the poll UI inside messages by setting PollContent on Channel. Start by removing the poll header and adjusting the bottom buttons. Reuse the default PollContent and override only the pieces you need.
import {
OverlayProvider,
Chat,
Channel,
MessageList,
MessageInput,
PollContent,
} from "stream-chat-react-native";
const MyPollButtons = () => {
return (
<>
<ShowAllOptionsButton />
<ViewResultsButton
onPress={({ message, poll }) =>
Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)
}
/>
<EndVoteButton />
</>
);
};
const MyPollContent = () => (
<PollContent PollHeader={() => null} PollButtons={MyPollButtons} />
);
const ChannelScreen = () => {
return (
<OverlayProvider>
<Chat client={client}>
<Channel channel={channel} PollContent={MyPollContent}>
<ChannelHeader />
<MessageList />
<MessageInput />
</Channel>
</Chat>
</OverlayProvider>
);
};

Now only two buttons render (View Results and End Vote). View Results shows an Alert instead of opening the modal.
Navigating to Poll Results
Next, reintroduce the PollResults screen using your own navigation instead of React Native Modals.
Since all Poll screens need to be children of the Channel component, introduce an additional Stack navigator inside Channel:
import {
OverlayProvider,
Chat,
Channel,
MessageList,
MessageInput,
PollContent,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";
const MyPollButtons = () => {
return (
<>
<ShowAllOptionsButton />
<ViewResultsButton
onPress={({ message, poll }) =>
Alert.alert(`Message ID: ${message.id} and Poll ID: ${poll.id}`)
}
/>
<EndVoteButton />
</>
);
};
const MyPollContent = () => (
<PollContent PollHeader={() => null} PollButtons={MyPollButtons} />
);
const ChannelMessageList = () => {
return (
<>
<ChannelHeader />
<MessageList />
<MessageInput />
</>
);
};
const ChannelStack = createStackNavigator<StackNavigatorParamList>();
const ChannelScreen = () => {
return (
<OverlayProvider>
<Chat client={client}>
<Channel channel={channel} PollContent={MyPollContent}>
<ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
<ChannelStack.Screen
name={"ChannelMessageList"}
options={{ headerShown: false }}
component={ChannelMessageList}
/>
<ChannelStack.Screen
name={"PollResultsScreen"}
options={{ headerShown: false }}
component={() => null}
/>
</ChannelStack.Navigator>
</Channel>
</Chat>
</OverlayProvider>
);
};For now, leave PollResultsScreen empty. This stack inside Channel can host all poll screens while keeping channel and other Channel customizations in one place.
Building the Results Screen
Reconstruct the PollResults screen using the PollModalHeader and PollResults components to get the default UI out of the box:
import {
OverlayProvider,
Chat,
Channel,
MessageList,
MessageInput,
PollContent,
PollResults,
PollModalHeader,
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';
const MyPollButtons = () => {
return (
<>
<ShowAllOptionsButton />
<ViewResultsButton
onPress={({ message, poll }) =>
navigation.navigate('PollResultsScreen', {
message,
poll,
});
}
/>
<EndVoteButton />
</>
)
}
// ... rest of the components
const PollResultsScreen = ({
route: {
params: { message, poll },
},
}) => {
const navigation = useNavigation();
return (
<SafeAreaView style={{ flex: 1 }}>
<PollModalHeader title={'RESULTS'} onPress={() => navigation.goBack()} />
<PollResults message={message} poll={poll} />
</SafeAreaView>
);
};
const ChannelScreen = () => {
return (
<OverlayProvider>
<Chat client={client}>
<Channel channel={channel} PollContent={MyPollContent}>
<ChannelStack.Navigator initialRouteName={'ChannelMessageList'}>
<ChannelStack.Screen
name={'ChannelMessageList'}
options={{ headerShown: false }}
component={ChannelMessageList}
/>
<ChannelStack.Screen
name={'PollResultsScreen'}
options={{ headerShown: false }}
component={PollResultsScreen}
/>
</ChannelStack.Navigator>
</Channel>
</Chat>
</OverlayProvider>
);
};View Results now navigates to PollResults in the stack instead of a modal:

The UI matches the default, but the title is now RESULTS.
Pinning the Poll Name as Header
For long option lists, pin the poll name at the top using the usePollState hook and the PollResultsContent component:
import {
// ...rest of the imports
usePollState,
PollResultsContent,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";
// ... rest of the components
const MyPollResultsContent = () => {
const { name } = usePollState();
const navigation = useNavigation();
return (
<>
<PollModalHeader title={name} onPress={() => navigation.goBack()} />
<PollResultsContent />
</>
);
};
const PollResultsScreen = ({
route: {
params: { message, poll },
},
}) => {
return (
<SafeAreaView style={{ flex: 1 }}>
<PollResults
message={message}
poll={poll}
PollResultsContent={MyPollResultsContent}
/>
</SafeAreaView>
);
};
const ChannelScreen = () => {
return (
<OverlayProvider>
<Chat client={client}>
<Channel channel={channel} PollContent={MyPollContent}>
<ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
<ChannelStack.Screen
name={"ChannelMessageList"}
options={{ headerShown: false }}
component={ChannelMessageList}
/>
<ChannelStack.Screen
name={"PollResultsScreen"}
options={{ headerShown: false }}
component={PollResultsScreen}
/>
</ChannelStack.Navigator>
</Channel>
</Chat>
</OverlayProvider>
);
};
This works because PollResults is wrapped in PollContext, and the button callback provides the data needed to initialize the provider.
Custom Poll Creation Screen
Apply the same navigation pattern to poll creation for consistency:
import {
// ...rest of the imports
CreatePoll,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";
// ... rest of the components
const MyCreatePollContent = ({
route: {
params: { sendMessage },
},
}) => {
const navigation = useNavigation();
return (
<SafeAreaView style={{ flex: 1 }}>
<CreatePoll
sendMessage={sendMessage}
closePollCreationDialog={() => navigation.goBack()}
/>
</SafeAreaView>
);
};
const ChannelScreen = () => {
const navigation = useNavigation();
return (
<OverlayProvider>
<Chat client={client}>
<Channel
channel={channel}
PollContent={MyPollContent}
openPollCreationDialog={({ sendMessage }) =>
navigation.navigate("CreatePollScreen", { sendMessage })
}
>
<ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
<ChannelStack.Screen
name={"ChannelMessageList"}
options={{ headerShown: false }}
component={ChannelMessageList}
/>
<ChannelStack.Screen
name={"PollResultsScreen"}
options={{ headerShown: false }}
component={PollResultsScreen}
/>
<ChannelStack.Group screenOptions={{ presentation: "modal" }}>
<ChannelStack.Screen
name={"CreatePollScreen"}
options={{ headerShown: false }}
component={MyCreatePollContent}
/>
</ChannelStack.Group>
</ChannelStack.Navigator>
</Channel>
</Chat>
</OverlayProvider>
);
};
Theming Poll Components
All Poll components are theme-compatible. Change the background color by overriding the default theme:
import {
// ...rest of the imports
ThemeProvider,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";
// ... rest of the components
const myTheme: DeepPartial<Theme> = {
poll: {
message: {
container: {
backgroundColor: "pink",
},
},
},
};
const ChannelScreen = () => {
const navigation = useNavigation();
return (
<ThemeProvider style={myTheme}>
<OverlayProvider>
<Chat client={client}>
<Channel
channel={channel}
PollContent={MyPollContent}
openPollCreationDialog={({ sendMessage }) =>
navigation.navigate("CreatePollScreen", { sendMessage })
}
>
<ChannelStack.Navigator initialRouteName={"ChannelMessageList"}>
<ChannelStack.Screen
name={"ChannelMessageList"}
options={{ headerShown: false }}
component={ChannelMessageList}
/>
<ChannelStack.Screen
name={"PollResultsScreen"}
options={{ headerShown: false }}
component={PollResultsScreen}
/>
<ChannelStack.Group screenOptions={{ presentation: "modal" }}>
<ChannelStack.Screen
name={"CreatePollScreen"}
options={{ headerShown: false }}
component={MyCreatePollContent}
/>
</ChannelStack.Group>
</ChannelStack.Navigator>
</Channel>
</Chat>
</OverlayProvider>
</ThemeProvider>
);
};
Set the theme above your OverlayProvider so that poll customizations are also reflected on the message preview when it is long pressed.
Custom Poll Answers List
If the default PollAnswersList UI doesn't fit your needs, you can replace it.
Adding Comment Buttons
First, add ShowAllCommentsButton and AddCommentButton to your custom buttons:
import {
// ...rest of the imports
ShowAllCommentsButton,
AddCommentButton
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';
// ... rest of the components
const MyPollButtons = () => {
return (
<>
<ShowAllOptionsButton />
<ShowAllCommentsButton />
<AddCommentButton />
<ViewResultsButton
onPress={({ message, poll }) =>
navigation.navigate('PollResultsScreen', {
message,
poll,
});
}
/>
<EndVoteButton />
</>
)
}
// ... the ChannelScreen componentTo create comments, enable answers/comments when creating the poll, then use the Add Comment button:


Navigating to the Answers Screen
Add a dedicated navigation screen for AnswersList, starting with the default UI:
import {
// ...rest of the imports
PollAnswersList,
} from 'stream-chat-react-native';
import { createStackNavigator } from '@react-navigation/stack';
// ... rest of the components
const MyPollButtons = () => {
return (
<>
<ShowAllOptionsButton />
<ShowAllCommentsButton
onPress={({ message, poll }) => {
navigation.navigate('PollAnswersScreen', {
message,
poll,
});
}}
/>
<AddCommentButton />
<ViewResultsButton
onPress={({ message, poll }) =>
navigation.navigate('PollResultsScreen', {
message,
poll,
});
}
/>
<EndVoteButton />
</>
)
}
const PollAnswersScreen = ({
route: {
params: { message, poll },
},
}) => {
const navigation = useNavigation();
return (
<SafeAreaView style={{ flex: 1 }}>
<PollModalHeader title={'All Poll Answers'} onPress={() => navigation.goBack()} />
<PollAnswersList message={message} poll={poll} />
</SafeAreaView>
);
};
const ChannelScreen = () => {
const navigation = useNavigation();
return (
<ThemeProvider style={myTheme}>
<OverlayProvider>
<Chat client={client}>
<Channel
channel={channel}
PollContent={MyPollContent}
openPollCreationDialog={({ sendMessage }) => navigation.navigate('CreatePollScreen', { sendMessage })}
>
<ChannelStack.Navigator initialRouteName={'ChannelMessageList'}>
<ChannelStack.Screen
name={'ChannelMessageList'}
options={{ headerShown: false }}
component={ChannelMessageList}
/>
<ChannelStack.Screen
name={'PollResultsScreen'}
options={{ headerShown: false }}
component={PollResultsScreen}
/>
<ChannelStack.Screen
name={'PollAnswersScreen'}
options={{ headerShown: false }}
component={PollAnswersScreen}
/>
<ChannelStack.Group screenOptions={{ presentation: 'modal' }}>
<ChannelStack.Screen
name={'CreatePollScreen'}
options={{ headerShown: false }}
component={MyCreatePollContent}
/>
</ChannelStack.Group>
</ChannelStack.Navigator>
</Channel>
</Chat>
</OverlayProvider>
</ThemeProvider>
);
};This allows navigation to the default PollAnswersList UI:

Overriding the Answers List Content
Override PollAnswersListContent to fully customize the UI. Since the list of answers can be large, use the usePollAnswersPagination hook for pagination:
import {
// ...rest of the imports
usePollAnswersPagination,
} from "stream-chat-react-native";
import { createStackNavigator } from "@react-navigation/stack";
// ... rest of the components
const LoadingIndicator = () => {
/* some LoadingIndicator logic here */
};
const MyItem = ({ item }) => {
const { answer_text, user } = item;
return (
<Text>
{user.name} commented: {answer_text}
</Text>
);
};
const MyPollAnswersContent = () => {
const { pollAnswers, loading, loadMore } = usePollAnswersPagination();
return (
<FlatList
contentContainerStyle={{ flex: 1, padding: 16 }}
data={pollAnswers}
renderItem={MyItem}
onEndReached={loadMore}
ListFooterComponent={loading ? <LoadingIndicator /> : null}
/>
);
};
const PollAnswersScreen = ({
route: {
params: { message, poll },
},
}) => {
const navigation = useNavigation();
return (
<SafeAreaView style={{ flex: 1 }}>
<PollModalHeader
title={"All Poll Answers"}
onPress={() => navigation.goBack()}
/>
<PollAnswersList
message={message}
poll={poll}
PollAnswersListContent={MyPollAnswersContent}
/>
</SafeAreaView>
);
};
// ... the Channel screen
The list supports pagination on scroll and displays a loading indicator when fetching more answers.
Reusable Components
Any components from the default UI can be imported from the SDK. These include:
- All buttons mentioned here
CreatePollContentPollContentPollButtonsPollHeaderPollModalHeaderPollInputDialogCreatePollIconPollOptionPollResultsContentPollResultsItemPollVotePollAllOptionsContent