Skip to content

Session Timeout

Warn people before an inactive session signs out: a live countdown, Stay signed in, and a signed-out page when time runs out.

import { SessionSignedOut, SessionTimeout, SessionTimeoutState, UseSessionTimeoutOptions } from '@youj/react'

Installation & usage

npm i @youj/react

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Use a Session Timeout where a session is sensitive enough to end by itself (banking, admin consoles, health records) and the person must be told before it does. Wrap the app: the signed-out page replaces it once the session ends.

'use client'

import { SessionTimeout } from '@youj/react'

export function Shell({ children }: { children: React.ReactNode }) {
  return (
    <SessionTimeout
      timeout={15 * 60_000}
      warnBefore={2 * 60_000}
      onExtend={() => fetch('/api/session/extend', { method: 'POST' })}
      onTimeout={() => fetch('/api/session/end', { method: 'POST' })}
      onSignOut={() => fetch('/api/session/end', { method: 'POST' })}
      onSignIn={() => location.assign('/sign-in')}
    >
      {children}
    </SessionTimeout>
  )
}

warnBefore defaults to 2 minutes, as the UK government's session-timeout pattern recommends. onTimeout fires when inactivity ends the session and onSignOut when the person chooses Sign out now; both usually end the session on the server. The clock only knows about the page: renewing the session server-side stays in onExtend.

What counts as activity. Pointer, key, scroll and a tab being shown again, throttled to once a throttle (1 second). Narrow it with events. Once the warning is up activity is ignored: only Stay signed in extends, so a stray mouse move can't hide a sign-out the person hasn't read.

Hidden tabs. The clock stores the time of the last activity and works out what is left from Date.now(), never by counting ticks. A tab that was asleep past the deadline signs out the moment it wakes.

Several tabs. Tabs on the same channel share one clock over a BroadcastChannel: activity or Stay signed in in any tab resets all of them, and a sign-out in one signs out the rest (without calling onTimeout again; the tab that ended it did). Give each app its own channel, or pass false to keep tabs apart. Where BroadcastChannel isn't available each tab times out on its own.

The page as a function. children may be a function of the session (status, secondsLeft, extend, simulateIdle…), for a header that shows the time left or a button that simulates idling in a demo.

Reference

Propstimeout, warnBefore, events, throttle, channel, onExtend, onTimeout…

Props

SessionTimeout props
PropType
childrenReactNode | ((session: SessionTimeoutState) => ReactNode)

The app, or a function of the session (status, secondsLeft, extend, simulateIdle…). Replaced by the signed-out page once the session ends, so nothing stays on screen.

–
onSignIn() => unknown

Called by the signed-out page's Sign in again button: go to your sign-in.

–
signedOutReactNode | false

Replaces the signed-out page. false shows none.

–
timeoutRequirednumber

How long a session may sit without activity before it ends, in ms.

–
warnBeforenumber

How long before the end the warning starts, in ms. Default 120000 (2 minutes).

–
eventsSessionActivityEvent[]

What counts as activity. Default all four; visibility counts the tab being shown again.

–
throttlenumber

Activity is noted at most this often, in ms.

default 1000

1000
enabledboolean

Turn the clock off, say while the person is signed out.

default true

true
channelstring | false

The BroadcastChannel name that ties tabs together. Give each app its own. false keeps tabs apart.

default `youj-session-timeout`

`youj-session-timeout`
onWarn() => void

Fires once when the warning starts.

–
onExtend() => unknown

"Stay signed in": renew the session on the server. Awaited; if it rejects the warning stays up.

–
onTimeout() => void

Fires once when the session ends because of inactivity: sign out here.

–
onSignOut() => unknown

Fires when the person chooses "Sign out now". Awaited.

–

Signed out

SessionSignedOut props
PropType
onSignIn() => unknown

Called by the Sign in again button.

–
titleReactNode

Heading.

default "You've been signed out"

"You've been signed out"
descriptionReactNode

Explanation under the heading.

–
signInLabelstring

The button's label.

default "Sign in again"

"Sign in again"
Other props pass through: extends ComponentProps<'section'>.

Data attributes

The warning dialog is an Alert Dialog; its content carries data-slot="session-timeout-dialog", with session-timeout-countdown (the visible line), session-timeout-sign-out and session-timeout-stay on its parts. The hidden live region is session-timeout-announcer. The signed-out page is session-signed-out, its button session-sign-in.

MotionDialog, countdown · reduced motion

Motion

Motion tokens
MomentTokens
Warning opens and closesthe Alert Dialog’s own pop over a fading scrim
duration.normalease.enter

undefinedms · cubic-bezier(0.16, 1, 0.3, 1)

undefinedmscubic-bezier(0.16, 1, 0.3, 1)
Countdownthe digits change once a second, with no animation of their own
no token

Stay signed inan Async Button: spinner, then a check, while onExtend runs
no token

Reduced motion

The dialog appears at once, and the visible line is a static phrase (“2 minutes”) that changes only when it is spoken, instead of a ticking clock.

Reduced motion

The visible countdown is static: it shows the same phrase screen readers hear, updated at the start, each minute and at 30 and 15 seconds.

AccessibilityFocus, Escape, announcements, labels

Accessibility

Follows the GOV.UK and DWP session-timeout guidance.

  • The warning is an alertdialog named by its title. Focus moves into it, onto Stay signed in, and returns to where it was once the person stays.
  • Escape means stay signed in, not dismiss into nowhere. The dialog doesn't close on a press outside.
  • The ticking line is hidden from screen readers. The dialog's description says the time as it opens ("2 minutes"), and a polite live region speaks it again each minute and at 30 and 15 seconds, never every second.
  • Times are spoken as words in the person's language ("1 minute, 42 seconds") and shown as a clock ("1:42").
  • When the session ends, focus moves to the signed-out page's heading, and Sign in again is a button.
  • The words (title, warning, reason, both buttons, the signed-out page) are Youj labels in every built-in locale; pass labels to the provider to change them.