shadimage
better-auth-ui
componentactive-sessionssource: ok · hit

Active Sessions

Display and manage all active sessions for the current user with revoke controls. Shows each session's browser, OS, and creation time.

npx shadcn add "https://better-auth-ui.com/r/{style}/active-sessions.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Registry dependencies

alertalert-dialogbadgebuttoncarditemskeletonsonnerspinner

Files4 · source cached 10/9/2026, 11:54:50 PM

examples/start-shadcn-example/src/components/auth/reauthentication.tsx
"use client"

import {
  getReauthenticationSignInURL,
  isReauthenticationSignInURL
} from "@better-auth-ui/core"
import { useAuth, useSignOut } from "@better-auth-ui/react"
import { useSyncExternalStore } from "react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"
import { cn } from "cn"

const subscribeToLocation = () => () => undefined

function useIsReauthenticationSignIn() {
  return useSyncExternalStore(
    subscribeToLocation,
    () => isReauthenticationSignInURL(new URL(window.location.href)),
    () => false
  )
}

export type ReauthenticationActionProps = {
  className?: string
  showTitle?: boolean
}

export function ReauthenticationAction({
  className,
  showTitle = true
}: ReauthenticationActionProps) {
  const auth = useAuth()
  const signOut = useSignOut(auth.authClient)

  const handleReauthentication = () => {
    const signInURL = getReauthenticationSignInURL(
      new URL(window.location.href),
      `${auth.basePaths.auth}/${auth.viewPaths.auth.signIn}`
    )
    signOut.mutate(undefined, {
      onSuccess: () => auth.navigate({ to: signInURL })
    })
  }

  return (
    <div className={cn("flex flex-col items-start gap-3 p-4", className)}>
      <div className="flex flex-col gap-1">
        {showTitle ? (
          <h3 className="text-sm font-medium">
            {auth.localization.settings.reauthenticationTitle}
          </h3>
        ) : null}
        <p className="text-sm text-muted-foreground">
          {auth.localization.settings.reauthenticationDescription}
        </p>
      </div>
      <Button
        disabled={signOut.isPending}
        onClick={handleReauthentication}
        size="sm"
      >
        {signOut.isPending ? <Spinner data-icon="inline-start" /> : null}
        {auth.localization.settings.reauthenticationAction}
      </Button>
    </div>
  )
}

export function ReauthenticationNotice() {
  const auth = useAuth()
  const isReauthenticationSignIn = useIsReauthenticationSignIn()
  if (!isReauthenticationSignIn) return null

  return (
    <Alert className="mx-4 w-auto group-data-[size=sm]/card:mx-3">
      <AlertTitle>
        {auth.localization.settings.reauthenticationTitle}
      </AlertTitle>
      <AlertDescription>
        {auth.localization.settings.reauthenticationDescription}
      </AlertDescription>
    </Alert>
  )
}
examples/start-shadcn-example/src/components/auth/settings/security/active-session.tsx
"use client"

import { useAuth, useRevokeSession, useSession } from "@better-auth-ui/react"
import type { Session } from "better-auth"
import Bowser from "bowser"
import { LogOut, Monitor, Smartphone, X } from "lucide-react"
import { toast } from "sonner"

import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Item,
  ItemActions,
  ItemContent,
  ItemDescription,
  ItemMedia,
  ItemTitle
} from "@/components/ui/item"
import { Spinner } from "@/components/ui/spinner"

function timeAgo(date: Date) {
  const seconds = Math.floor((Date.now() - date.getTime()) / 1000)
  const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" })

  const UNITS: [Intl.RelativeTimeFormatUnit, number][] = [
    ["year", 31536000],
    ["month", 2592000],
    ["week", 604800],
    ["day", 86400],
    ["hour", 3600],
    ["minute", 60],
    ["second", 1]
  ]

  for (const [unit, threshold] of UNITS) {
    if (seconds >= threshold) {
      return rtf.format(-Math.floor(seconds / threshold), unit)
    }
  }

  return rtf.format(0, "second")
}

export type ActiveSessionProps = {
  activeSession: Session
}

/**
 * Render a single active session row with device info and revoke control.
 *
 * Shows the session's browser, OS, and creation time. The current session is marked
 * and navigates to sign-out on click, while other sessions can be revoked individually.
 *
 * @param session - The session object containing id, token, userAgent, ipAddress, and createdAt
 * @returns A JSX element containing the active session row
 */
export function ActiveSession({ activeSession }: ActiveSessionProps) {
  const { authClient, basePaths, localization, viewPaths, navigate } = useAuth()
  const { data: session } = useSession(authClient, { refetchOnMount: false })

  const { mutate: revokeSession, isPending: isRevoking } = useRevokeSession(
    authClient,
    {
      onSuccess: () => toast.success(localization.settings.revokeSessionSuccess)
    }
  )

  const isCurrentSession = activeSession.token === session?.session.token
  const ua = activeSession.userAgent
    ? Bowser.parse(activeSession.userAgent)
    : undefined
  const isMobile =
    ua?.platform.type === "mobile" || ua?.platform.type === "tablet"

  return (
    <Item>
      <ItemMedia variant="icon">
        {isMobile ? <Smartphone /> : <Monitor />}
      </ItemMedia>
      <ItemContent>
        <ItemTitle>
          {ua?.browser.name || "Unknown Browser"}
          {ua?.os.name ? `, ${ua.os.name}` : ""}
        </ItemTitle>
        {isCurrentSession ? (
          <Badge variant="secondary">
            {localization.settings.currentSession}
          </Badge>
        ) : (
          activeSession.createdAt && (
            <ItemDescription className="capitalize">
              {timeAgo(activeSession.createdAt)}
            </ItemDescription>
          )
        )}
      </ItemContent>
      <ItemActions>
        <Button
          variant="outline"
          size="sm"
          onClick={() =>
            isCurrentSession
              ? navigate({
                  to: `${basePaths.auth}/${viewPaths.auth.signOut}`
                })
              : revokeSession(activeSession)
          }
          disabled={isRevoking}
          aria-label={
            isCurrentSession
              ? localization.auth.signOut
              : localization.settings.revokeSession
          }
        >
          {isRevoking ? <Spinner /> : isCurrentSession ? <LogOut /> : <X />}

          {isCurrentSession
            ? localization.auth.signOut
            : localization.settings.revoke}
        </Button>
      </ItemActions>
    </Item>
  )
}
examples/start-shadcn-example/src/components/auth/settings/security/active-sessions.tsx
"use client"

import { isReauthenticationRequiredError } from "@better-auth-ui/core"
import { useAuth, useListSessions, useSession } from "@better-auth-ui/react"
import { Fragment } from "react"
import { Card, CardContent } from "@/components/ui/card"
import {
  Item,
  ItemContent,
  ItemGroup,
  ItemMedia,
  ItemSeparator
} from "@/components/ui/item"
import { Skeleton } from "@/components/ui/skeleton"
import { cn } from "cn"
import { ReauthenticationAction } from "../../reauthentication"
import { ActiveSession } from "./active-session"
import { SessionActions } from "./session-actions"

export type ActiveSessionsProps = {
  className?: string
}

/**
 * Render a card listing all active sessions for the current user with revoke controls.
 *
 * Shows each session's browser, OS, IP address, and creation time. The current session is marked
 * and navigates to sign-out on click, while other sessions can be revoked individually.
 *
 * @returns A JSX element containing the sessions card
 */
export function ActiveSessions({ className }: ActiveSessionsProps) {
  const { authClient, localization } = useAuth()
  const { data: session } = useSession(authClient)

  const sessionsQuery = useListSessions(authClient, {
    meta: { errorPresentation: "inline" }
  })
  const { data: sessions, error, isPending } = sessionsQuery

  const activeSessions = [...(sessions ?? [])].sort((activeSession) =>
    activeSession.id === session?.session.id ? -1 : 1
  )

  return (
    <div>
      <h2 className="text-sm font-semibold mb-3">
        {localization.settings.activeSessions}
      </h2>

      <Card className={cn("gap-0 p-0", className)}>
        <CardContent className="p-0">
          {isReauthenticationRequiredError(error) ? (
            <ReauthenticationAction />
          ) : error ? (
            <div className="p-4 text-destructive text-sm">{error.message}</div>
          ) : isPending ? (
            <SessionRowSkeleton />
          ) : (
            <ItemGroup className="gap-0!">
              {activeSessions?.map((activeSession, index) => (
                <Fragment key={activeSession.id}>
                  {index > 0 && <ItemSeparator className="my-0!" />}
                  <ActiveSession activeSession={activeSession} />
                </Fragment>
              ))}
            </ItemGroup>
          )}
        </CardContent>
        {!isPending && !error && (
          <SessionActions
            hasOtherSessions={activeSessions.some(
              (activeSession) => activeSession.id !== session?.session.id
            )}
          />
        )}
      </Card>
    </div>
  )
}

function SessionRowSkeleton() {
  return (
    <Item>
      <ItemMedia>
        <Skeleton className="size-10 rounded-md" />
      </ItemMedia>
      <ItemContent>
        <Skeleton className="h-4 w-20" />
        <Skeleton className="h-3 w-32" />
      </ItemContent>
    </Item>
  )
}
examples/start-shadcn-example/src/components/auth/settings/security/session-actions.tsx
"use client"

import {
  useAuth,
  useRevokeOtherSessions,
  useRevokeSessions
} from "@better-auth-ui/react"
import { LogOut } from "lucide-react"
import { useState } from "react"
import { toast } from "sonner"

import {
  AlertDialog,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogTitle
} from "@/components/ui/alert-dialog"
import { Button } from "@/components/ui/button"
import { Spinner } from "@/components/ui/spinner"

type PendingAction = "other" | "all"

export function SessionActions(props: { hasOtherSessions: boolean }) {
  const { authClient, basePaths, localization, navigate, viewPaths } = useAuth()
  const [action, setAction] = useState<PendingAction | null>(null)

  const revokeOtherSessions = useRevokeOtherSessions(authClient, {
    onSuccess: () => {
      toast.success(localization.settings.signOutOtherDevicesSuccess)
      setAction(null)
    }
  })
  const revokeSessions = useRevokeSessions(authClient, {
    onSuccess: () =>
      navigate({
        to: `${basePaths.auth}/${viewPaths.auth.signIn}`,
        replace: true
      })
  })

  const isPending = revokeOtherSessions.isPending || revokeSessions.isPending
  const isEverywhere = action === "all"

  return (
    <>
      <div className="flex flex-wrap justify-end gap-2 border-t p-4">
        <Button
          disabled={!props.hasOtherSessions || isPending}
          onClick={() => setAction("other")}
          size="sm"
          type="button"
          variant="outline"
        >
          {localization.settings.signOutOtherDevices}
        </Button>
        <Button
          disabled={isPending}
          onClick={() => setAction("all")}
          size="sm"
          type="button"
          variant="destructive"
        >
          {localization.settings.signOutEverywhere}
        </Button>
      </div>

      <AlertDialog
        open={action !== null}
        onOpenChange={(open) => !open && !isPending && setAction(null)}
      >
        <AlertDialogContent>
          <AlertDialogHeader>
            <AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
              <LogOut />
            </AlertDialogMedia>
            <AlertDialogTitle>
              {isEverywhere
                ? localization.settings.signOutEverywhere
                : localization.settings.signOutOtherDevices}
            </AlertDialogTitle>
            <AlertDialogDescription>
              {isEverywhere
                ? localization.settings.signOutEverywhereDescription
                : localization.settings.signOutOtherDevicesDescription}
            </AlertDialogDescription>
          </AlertDialogHeader>
          <AlertDialogFooter>
            <AlertDialogCancel disabled={isPending}>
              {localization.settings.cancel}
            </AlertDialogCancel>
            <Button
              disabled={isPending}
              onClick={() =>
                isEverywhere
                  ? revokeSessions.mutate()
                  : revokeOtherSessions.mutate()
              }
              type="button"
              variant={isEverywhere ? "destructive" : "default"}
            >
              {isPending && <Spinner />}
              {isEverywhere
                ? localization.settings.signOutEverywhere
                : localization.settings.signOutOtherDevices}
            </Button>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  )
}

More from better-auth-ui

Browse all components in this library →