ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto

replace excessive toasts with aria-live announcer

Remove redundant success/info toasts (logout, upload loaded, no results).
Keep only error toasts for critical feedback.
Add AriaLiveAnnouncer component for screen reader accessibility.

byarielm.fyi cc586d28 ebb1e05c

verified
Changed files
+68 -19
src
components
+23 -19
src/App.tsx
··· 9 9 import Firefly from "./components/Firefly"; 10 10 import NotificationContainer from "./components/common/NotificationContainer"; 11 11 import ErrorBoundary from "./components/common/ErrorBoundary"; 12 + import AriaLiveAnnouncer from "./components/common/AriaLiveAnnouncer"; 12 13 import { SearchResultSkeleton } from "./components/common/LoadingSkeleton"; 13 14 import { DEFAULT_SETTINGS } from "./types/settings"; 14 15 import type { UserSettings, SearchResult } from "./types"; ··· 48 49 logout, 49 50 } = useAuth(); 50 51 51 - // Notifications hook (replaces alerts) 52 - const { notifications, removeNotification, success, error, info } = 53 - useNotifications(); 52 + // Notifications hook (only for errors now) 53 + const { notifications, removeNotification, error } = useNotifications(); 54 + 55 + // Aria-live announcements for non-error feedback (invisible, screen-reader only) 56 + const [ariaAnnouncement, setAriaAnnouncement] = useState(""); 54 57 55 58 // Theme hook 56 59 const { isDark, reducedMotion, toggleTheme, toggleMotion } = useTheme(); ··· 169 172 setSearchResults([]); 170 173 setCurrentPlatform("tiktok"); 171 174 setCurrentStep("home"); 172 - info("No previous results found."); 175 + // No visual feedback needed - empty state will show in UI 176 + setAriaAnnouncement("No previous results found."); 173 177 return; 174 178 } 175 179 ··· 194 198 195 199 setSearchResults(loadedResults); 196 200 setCurrentStep("results"); 197 - success(`Loaded ${loadedResults.length} results from previous upload`); 201 + // Announce to screen readers only - visual feedback is navigation to results page 202 + setAriaAnnouncement( 203 + `Loaded ${loadedResults.length} results from previous upload`, 204 + ); 198 205 } catch (err) { 199 206 console.error("Failed to load upload:", err); 200 207 error("Failed to load previous upload. Please try again."); 201 208 setCurrentStep("home"); 202 209 } 203 210 }, 204 - [setStatusMessage, setCurrentStep, setSearchResults, info, error, success], 211 + [setStatusMessage, setCurrentStep, setSearchResults, setAriaAnnouncement, error], 205 212 ); 206 213 207 214 // Login handler ··· 231 238 setSearchResults([]); 232 239 setCurrentPlatform("tiktok"); 233 240 setSavedUploads(new Set()); 234 - success("Logged out successfully"); 241 + // No visual feedback needed - user sees login page 242 + setAriaAnnouncement("Logged out successfully"); 235 243 } catch (err) { 236 244 error("Failed to logout. Please try again."); 237 245 } 238 - }, [logout, setSearchResults, success, error]); 246 + }, [logout, setSearchResults, setAriaAnnouncement, error]); 239 247 240 248 return ( 241 249 <ErrorBoundary> 242 250 <div className="min-h-screen relative overflow-hidden"> 243 - {/* Notification Container */} 251 + {/* Notification Container - errors only */} 244 252 <NotificationContainer 245 253 notifications={notifications} 246 254 onRemove={removeNotification} 247 255 /> 256 + 257 + {/* Invisible announcer for screen readers - non-error feedback */} 258 + <AriaLiveAnnouncer message={ariaAnnouncement} politeness="polite" /> 259 + 260 + {/* Status message for screen readers - loading/progress updates */} 261 + <AriaLiveAnnouncer message={statusMessage} politeness="polite" /> 248 262 249 263 {/* Firefly particles - only render if motion not reduced */} 250 264 {!reducedMotion && ( ··· 258 272 ))} 259 273 </div> 260 274 )} 261 - 262 - {/* Status message for screen readers */} 263 - <div 264 - role="status" 265 - aria-live="polite" 266 - aria-atomic="true" 267 - className="sr-only" 268 - > 269 - {statusMessage} 270 - </div> 271 275 272 276 {/* Skip to main content link */} 273 277 <a
+45
src/components/common/AriaLiveAnnouncer.tsx
··· 1 + import React, { useEffect, useState } from "react"; 2 + 3 + interface AriaLiveAnnouncerProps { 4 + message: string; 5 + politeness?: "polite" | "assertive"; 6 + clearAfter?: number; 7 + } 8 + 9 + /** 10 + * Invisible component that announces messages to screen readers 11 + * without displaying visual toasts 12 + */ 13 + const AriaLiveAnnouncer: React.FC<AriaLiveAnnouncerProps> = ({ 14 + message, 15 + politeness = "polite", 16 + clearAfter = 5000, 17 + }) => { 18 + const [announcement, setAnnouncement] = useState(""); 19 + 20 + useEffect(() => { 21 + if (message) { 22 + // Set the message to trigger screen reader announcement 23 + setAnnouncement(message); 24 + 25 + // Clear after specified time to allow new announcements 26 + if (clearAfter > 0) { 27 + const timer = setTimeout(() => setAnnouncement(""), clearAfter); 28 + return () => clearTimeout(timer); 29 + } 30 + } 31 + }, [message, clearAfter]); 32 + 33 + return ( 34 + <div 35 + role="status" 36 + aria-live={politeness} 37 + aria-atomic="true" 38 + className="sr-only" 39 + > 40 + {announcement} 41 + </div> 42 + ); 43 + }; 44 + 45 + export default AriaLiveAnnouncer;