# 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.

<!-- components/account-menu.tsx -->
```tsx
"use client";

import { ElvixSignOutButton } from "@elvix.is/sdk/react";

export function AccountMenu() {
  return (
    <ElvixSignOutButton
      tone="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:

<!-- components/sign-out-forms.tsx -->
```tsx
"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>
    </>
  );
}
```

> **Note:** `<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.

> **Warning:** 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.

> **Note:** 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.

> **Note:** 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} />}`.

> **Note:** 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.

> **Note:** 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`.

> **Warning:** This button handles USER-INITIATED sign-out. Forced sign-outs (ban / pause / delete from the Console) flow through [`ElvixLifecycleWatcher`](/docs/components/elvix-lifecycle-watcher.md), 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.

> **Note:** **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.

> **Note:** **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.

<!-- components/account-menu-item.tsx -->
```tsx
"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.

<!-- lib/keyboard-shortcuts.ts -->
```ts
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);
  }
});
```

> **Note:** 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](/docs/components/elvix-sign-in-button.md): 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.
- [ElvixSignInForm](/docs/components/elvix-sign-in-form.md): 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.
- [Ban / pause / delete watcher](/docs/components/elvix-lifecycle-watcher.md): 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.
