Session Timeout
Non-dismissible full-screen overlay for auth session expiry. Renders at portal root, blurs app content beneath, and redirects to login with the current path preserved as a return URL.
Basic
Triggers the session timeout overlay. Click "Sign in" or "Return home" to dismiss the demo.
Redirecting State
When onSignIn returns a Promise, the overlay automatically enters the redirecting state — the CTA shows a spinner, the label changes to "Signing in…", and both buttons are disabled until the Promise resolves.
Without Return Home
Omit onReturnHome to hide the ghost escape hatch. The user can only sign in.
Installation
npx visor add session-timeoutThis copies two files into your project:
components/ui/session-timeout/session-timeout.tsx— the componentcomponents/ui/session-timeout/session-timeout.module.css— the styles
Usage
Mount SessionTimeout near the top of your app tree (e.g., in the root layout or a shared auth provider). Drive open from your auth event listener and call your login redirect inside onSignIn.
import { SessionTimeout } from '@/components/ui/session-timeout/session-timeout';
import { useRouter, usePathname } from 'next/navigation';
export function SessionGuard({ isExpired }: { isExpired: boolean }) {
const router = useRouter();
const pathname = usePathname();
async function handleSignIn() {
const redirect = encodeURIComponent(pathname);
router.push(`/login?redirect=${redirect}`);
}
return (
<SessionTimeout
open={isExpired}
onSignIn={handleSignIn}
onReturnHome={() => router.push('/')}
/>
);
}With Supabase
import { useEffect, useState } from 'react';
import { createClient } from '@/lib/supabase/client';
import { SessionTimeout } from '@/components/ui/session-timeout/session-timeout';
import { useRouter, usePathname } from 'next/navigation';
export function SessionGuard({ children }: { children: React.ReactNode }) {
const [expired, setExpired] = useState(false);
const router = useRouter();
const pathname = usePathname();
const supabase = createClient();
useEffect(() => {
const { data: { subscription } } = supabase.auth.onAuthStateChange(
(event) => {
if (event === 'SIGNED_OUT') setExpired(true);
if (event === 'SIGNED_IN') setExpired(false);
}
);
return () => subscription.unsubscribe();
}, [supabase]);
async function handleSignIn() {
const redirect = encodeURIComponent(pathname);
router.push(`/login?redirect=${redirect}`);
}
return (
<>
{children}
<SessionTimeout
open={expired}
onSignIn={handleSignIn}
onReturnHome={() => router.push('/')}
/>
</>
);
}API Reference
SessionTimeoutProps
No props data available for “session-timeout”.
Behavior
| Behavior | Detail |
|---|---|
| ESC key | Inert — does not close the overlay |
| Backdrop click | Inert — does not close the overlay |
onSignIn (sync) | Closes immediately (consumer's responsibility) |
onSignIn (async Promise) | Shows spinner + "Signing in…", disables both buttons until settled |
onReturnHome omitted | "Return home" link is not rendered |
open flips to false | Redirecting state resets automatically |
Composition
SessionTimeout builds on @radix-ui/react-dialog via DialogPrimitive. It sets onEscapeKeyDown, onPointerDownOutside, and onInteractOutside to event.preventDefault() to enforce the non-dismissible contract. The overlay renders via DialogPrimitive.Portal at the React portal root — it is never trapped inside a page component's DOM subtree.
Accessibility
aria-labelledbylinks the visible headline;aria-describedbylinks the body copy.- The card surface carries
role="alertdialog"andaria-modal="true". - Focus is trapped inside the overlay while open (Radix built-in).
- The "Sign in" button reports
aria-busyduring theredirectingstate. - The lock icon is
aria-hidden; content meaning is carried by the headline and body copy.