Interactive Toast Implementation

Vega provides a built-in useToastKepler hook for displaying simple on-screen notifications with a title, description, and optional icon.

However, if you need a fully custom toast UI with features like progress bars and hold-to-start interactions, this guide shows how to build one using react-native-toast-message.

TV applications face unique challenges in promoting in-app content discovery, where static banners are often ignored and traditional notifications can disrupt the viewing experience. This guide demonstrates how to implement an engaging toast message that alerts users to available content and allows them to hold the play/pause button to directly start streaming. The example here focuses on the technical implementation for testing and illustration purposes — in a production environment, you’d integrate this component with a backend service that triggers toast messages based on user preferences and viewing patterns.

This solution demonstrates how to modify the Vega Video Sample App, our reference implementation for video streaming applications, with “Beautiful Whale Tail Uvita Costa Rica” video as our example content.

VegaToast

Prerequisites

// Required dependencies in package.json
{
  "dependencies": {
    "react-native-toast-message": "^2.2.0",
  }
}

Step 1: Custom Toast Component

A custom React component that implements a TV-optimized interactive toast notification. Features a clean rectangular design positioned for optimal visibility with real-time progress feedback during user interaction.

File: src/components/ProgressToast.tsx

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,
  },
});
// Register this config in App.tsx: <Toast config={toastConfig} />
export const toastConfig = {
  progress: (props: ProgressToastProps) => <ProgressToast {...props} />,
};

Step 2: Business Logic

A custom hook that manages the complete lifecycle of play/pause button interactions and toast visibility. The hook handles all aspects of this interaction, including automatic toast display timing, smooth progress animations, completion detection with callbacks, and proper cleanup of all timers and intervals. Using Vega’s TVEventHandler, the hook provides reliable remote control event management and precise long press detection for the toast’s hold-to-start interaction.

File: src/hooks/useLongPressToast.tsx

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); // ref avoids stale closure in TV event handler
  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); // 50ms interval for smooth animation
  }, [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, secondaryText]);
  const handleTVEvent = useCallback((evt: any) => {
    if (evt.eventType !== targetButton || !toastVisibleRef.current) return;
    if (evt.eventKeyAction === 0) startProgress();      // button down
    else if (evt.eventKeyAction === 1) stopProgress();   // button up
  }, [startProgress, stopProgress, targetButton]);
  useTVEventHandler(handleTVEvent);
  // Auto-show toast after delay when screen is focused
  useEffect(() => {
    if (!isScreenFocused) return;
    setState(prev => ({ ...prev, hasAutoShown: false }));
    autoShowTimeoutRef.current = setTimeout(() => showInitialToast(), autoShowDelay);
    return () => {
      if (autoShowTimeoutRef.current) clearTimeout(autoShowTimeoutRef.current);
    };
  }, [autoShowDelay, showInitialToast, isScreenFocused]);
  // Cleanup all timers on unmount
  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,
  };
};

Step 3: App/HomeScreen Integration

File: src/App.tsx

App.tsx: Added one line to register our custom toast component so the app knows to use our ProgressToast design instead of the default toast styling.

// Add to existing App.tsx imports
import Toast from 'react-native-toast-message';
import { toastConfig } from './src/components/ProgressToast';
const App = () => {
  return (
    <Provider store={store}>
      <ThemeProvider theme={theme}>
        <NavigationContainer>
          <AppStack />
        </NavigationContainer>
        {/* Toast provider with custom progress toast configuration */}
        <Toast config={toastConfig} />
      </ThemeProvider>
    </Provider>
  );
};
export default App;

File: src/screens/HomeScreen.tsx (Integration Example)

Please modify logic for your specific implementation for surfacing Toast message

HomeScreen.tsx: Added the useLongPressToast hook with isScreenFocused: isFocused to ensure the toast only appears when the HomeScreen is actually visible, preventing it from showing up on other screens like the video player.

import { useLongPressToast } from '../hooks/useLongPressToast';
const HomeScreen = ({ navigation }: AppStackScreenProps<Screens.HOME_SCREEN>) => {
  const isFocused = useIsFocused();
  // ... existing HomeScreen code ...
  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]);
  // Only activate when HomeScreen is focused to prevent background toasts
  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 ...
};

Whether implementing this for live events, premium content, or personalized recommendations, this solution offers an approach to capturing viewer attention and driving engagement in your application. The code provided serves as a foundation that can be built upon to create engaging, user-friendly TV experiences that drive content discovery and user satisfaction.

For a production implementation, consider adding user opt-in mechanisms for specific content types (such as favorite sports teams or TV shows), smart notification scheduling that respects viewing sessions, and rate limiting to prevent notification fatigue. Avoid interrupting active viewing and prioritize notifications based on user interests and viewing history.

Related resources

Last updated: Mar 11, 2026