Implement Interactive Toast Notifications in Vega
In this article, you'll implement an engaging toast notification system that alerts users to available content and allows them to hold the play/pause button to start streaming directly. This solution demonstrates how to modify the Vega Video Sample App with a TV-optimized interactive toast that promotes in-app content discovery without disrupting the viewing experience.
Prerequisites
- Vega development environment set up
- Vega Video Sample App installed and configured
- Basic knowledge of React Native and TypeScript
Install the required dependency
Add the toast message library to your project.
npm install react-native-toast-message@^2.2.0
Create the custom toast component
Create a new file for the TV-optimized toast component that displays a progress bar during user interaction.
Create src/components/ProgressToast.tsx with the following implementation.
import React from 'react';
import { View, Text, StyleSheet, Animated } from 'react-native';
import { BaseToast, ToastProps } from 'react-native-toast-message';
interface ProgressToastProps extends ToastProps {
props?: {
progress?: number;
isHolding?: boolean;
};
}
export const ProgressToast: React.FC<ProgressToastProps> = ({ props, text1, text2, ...rest }) => {
const progress = props?.progress ?? 0;
const isHolding = props?.isHolding ?? false;
return (
<View style={styles.toastContainer}>
<Text style={styles.instructionText}>
{text1 || 'Hold ⏯️ to start...'}
</Text>
{text2 && (
<Text style={styles.secondaryText}>
{text2}
</Text>
)}
<View style={styles.progressBackground}>
<Animated.View
style={[
styles.progressFill,
{
width: `${progress * 100}%`,
backgroundColor: isHolding ? '#00ff00' : '#007bff',
}
]}
/>
</View>
</View>
);
};
const styles = StyleSheet.create({
toastContainer: {
position: 'absolute',
top: '20%',
right: '2%',
width: 480,
backgroundColor: 'rgba(26, 26, 26, 0.95)',
borderRadius: 8,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 2, height: 2 },
shadowOpacity: 0.8,
shadowRadius: 6,
elevation: 10,
borderWidth: 1,
borderColor: 'rgba(0, 123, 255, 0.3)',
},
instructionText: {
fontSize: 22,
fontWeight: '600',
color: '#ffffff',
textAlign: 'center',
marginBottom: 8,
textShadowColor: 'rgba(0, 0, 0, 0.8)',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 2,
},
secondaryText: {
fontSize: 20,
fontWeight: '500',
color: '#e0e0e0',
textAlign: 'center',
marginBottom: 12,
textShadowColor: 'rgba(0, 0, 0, 0.8)',
textShadowOffset: { width: 1, height: 1 },
textShadowRadius: 2,
},
progressBackground: {
width: '100%',
height: 12,
backgroundColor: '#333333',
borderRadius: 6,
overflow: 'hidden',
marginTop: 4,
},
progressFill: {
height: '100%',
borderRadius: 6,
},
});
export const toastConfig = {
progress: (props: ProgressToastProps) => <ProgressToast {...props} />,
};
This component creates a rectangular toast positioned on the right side of the screen with real-time progress feedback. The progress bar changes from blue to green when the user actively holds the button.
Create the long press hook
Create a custom hook that manages the complete lifecycle of play/pause button interactions and toast visibility.
Create src/hooks/useLongPressToast.tsx with the following implementation.
import { useState, useRef, useCallback, useEffect } from 'react';
import { useTVEventHandler } from '@amazon-devices/react-native-kepler';
import Toast from 'react-native-toast-message';
export type TVButtonType = 'playpause' | 'select' | 'menu' | 'back' | 'up' | 'down' | 'left' | 'right';
interface UseLongPressToastProps {
onLongPressComplete: () => void;
targetButton?: TVButtonType;
autoShowDelay?: number;
longPressDuration?: number;
toastTimeout?: number;
initialText?: string;
holdingText?: string;
secondaryText?: string;
completionText?: string;
isScreenFocused?: boolean;
}
interface LongPressState {
isHolding: boolean;
progress: number;
toastVisible: boolean;
hasAutoShown: boolean;
hasCompleted: boolean;
}
export const useLongPressToast = ({
onLongPressComplete,
targetButton = 'playpause',
autoShowDelay = 5000,
longPressDuration = 2000,
toastTimeout = 5000,
initialText = 'Hold Button to Continue',
holdingText = 'Keep Holding...',
secondaryText,
completionText = 'Action Completed',
isScreenFocused = true
}: UseLongPressToastProps) => {
const [state, setState] = useState<LongPressState>({
isHolding: false,
progress: 0,
toastVisible: false,
hasAutoShown: false,
hasCompleted: false,
});
const progressIntervalRef = useRef<NodeJS.Timeout | null>(null);
const autoShowTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const toastTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const holdStartTimeRef = useRef<number>(0);
const isProgressCompleteRef = useRef<boolean>(false);
const toastVisibleRef = useRef<boolean>(false);
const showInitialToast = useCallback(() => {
if (state.toastVisible || state.hasAutoShown || state.hasCompleted) return;
Toast.show({
type: 'progress',
text1: initialText,
text2: secondaryText,
visibilityTime: toastTimeout,
autoHide: false,
props: {
progress: 0,
isHolding: false,
},
});
setState(prev => ({
...prev,
toastVisible: true,
hasAutoShown: true,
}));
toastVisibleRef.current = true;
toastTimeoutRef.current = setTimeout(() => {
hideToast();
}, toastTimeout);
}, [state.toastVisible, state.hasAutoShown, state.hasCompleted, toastTimeout, initialText, secondaryText]);
const hideToast = useCallback(() => {
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
toastTimeoutRef.current = null;
}
Toast.hide();
setState(prev => ({
...prev,
toastVisible: false,
}));
toastVisibleRef.current = false;
}, []);
const startProgress = useCallback(() => {
if (state.isHolding) return;
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
toastTimeoutRef.current = null;
}
holdStartTimeRef.current = Date.now();
isProgressCompleteRef.current = false;
setState(prev => ({
...prev,
isHolding: true,
progress: 0,
toastVisible: true,
}));
toastVisibleRef.current = true;
progressIntervalRef.current = setInterval(() => {
const elapsed = Date.now() - holdStartTimeRef.current;
const newProgress = Math.min(elapsed / longPressDuration, 1);
setState(prev => ({
...prev,
progress: newProgress,
}));
Toast.show({
type: 'progress',
text1: holdingText,
text2: secondaryText,
autoHide: false,
props: {
progress: newProgress,
isHolding: true,
},
});
if (newProgress >= 1 && !isProgressCompleteRef.current) {
isProgressCompleteRef.current = true;
completeProgress();
}
}, 50);
}, [state.isHolding, longPressDuration, holdingText, secondaryText]);
const stopProgress = useCallback(() => {
if (!state.isHolding) return;
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
progressIntervalRef.current = null;
}
if (!isProgressCompleteRef.current) {
hideToast();
}
setState(prev => ({
...prev,
isHolding: false,
progress: 0,
}));
}, [state.isHolding, hideToast]);
const completeProgress = useCallback(() => {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
progressIntervalRef.current = null;
}
if (completionText) {
Toast.show({
type: 'progress',
text1: completionText,
visibilityTime: 2000,
props: {
progress: 1,
isHolding: false,
},
});
}
setState(prev => ({
...prev,
isHolding: false,
progress: 1,
toastVisible: false,
hasCompleted: true,
}));
toastVisibleRef.current = false;
setTimeout(() => {
onLongPressComplete();
}, 500);
}, [onLongPressComplete, completionText]);
const handleTVEvent = useCallback((evt: any) => {
if (evt.eventType !== targetButton || !toastVisibleRef.current) {
return;
}
if (evt.eventKeyAction === 0) {
startProgress();
} else if (evt.eventKeyAction === 1) {
stopProgress();
}
}, [startProgress, stopProgress, targetButton]);
useTVEventHandler(handleTVEvent);
useEffect(() => {
if (!isScreenFocused) {
return;
}
setState(prev => ({ ...prev, hasAutoShown: false }));
autoShowTimeoutRef.current = setTimeout(() => {
showInitialToast();
}, autoShowDelay);
return () => {
if (autoShowTimeoutRef.current) {
clearTimeout(autoShowTimeoutRef.current);
}
};
}, [autoShowDelay, showInitialToast, isScreenFocused]);
useEffect(() => {
return () => {
if (progressIntervalRef.current) {
clearInterval(progressIntervalRef.current);
}
if (autoShowTimeoutRef.current) {
clearTimeout(autoShowTimeoutRef.current);
}
if (toastTimeoutRef.current) {
clearTimeout(toastTimeoutRef.current);
}
};
}, []);
return {
isHolding: state.isHolding,
progress: state.progress,
toastVisible: state.toastVisible,
hasAutoShown: state.hasAutoShown,
showInitialToast,
hideToast,
};
};
This hook manages automatic toast display timing, smooth progress animations, completion detection with callbacks, and proper cleanup of all timers and intervals using Vega's TVEventHandler.
Register the toast component
Update your main App component to register the custom toast configuration.
Add the following imports to src/App.tsx.
import Toast from 'react-native-toast-message';
import { toastConfig } from './components/ProgressToast';
Add the Toast component inside your App return statement.
const App = () => {
return (
<Provider store={store}>
<ThemeProvider theme={theme}>
<NavigationContainer>
<AppStack />
</NavigationContainer>
<Toast config={toastConfig} />
</ThemeProvider>
</Provider>
);
};
Integrate the toast in your screen
To add the long press toast hook to your HomeScreen component with screen focus tracking, add the following imports to src/screens/HomeScreen.tsx.
import { useLongPressToast } from '../hooks/useLongPressToast';
import { useIsFocused } from '@react-navigation/native';
Add the hook inside your HomeScreen component.
// Assumes tileData and Screens are defined in your app
const HomeScreen = ({ navigation }: AppStackScreenProps<Screens.HOME_SCREEN>) => {
const isFocused = useIsFocused();
const navigateToStream = useCallback(() => {
const params = {
data: tileData,
sendDataOnBack: () => {},
};
try {
navigation.navigate(Screens.PLAYER_SCREEN, params);
} catch (error) {
console.error('Failed to navigate to stream:', error);
navigation.goBack();
}
}, [navigation]);
useLongPressToast({
onLongPressComplete: navigateToStream,
initialText: tileData.title + ' starting soon!',
holdingText: 'Keep Holding...',
secondaryText: 'Hold ⏯️ to start',
completionText: 'Starting stream',
isScreenFocused: isFocused,
});
// ... rest of existing HomeScreen code
};
The isScreenFocused parameter makes sure the toast only appears when the HomeScreen is visible, preventing it from showing on other screens.
Test the implementation
Build and run your app on a Fire TV device or emulator:
- Navigate to the Home Screen.
- Wait five seconds for the toast to appear automatically.
- Press and hold the play/pause button on your remote.
- Observe the progress bar filling over two seconds.
- Release early to cancel, or hold for the full duration to trigger navigation.
Related content
Last updated: Mar 10, 2026

