Skip to content

Instantly share code, notes, and snippets.

@techtony92
Forked from JoeEverest/Toast.tsx
Created March 9, 2026 00:02
Show Gist options
  • Select an option

  • Save techtony92/f2324d5b573c9d7080160505907cc8a1 to your computer and use it in GitHub Desktop.

Select an option

Save techtony92/f2324d5b573c9d7080160505907cc8a1 to your computer and use it in GitHub Desktop.
React Native Toast component
import React, { createContext, useContext, useRef, useState } from "react";
import { View, StyleSheet, Animated, Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { Text } from "./Text";
import { Check, Info, AlertCircle } from "lucide-react-native";
type ToastType = "success" | "error" | "info" | "subtle";
type ToastConfig = {
message: string;
type: ToastType;
duration?: number;
};
type ToastContextType = {
showToast: (config: ToastConfig) => void;
};
// Create a singleton to manage toast outside of React components
type ToastServiceType = {
showToast: ((config: ToastConfig) => void) | null;
};
// Toast service for static access
export const ToastService: ToastServiceType = {
showToast: null,
};
const ToastContext = createContext<ToastContextType | null>(null);
export const useToast = () => {
const context = useContext(ToastContext);
if (!context) {
throw new Error("useToast must be used within a ToastProvider");
}
return context;
};
type ToastProviderProps = {
children: React.ReactNode;
};
export function ToastProvider({ children }: ToastProviderProps) {
const [toastConfig, setToastConfig] = useState<ToastConfig | null>(null);
const fadeAnim = useRef(new Animated.Value(0)).current;
const translateY = useRef(new Animated.Value(20)).current;
const timeoutRef = useRef<NodeJS.Timeout>();
const insets = useSafeAreaInsets();
const showToast = (config: ToastConfig) => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
setToastConfig(config);
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 1,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(translateY, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
]).start();
timeoutRef.current = setTimeout(() => {
Animated.parallel([
Animated.timing(fadeAnim, {
toValue: 0,
duration: 200,
useNativeDriver: true,
}),
Animated.timing(translateY, {
toValue: 20,
duration: 200,
useNativeDriver: true,
}),
]).start(() => setToastConfig(null));
}, config.duration || 3000);
};
// Register the showToast method with the ToastService
React.useEffect(() => {
ToastService.showToast = showToast;
return () => {
ToastService.showToast = null;
};
}, []);
const getToastStyle = (type: ToastType) => {
switch (type) {
case "success":
return styles.success;
case "error":
return styles.error;
case "info":
return styles.info;
case "subtle":
return styles.subtle;
default:
return styles.subtle;
}
};
const getToastIcon = (type: ToastType) => {
switch (type) {
case "success":
return <Check size={20} color={"#059669"} />;
case "error":
return <AlertCircle size={20} color={"#DC2626"} />;
case "info":
return <Info size={20} color={"#0B6BCF"} />;
default:
return null;
}
};
return (
<ToastContext.Provider value={{ showToast }}>
{children}
{toastConfig && (
<Animated.View
style={[
styles.container,
getToastStyle(toastConfig.type),
{ bottom: insets.bottom + 16 },
{ opacity: fadeAnim, transform: [{ translateY }] },
]}
>
{toastConfig.type !== "subtle" && (
<View style={styles.iconContainer}>{getToastIcon(toastConfig.type)}</View>
)}
<Text style={[styles.message, toastConfig.type === "subtle" && styles.subtleMessage]}>
{toastConfig.message}
</Text>
</Animated.View>
)}
</ToastContext.Provider>
);
}
const styles = StyleSheet.create({
container: {
position: "absolute",
left: 16,
right: 16,
paddingHorizontal: 16,
paddingVertical: 12,
borderRadius: 8,
flexDirection: "row",
alignItems: "center",
...Platform.select({
ios: {
shadowColor: "#111827",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
},
android: {
elevation: 4,
},
}),
},
success: {
backgroundColor: "#ECFDF5",
borderColor: "#D1FAE5",
borderWidth: 1,
},
error: {
backgroundColor: "#FEF2F2",
borderColor: "#D1FAE5",
borderWidth: 1,
},
info: {
backgroundColor: "#EFF8FF",
borderColor: "#D1E9FF",
borderWidth: 1,
},
subtle: {
backgroundColor: "#111827",
},
iconContainer: {
marginRight: 8,
},
message: {
flex: 1,
fontSize: 15,
color: "#111827",
},
subtleMessage: {
color: "#FFFFFF",
textAlign: "center",
},
});
// Component with static methods that use the ToastService
export const Toast = {
success: (message: string, duration?: number) => {
if (ToastService.showToast) {
ToastService.showToast({ message, type: "success", duration });
} else {
console.warn("Toast service not initialized. Make sure ToastProvider is mounted.");
}
},
error: (message: string, duration?: number) => {
if (ToastService.showToast) {
ToastService.showToast({ message, type: "error", duration });
} else {
console.warn("Toast service not initialized. Make sure ToastProvider is mounted.");
}
},
info: (message: string, duration?: number) => {
if (ToastService.showToast) {
ToastService.showToast({ message, type: "info", duration });
} else {
console.warn("Toast service not initialized. Make sure ToastProvider is mounted.");
}
},
subtle: (message: string, duration?: number) => {
if (ToastService.showToast) {
ToastService.showToast({ message, type: "subtle", duration });
} else {
console.warn("Toast service not initialized. Make sure ToastProvider is mounted.");
}
},
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment