ElvixSignOutButton
Symmetric counterpart of `<ElvixSignInButton>`. One click invalidates the elvix session server-side, clears the SDK's local token, drops the `elvix_token` cookie, then navigates.
"use client";import { ElvixSignOutButton } from "@elvix.is/sdk/react";export function AccountMenu() {return (<ElvixSignOutButtontone="destructive"variant="filled"size="md"redirectAfterSignOut="/"onResult={(r) => {if (!r.ok) console.warn(r.error, r.message);// The button has already cleared the SDK's in-memory + cookie// token by the time onResult fires; the navigation also runs// (unless you set redirectAfterSignOut={null}).}}/>);}
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
as | "button" | "menuitem" | "link" | optional | "button" | The form the action takes. button is the standalone CTA, menuitem a full-width dropdown / account-menu row, link an inline text link. Same sign-out behaviour, different placement. <ElvixSignOutMenuItem> and <ElvixSignOutLink> are thin aliases for the last two. |
children | (api: { signOut, busy }) => ReactNode | optional | none | Headless escape hatch. Pass a render-fn and YOU own the element (a Radix <DropdownMenuItem>, your own button, anything). When provided, as and all styling props are ignored. |
tone | "neutral" | "brand" | "destructive" | optional | "neutral" | Affordance. neutral for low-key footer / menu items, brand for emphasised primary action, destructive (red) for security-leaning surfaces. |
variant | "filled" | "outline" | "ghost" | optional | "outline" | Visual treatment within the chosen tone. |
label | string | optional | from preset | Button text override. |
preset | "sign-out" | "log-out" | "sign-out-of-elvix" | "sign-out-of-app" | optional | "sign-out" | Canned verb. SaaS defaults to "Sign out"; banks / B2B legacy prefer "Log out". |
type | "standard" | "icon" | optional | "standard" | Icon-only or icon + label. |
shape | "rectangle" | "pill" | "square" | "circle" | optional | "rectangle" | Corner radius. Icon-only buttons coerce pill -> circle, rectangle -> square. |
size | "sm" | "md" | "lg" | optional | "md" | Matches <ElvixSignInButton> sizing. |
theme | "auto" | "light" | "dark" | optional | "auto" | Force a chord. auto inherits from the surrounding theme. |
showIcon | boolean | optional | true | Show the leading icon. Set false for a label-only button. |
icon | (size: number) => ReactNode | optional | lucide LogOut | Override the default sign-out icon. Receives pixel size, returns a node. |
brandColor | string | optional | elvix lavender (#6c5ce7) | Overrides the brand chord on tone="brand" + variant="filled". Use the host's own brand. |
onBrandColor | string | optional | "#ffffff" | Foreground (icon + label) painted on top of brandColor. Pick a WCAG-AA contrast pair. |
align | "left" | "center" | "right" | optional | "center" | Content alignment inside the button. Combine with className="w-full" for a full-width menu item. |
fontSize | number | string | optional | from size preset | Override the label font size. Number is treated as px; string is any CSS length. |
borderRadius | number | string | optional | from shape preset | Custom corner radius. Wins over shape. Number is px; string is any CSS length ("8px", "50%"). |
redirectAfterSignOut | string | null | optional | "/" | Where to navigate after sign-out. Pass null to let the host own navigation via onResult. |
cookieName | string | null | optional | "elvix_token" | Client-side cookie to clear after the server invalidates the session. Set to null to skip (your server clears its own httpOnly cookie). |
onResult | (r: ElvixSignOutResult) => void | optional | none | Terminal callback. Fires AFTER the session is invalidated server-side AND the local token + cookie are cleared. ResponseDto-shape. |
One action, four forms
The same sign-out logic ships in whatever shape your surface needs. Switch with as, or reach for a named alias, or go fully headless with a render-fn:
"use client";import {ElvixSignOutButton,ElvixSignOutMenuItem,ElvixSignOutLink,} from "@elvix.is/sdk/react";import { DropdownMenuItem } from "@/components/ui/dropdown-menu";export function SignOutForms() {return (<>{/* 1. standalone button (default) */}<ElvixSignOutButton tone="destructive" />{/* 2. account-menu row, via prop OR the named alias */}<ElvixSignOutButton as="menuitem" align="left" className="w-full" /><ElvixSignOutMenuItem align="left" className="w-full" />{/* 3. inline text link */}<ElvixSignOutLink label="Log out" />{/* 4. fully headless — wire onto your own design-system element */}<ElvixSignOutButton>{({ signOut, busy }) => (<DropdownMenuItem onSelect={signOut} disabled={busy}>{busy ? "Signing out…" : "Sign out"}</DropdownMenuItem>)}</ElvixSignOutButton></>);}
<ElvixSignOutMenuItem> (= as="menuitem") and <ElvixSignOutLink> (= as="link") are exported alongside <ElvixSignOutButton> for hosts that prefer an explicit name over a prop. All three, plus the headless render-fn and the useSignOut / signOut entry points below, share one implementation — pick whichever fits the element you already have.
httpOnly-cookie hosts: clearing your own cookie is NOT a sign-out. If your server keeps the elvix token in its own httpOnly cookie (so the SDK can't read it client-side), deleting that cookie leaves the elvix session alive. On your sign-in page, Google One Tap auto-select (or redirect-if-authenticated) then silently re-signs the user in and bounces them straight back to the dashboard. Do all three: (1) call the SDK signOut() on the CLIENT so it sets the one-shot 'signed out' flag the sign-in form reads to suppress that auto-resume — pass redirectAfterSignOut: null so it does NOT navigate (the SDK can't revoke here anyway, since it can't read your httpOnly token; without null its default '/' redirect tears down the page and races your server action so the revoke below never runs); (2) end the elvix session server-side from a route/server-action that CAN read the cookie, by POSTing Authorization: Bearer <token> to /api/auth/sign-out?surface=app, then redirect to your sign-in route; (3) clear your own cookie. Step 3 alone is the recurring bug; the redirectAfterSignOut: null in step 1 is the second.
Three tones cover the common placements. neutral (default) reads as a low-pressure account-menu item. brand uses your app's brand chord for an emphasised primary action. destructive is solid red, the affordance pattern you want when sign-out is part of a security surface ("Sign out everywhere", "End this session"). The host picks the tone per placement; the surface stays consistent across the app.
The default leading icon is lucide's LogOut (door + arrow), the universal sign-out glyph. Hide it with showIcon={false} for label-only, or pass a custom node via icon={(px) => <YourIcon size={px} />}.
Cross-origin and same-origin behave identically. The SDK's cross-origin interceptor attaches Authorization: Bearer <token> and rewrites the URL to https://elvix.is/api/auth/sign-out when the host is on a customer origin. Same-origin (running on elvix.is itself) sends the session cookie. Either way, the backend emits the user.signed_out webhook for your application.
Need a host-controlled flow (e.g. you set your own httpOnly cookie server-side and want to tear it down through your route)? Set redirectAfterSignOut={null} and handle navigation inside onResult. The component still invalidates the elvix session and clears the SDK's local token; the cookie clear is gated by cookieName.
This button handles USER-INITIATED sign-out. Forced sign-outs (ban / pause / delete from the Console) flow through `ElvixLifecycleWatcher`, which clears the same state within ~9 seconds without any user interaction. Mount the watcher once on any signed-in surface; the button covers the click path.
Loading state. As soon as the user clicks, the icon swaps to a spinning Loader2 and the label switches to Signing out… until the server invalidates the session and the SDK clears its local state. aria-busy flips on the button so screen readers track the transition. The chord stays the same (tone / variant / brandColor unchanged) so the button doesn't shift.
First-click reliability. Internally the post-sign-out navigation now goes through window.location.replace(new URL(target, location.origin).toString()) — synchronous, with the host's own origin as the resolution base. The previous microtask wrap was occasionally letting a parallel host re-render swallow the redirect (the historical "have to click twice" report); this path is single-attempt and host-origin-anchored.
Use your own button: useSignOut hook
Already have a sign-out affordance in your design system? Skip the chrome and call the hook directly. Returns { run, busy }; run() resolves with the same SignOutResult shape, and busy is the in-flight state for spinner / disabled UI.
"use client";import { useSignOut } from "@elvix.is/sdk/react";export function AccountMenuItem() {const { run, busy } = useSignOut({ redirectAfterSignOut: "/" });return (<button type="button" disabled={busy} onClick={() => run()}>{busy ? "Signing out…" : "Sign out"}</button>);}
Use from outside React: signOut() function
Vanilla function for non-React contexts: keyboard shortcuts, idle-timeout handlers, route loaders, anywhere a hook can't be called. Same options object, same return type, no React imports.
import { signOut } from "@elvix.is/sdk/react";// From a keyboard shortcut, an idle-timeout handler, anywhere outside// React. No hook needed.window.addEventListener("keydown", async (e) => {if (e.ctrlKey && e.shiftKey && e.key === "Q") {const r = await signOut({ redirectAfterSignOut: "/" });if (!r.ok) console.warn(r.error, r.message);}});
All three entry points share one implementation. Whichever you reach for, the flow is identical: POST /api/auth/sign-out?surface=app (cookie OR bearer, automatically), clear the SDK token, clear the cookie, navigate. Pick the one that fits the surface you have.
Related
ElvixSignInButton
Drop-in 'Sign in with elvix' button. Four variants by three sizes by four shapes. The cheapest entry point: one line and you have a working sign-in flow.
ReadElvixSignInForm
The full branded sign-in surface (card, header, brand wash, every Console-enabled method, Secured-by-elvix badge). Exported as both `ElvixSignInForm` and the short alias `ElvixSignIn` — same component.
ReadBan / pause / delete watcher
Mount once in any authenticated layout. When the user's membership flips off active, the watcher paints a full-screen blocking modal with a 9-second countdown, then signs them out and redirects.
Read