diff --git a/web/components/examples/live-region-example.tsx b/web/components/examples/live-region-example.tsx
new file mode 100644
index 00000000..68262f97
--- /dev/null
+++ b/web/components/examples/live-region-example.tsx
@@ -0,0 +1,89 @@
+import {useState} from 'react'
+import {useLiveRegion} from 'web/components/live-region'
+
+function LikeButton({profileId: _profileId}: {profileId: string}) {
+ const [liked, setLiked] = useState(false)
+ const {announce} = useLiveRegion()
+
+ const handleLike = () => {
+ const newLiked = !liked
+ setLiked(newLiked)
+
+ if (newLiked) {
+ announce('Profile liked', 'polite')
+ } else {
+ announce('Like removed', 'polite')
+ }
+ }
+
+ return (
+
+ )
+}
+
+function MessageSentNotification() {
+ const {announce} = useLiveRegion()
+
+ const handleSend = () => {
+ announce('Message sent successfully', 'polite')
+ }
+
+ return
+}
+
+function ErrorAlert({message}: {message: string}) {
+ const {announce} = useLiveRegion()
+
+ const handleRetry = () => {
+ announce('Retrying connection...', 'polite')
+ }
+
+ return (
+
+
Error: {message}
+
+
+ )
+}
+
+function FormSubmission() {
+ const [status, setStatus] = useState<'idle' | 'submitting' | 'success' | 'error'>('idle')
+ const {announce} = useLiveRegion()
+
+ const handleSubmit = async () => {
+ setStatus('submitting')
+ announce('Submitting form, please wait', 'polite')
+
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 1000))
+ setStatus('success')
+ announce('Form submitted successfully', 'polite')
+ } catch {
+ setStatus('error')
+ announce('Form submission failed. Please try again.', 'assertive')
+ }
+ }
+
+ return (
+
+ )
+}
+
+function LoadingIndicator({isLoading}: {isLoading: boolean}) {
+ const {announce} = useLiveRegion()
+
+ if (isLoading) {
+ announce('Loading more results', 'polite')
+ }
+
+ return isLoading ? Loading... : null
+}
+
+export {ErrorAlert, FormSubmission, LikeButton, LoadingIndicator, MessageSentNotification}
diff --git a/web/components/live-region.tsx b/web/components/live-region.tsx
new file mode 100644
index 00000000..537e26e3
--- /dev/null
+++ b/web/components/live-region.tsx
@@ -0,0 +1,38 @@
+import {createContext, useCallback, useContext, useRef} from 'react'
+
+interface LiveRegionContextValue {
+ announce: (message: string, priority?: 'polite' | 'assertive') => void
+}
+
+const LiveRegionContext = createContext(null)
+
+export function useLiveRegion() {
+ const context = useContext(LiveRegionContext)
+ if (!context) {
+ return {announce: () => {}}
+ }
+ return context
+}
+
+export function LiveRegionProvider({children}: {children: React.ReactNode}) {
+ const politeRef = useRef(null)
+ const assertiveRef = useRef(null)
+
+ const announce = useCallback((message: string, priority: 'polite' | 'assertive' = 'polite') => {
+ const element = priority === 'assertive' ? assertiveRef.current : politeRef.current
+ if (element) {
+ element.textContent = ''
+ setTimeout(() => {
+ if (element) element.textContent = message
+ }, 50)
+ }
+ }, [])
+
+ return (
+
+ {children}
+
+
+
+ )
+}
diff --git a/web/pages/_app.tsx b/web/pages/_app.tsx
index 7ee606ef..177ff5ac 100644
--- a/web/pages/_app.tsx
+++ b/web/pages/_app.tsx
@@ -16,6 +16,8 @@ import posthog from 'posthog-js'
import {PostHogProvider} from 'posthog-js/react'
import {useEffect, useState} from 'react'
import {AuthProvider, AuthUser} from 'web/components/auth-context'
+import {ErrorBoundary} from 'web/components/error-boundary'
+import {LiveRegionProvider} from 'web/components/live-region'
import {useFontPreferenceManager} from 'web/hooks/use-font-preference'
import {useHasLoaded} from 'web/hooks/use-has-loaded'
import {HiddenProfilesProvider} from 'web/hooks/use-hidden-profiles'
@@ -181,27 +183,31 @@ function MyApp(props: AppProps) {
/>
-
-
-
-
-
-
-
-
-
-
- {/* Workaround for https://github.com/tailwindlabs/headlessui/discussions/666, to allow font CSS variable */}
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {/* Workaround for https://github.com/tailwindlabs/headlessui/discussions/666, to allow font CSS variable */}
+
-
+
>
)