Active Sessions
Signed-in devices with place and last active: this one is pinned, revoke others with a confirm or all at once.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/active-sessions.json'use client'
import { useState } from 'react'
import './active-sessions.css'
export interface Session { id: string; device: 'laptop' | 'phone' | 'tablet'; name: string; browser: string; place: string; last: string; current?: boolean }
const IC = {
laptop: <><rect x="4" y="5" width="16" height="11" rx="1.5" /><path d="M2 19h20" /></>,
phone: <><rect x="7" y="3" width="10" height="18" rx="2.2" /><path d="M11 18h2" /></>,
tablet: <><rect x="4.5" y="3" width="15" height="18" rx="2" /><path d="M11 18h2" /></>,
}
export interface ActiveSessionsProps {
sessions: Session[]
onRevoke?: (ids: string[]) => void
confirm?: boolean
fill?: 'dark' | 'light'
className?: string
}
/** Session list with per-device and bulk sign-out. */
export function ActiveSessions({ sessions: s0, onRevoke, confirm = true, fill = 'dark', className }: ActiveSessionsProps) {
const [list, setList] = useState(s0)
const [gone, setGone] = useState<string[]>([])
const [sure, setSure] = useState<string | null>(null)
const [msg, setMsg] = useState('')
const drop = (ids: string[]) => {
setGone((g) => [...g, ...ids]); onRevoke?.(ids)
setTimeout(() => { setList((l) => l.filter((x) => !ids.includes(x.id))); setGone((g) => g.filter((x) => !ids.includes(x))) }, 300)
}
const revoke = (s: Session) => {
if (confirm && sure !== s.id) { setSure(s.id); return }
setSure(null); drop([s.id]); setMsg(`Signed out ${s.name}`)
}
const others = list.filter((s) => !s.current)
return (
<div className={['sd-ses', className].filter(Boolean).join(' ')} data-fill={fill}>
<div className="sd-ses__h"><b>Where you’re signed in</b><span>{list.length} active {list.length === 1 ? 'session' : 'sessions'}</span></div>
<ul className="sd-ses__list">
{[...list].sort((a, b) => Number(!!b.current) - Number(!!a.current)).map((s) => (
<li key={s.id} className="sd-ses__it" data-gone={gone.includes(s.id) || undefined}>
<span className="sd-ses__ic" aria-hidden><svg viewBox="0 0 24 24">{IC[s.device]}</svg></span>
<span className="sd-ses__d">{s.name}{s.current && <span className="sd-ses__cur">This device</span>}<small>{s.browser} · {s.place} · {s.current ? 'Active now' : s.last}</small></span>
{!s.current && (
<button type="button" className="sd-ses__btn" data-sure={sure === s.id || undefined} aria-label={sure === s.id ? `Confirm sign out of ${s.name}` : `Sign out of ${s.name}`}
onClick={() => revoke(s)} onBlur={() => setSure((x) => (x === s.id ? null : x))}>{sure === s.id ? 'Sign out?' : 'Revoke'}</button>
)}
</li>
))}
</ul>
<button type="button" className="sd-ses__all" disabled={!others.length} onClick={() => { drop(others.map((s) => s.id)); setMsg(`Signed out of ${others.length} other ${others.length === 1 ? 'session' : 'sessions'}`) }}>
Sign out all other sessions
</button>
<p className="sd-ses__msg" role="status">{msg}</p>
</div>
)
}.sd-ses { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-ses[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-ses :where(button) { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-ses :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-ses .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-ses { width: min(100%, 340px); border-radius: 20px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); overflow: hidden; }
.sd-ses__h { padding: 14px 16px 10px; }
.sd-ses__h b { display: block; font-size: 15px; }
.sd-ses__h span { font-size: 12.5px; color: var(--mute); }
.sd-ses__list { list-style: none; margin: 0; padding: 0 6px; }
.sd-ses__it { display: grid; grid-template-columns: 36px 1fr auto; align-items: center; gap: 12px; padding: 10px; border-radius: 12px; transition: opacity 260ms, transform 260ms, max-height 300ms; max-height: 80px; }
.sd-ses__it[data-gone] { opacity: 0; transform: translateX(30px); max-height: 0; padding-top: 0; padding-bottom: 0; }
.sd-ses__ic { display: grid; place-items: center; width: 36px; height: 36px; border-radius: 10px; background: var(--soft); }
.sd-ses__ic svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.sd-ses__d { min-width: 0; font-size: 14px; }
.sd-ses__d small { display: block; font-size: 12px; color: var(--mute); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.sd-ses__cur { display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 99px; background: var(--strong); color: var(--on); font-size: 10.5px; font-weight: 700; vertical-align: 1px; }
.sd-ses__btn { padding: 6px 10px; border-radius: 8px; font-size: 12.5px; font-weight: 500; box-shadow: inset 0 0 0 1px var(--line); white-space: nowrap; }
.sd-ses__btn:hover { background: var(--soft); }
.sd-ses__btn[data-sure] { background: var(--strong) !important; color: var(--on) !important; box-shadow: none; font-weight: 600; }
.sd-ses__all { display: block; width: calc(100% - 32px); margin: 8px 16px 16px; padding: 11px; border-radius: 12px; font-size: 13.5px; font-weight: 600; box-shadow: inset 0 0 0 1px var(--line); }
.sd-ses__all:hover { background: var(--soft); }
.sd-ses__all:disabled { opacity: 0.4; cursor: default; }
.sd-ses__msg { margin: -6px 16px 14px; font-size: 12.5px; color: var(--mute); text-align: center; }
@media (prefers-reduced-motion: reduce) { .sd-ses *, .sd-ses *::before, .sd-ses *::after { animation: none !important; transition: none !important; } }Overview
Each session shows device, browser, rough location and when it was last used. Revoke asks once inline, then the row slides away.
“Sign out all other sessions” keeps the current device and clears the rest, with a count of what it did.
When to use
- Account security pages, admin user detail views.
When not to use
- Apps without server-side sessions to revoke.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/active-sessions.json$
npx shadcn@latest add https://sundaeui.dev/r/active-sessions.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/active-sessions.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/active-sessions.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy active-sessions.tsx and active-sessions.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { ActiveSessions } from '@/components/sundae/active-sessions'
<ActiveSessions />Props
| Prop | Type | Default |
|---|---|---|
sessions*Device, browser, place, lastActive, current. | Session[] | — |
onRevokeCalled with revoked ids. | (ids: string[]) => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- Each session is a list item; revoke buttons name the device.
- The confirm step moves focus to its confirm button; results are announced.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-ses; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Active Sessions free?
- Yes. Active Sessions is free for personal and commercial projects, with full source on this page.
- How do I install Active Sessions?
- Run npx shadcn@latest add https://sundaeui.dev/r/active-sessions.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Active Sessions support keyboard and screen readers?
- Each session is a list item; revoke buttons name the device. The confirm step moves focus to its confirm button; results are announced.
- Does Active Sessions respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All components →API Key Row
Components · RefinedSmart Lock
Components · RefinedToast Stack
Components · Refined- Pro
Sheet Drawer
Components · Refined Avatar Stack
Components · Refined- Pro
Team Members
Components · Understated