Agora

React Components

Pre-built, themeable components — buttons, profile and organization cards, the interaction forms, PasskeyButton, CanPermission, and KeyRotation.

@adonis-agora/authkit-react exports three layers: auth-state primitives, a typed HTTP client with TanStack Query hooks, and the pre-built UI components documented here. The components are built on the headless hooks and consume the same auth state and AuthkitProvider config — no extra wiring beyond what React sets up. Several render nothing when the user is unauthenticated, so they're safe to drop into a shared layout.

For data-fetching in management screens (admin panels, account pages), see Typed Client & TanStack Query.

Import the stylesheet once (anywhere in your app entry):

import '@adonis-agora/authkit-react/styles.css'

import {
  SignInButton,
  SignOutButton,
  UserButton,
  UserProfile,
  AuthorizedApps,
  Avatar,
  PasswordStrengthMeter,
  OrganizationSwitcher,
  OrganizationProfile,
  InteractionForm,
  MagicLinkButton,
  OAuthButton,
  PasskeyButton,
  CanPermission,
  KeyRotation,
} from '@adonis-agora/authkit-react'

All components are SSR-safe (no window access at module load).

SignInButton

Starts the OIDC login redirect (via useSignIn). Hidden when already authenticated unless showWhenAuthenticated is set. Extends <button> — any extra HTML button attribute is forwarded.

PropTypeDefaultDescription
childrenReactNode'Entrar'Button label.
returnTostringcurrent URLWhere to return after login.
showWhenAuthenticatedbooleanfalseRender even when logged in.
classNamestringAppended to authkit-button authkit-button--primary.
...restButtonHTMLAttributesForwarded to the <button>.
<SignInButton returnTo="/dashboard">Sign in</SignInButton>

SignOutButton

Logs out (via useSignOut). Renders nothing when unauthenticated.

PropTypeDefaultDescription
childrenReactNode'Sair'Button label.
returnTostringWhere to go after logout.
classNamestringAppended to authkit-button authkit-button--ghost.
...restButtonHTMLAttributesForwarded to the <button>.
<SignOutButton returnTo="/" />

UserButton

A clickable avatar that opens a dropdown menu with a profile link (config.profileUrl from AuthkitProvider) and a sign-out item. Renders nothing when unauthenticated.

PropTypeDefaultDescription
profileLabelstring'Perfil'Label of the profile menu item.
signOutLabelstring'Sair'Label of the sign-out menu item.
classNamestringAppended to the authkit-userbutton wrapper.
<UserButton profileLabel="Profile" signOutLabel="Sign out" />

UserProfile

A card showing the avatar, name, and email, with an inline edit form that POSTs to the configured profile endpoint (via useProfile). Renders nothing when unauthenticated; disables the submit button while saving and surfaces errors.

PropTypeDefaultDescription
classNamestringAppended to authkit-card authkit-profile.
<UserProfile />

AuthorizedApps

Lists the OAuth/OIDC apps the user has authorized, each with a revoke button (via useAuthorizedApps). Shows loading, error, and empty states.

PropTypeDefaultDescription
revokeLabelstring'Revogar'Label of each revoke button.
emptyLabelstring'Nenhum app autorizado.'Shown when the list is empty.
classNamestringAppended to the authkit-apps list.
<AuthorizedApps revokeLabel="Revoke" emptyLabel="No authorized apps." />

Avatar

The avatar primitive used by UserButton and UserProfile. Renders the user's avatarUrl as an <img>, or falls back to initials derived from name / email.

PropTypeDefaultDescription
userPick<AuthUser, 'name' | 'email' | 'avatarUrl'>The user to render (required).
sizenumber36Width/height in pixels.
classNamestringAppended to authkit-avatar.
import { useAuth } from '@adonis-agora/authkit-react'

function Me() {
  const { user } = useAuth()
  return user ? <Avatar user={user} size={48} /> : null
}

PasswordStrengthMeter

A visual meter (four segments plus a label) for password strength feedback. It is not a dumb bar: give it the password and it calls usePasswordStrength itself, so the meter and the score can never drift apart.

PropTypeDefaultDescription
passwordstringThe password to evaluate (required).
scorer(password: string) => { score, feedback? }built-in heuristicCustom scorer, e.g. zxcvbn.
showFeedbackbooleantrueRender the tip list below the bar.
labels[string, string, string, string, string]['Very weak', 'Weak', 'Fair', 'Good', 'Strong']One label per score, for i18n.
classNamestringAppended to authkit-strength.
import { useState } from 'react'
import { PasswordStrengthMeter } from '@adonis-agora/authkit-react'

function PasswordField() {
  const [password, setPassword] = useState('')
  return (
    <>
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} />
      <PasswordStrengthMeter password={password} />
    </>
  )
}

The bar is a role="meter" with aria-valuemin=0, aria-valuemax=4, aria-valuenow set to the score, and the matching label as its accessible name — a screen reader gets the same signal as the colour.

Localise it with labels, hide the tips with showFeedback={false}, and swap the algorithm with scorer:

import { zxcvbn } from '@zxcvbn-ts/core'

// Module scope: a stable function identity keeps the memoisation working.
const scorer = (password: string) => {
  const { score, feedback } = zxcvbn(password)
  return { score, feedback: feedback.suggestions }
}

<PasswordStrengthMeter
  password={password}
  scorer={scorer}
  labels={['Muito fraca', 'Fraca', 'Razoável', 'Boa', 'Forte']}
/>

CSS hooks: .authkit-strength (root), .authkit-strength__bar (carries data-score="0…4"), .authkit-strength__segment / --filled, .authkit-strength__label, and .authkit-strength__feedback. Colour per score is driven from the data-score attribute, so you can restyle the scale without touching the component.

OrganizationSwitcher

A dropdown to switch the active organization. It reads the list and the active id from useOrganizations() and calls useSwitchOrganization() on selection — selecting the personal account entry deactivates the current org, selecting another org activates it. Each row shows the org name and the user's role in it.

It renders nothing when the user is unauthenticated, when the server reports that organizations are not supported, or when the provider is configured with idp: 'external'.

PropTypeDefaultDescription
personalAccountLabelstring'Conta pessoal'Label of the "no active org" entry.
classNamestringAppended to authkit-orgswitcher.
<OrganizationSwitcher personalAccountLabel="Personal account" />

Creating an organization is not part of this component — it is a server-side decision (see Organizations). Build the create flow with useOrganizations() and your own form when your deployment allows self-service creation.

CSS hooks: .authkit-orgswitcher, __trigger, __label, __chevron, __menu, __item (with --active), __item-name, __item-role.

OrganizationProfile

A card for the active organization: name and slug, the member list with each member's role, an invite form, and a "leave organization" button.

There is no orgId prop, and that is the point — the component asks useOrganizations() for the active org id and loads its detail with useOrganization(). The active org is server-side session state, so a prop here could disagree with the session; instead, switching orgs (via OrganizationSwitcher or useSwitchOrganization()) is what changes what this card shows. It renders nothing when unauthenticated or when there is no active org, and degrades to null under idp: 'external'.

The invite form only renders when the loaded org says the current member may manage it, so a plain member sees the roster without controls they cannot use.

PropTypeDefaultDescription
inviteLabelstring'Convidar membro'Heading and submit label of the invite form.
leaveLabelstring'Sair da organização'Label of the leave button.
classNamestringAppended to authkit-card authkit-org-profile.
<OrganizationProfile inviteLabel="Invite member" leaveLabel="Leave organization" />

InteractionForm

The composable primitive behind the login-page buttons. It renders a <form method="POST"> pointing at the right AuthKit interaction endpoint (resolved from the uid via interactionUrls, so you never hand-concatenate /auth/interaction/...) plus the hidden _csrf field — you own the fields and the styling inside it. Use it directly when you want full control of a step's form; the pre-built buttons are built on top of it.

PropTypeDefaultDescription
uidstringThe interaction uid (rendered by authkit-server into the login view).
step'identifier' | 'login' | 'magic' | 'otpVerify'Which POST endpoint to submit to.
csrfTokenstringBecomes the hidden _csrf field.
basePathstring'/auth/interaction'Mount prefix, if you changed it server-side.
childrenReactNodeYour fields and submit button.
...restFormHTMLAttributesForwarded to the <form> (minus method/action).

The four steps are the four POST endpoints of the login interaction:

stepWhat the form submitsTypical fields
identifierThe email/username, moving the user to the credential stepemail
loginPassword authenticationemail, password
magicA request for a magic link or an emailed codeemail (plus an optional channel)
otpVerifyThe six-digit code from the magic-link emailthe field named by OTP_CODE_FIELD
import { InteractionForm } from '@adonis-agora/authkit-react'

// Password step — the app owns the inputs and layout.
<InteractionForm uid={uid} step="login" csrfToken={csrfToken}>
  <input type="email" name="email" autoComplete="username" />
  <input type="password" name="password" autoComplete="current-password" />
  <button type="submit" className="authkit-button authkit-button--primary">Sign in</button>
</InteractionForm>

For the OTP step, take the input's name from OTP_CODE_FIELD rather than typing "code" — the screen and your end-to-end test then read the same constant, and a typo breaks both together instead of drifting apart silently:

import { InteractionForm, OTP_CODE_FIELD } from '@adonis-agora/authkit-react'

<InteractionForm uid={uid} step="otpVerify" csrfToken={csrfToken}>
  <input
    name={OTP_CODE_FIELD}
    inputMode="numeric"
    autoComplete="one-time-code"
    maxLength={6}
  />
  <button type="submit">Verify</button>
</InteractionForm>

Every InteractionForm carries a double-submit lock, whatever children you put in it: on submit, its submit buttons are disabled and the form gets aria-busy="true". These endpoints trigger expensive work — sending an email, starting a WebAuthn ceremony — and an impatient double click otherwise turns into a duplicate POST (and, on the magic step, into the host's rate limiter). The lock is applied after the browser has built the form's entry list, so a named submit button still contributes its value, and it never fires when a handler of yours called preventDefault() or when native validation blocked the submit. There is nothing to unlock: the response is a new document, so the state resets by itself.

MagicLinkButton

A drop-in magic-link button: an InteractionForm targeting the magic step with a single submit button. Themeable via className (merged with authkit-button) and children.

PropTypeDefaultDescription
uidstringThe interaction uid.
csrfTokenstringCSRF token for the form.
basePathstring'/auth/interaction'Mount prefix, if changed.
channel'code' | 'link'Submits a hidden channel field, asking for only the emailed code or only the link. Omit it to let the server decide.
childrenReactNode'Enviar link de login'Button label.
...restButtonHTMLAttributesForwarded to the <button> (minus type).
<MagicLinkButton uid={uid} csrfToken={csrfToken}>Email me a login link</MagicLinkButton>

{/* A "send me a code" button on a screen that then renders the otpVerify form */}
<MagicLinkButton uid={uid} csrfToken={csrfToken} channel="code">
  Email me a 6-digit code
</MagicLinkButton>

OAuthButton

A drop-in social-login link. Because the OAuth redirect is a full-page navigation, this renders an <a> (not a JS button) pointing at the provider's redirect URL (/{basePath}/{provider}/redirect/{uid}). It's intentionally generic — pass the provider's icon/label as children and style it with className.

PropTypeDefaultDescription
providerstringOAuth provider, e.g. 'google', 'github'.
uidstringThe interaction uid.
basePathstring'/auth'OAuth mount prefix, if changed.
childrenReactNodeEntrar com {Provider}Button content (icon + label).
...restAnchorHTMLAttributesForwarded to the <a> (minus href).
<OAuthButton provider="google" uid={uid}>
  <GoogleIcon /> Continue with Google
</OAuthButton>

PasskeyButton

A drop-in passkey login button — the top tier of the three-tier passkey API. It renders the button, runs the WebAuthn ceremony on click (options → browser prompt → full-page verify POST), disables itself while busy, and shows an inline error on failure. Drop to usePasskeyLogin when you want your own markup, and to the bare functions when your screen owns the state machine.

PropTypeDefaultDescription
optionsUrlstringThe interaction passkey/options endpoint (POST).
verifyUrlstringThe interaction passkey/verify endpoint (full-page POST).
csrfTokenstring?Sent as x-csrf-token on options and _csrf on verify.
childrenReactNode'Entrar com passkey'Button label.
errorContentReactNodedefault messageShown on failure. Pass null to disable (handle errors yourself).
classNamestringMerged with authkit-button.
...restButtonHTMLAttributesForwarded to the <button> (minus onClick).
import { PasskeyButton, interactionUrls } from '@adonis-agora/authkit-react'

const urls = interactionUrls(uid)

<PasskeyButton
  optionsUrl={urls.passkeyOptions}
  verifyUrl={urls.passkeyVerify}
  csrfToken={csrfToken}
>
  Sign in with a passkey
</PasskeyButton>

@simplewebauthn/browser is an optional peer dependency; without it the ceremony fails with an install instruction instead of reaching for a CDN. See WebAuthn / passkeys for the server side.

Interaction URLs

Every component on this page that talks to the login interaction takes URLs, not a uid plus string concatenation. interactionUrls(uid, basePath?) is the single typed source for them — if the server-side mount prefix changes, one argument moves instead of a dozen template literals breaking silently.

import { interactionUrls, oauthRedirectUrl, OTP_CODE_FIELD } from '@adonis-agora/authkit-react'

const urls = interactionUrls(uid)
// urls.identifier      POST — submit the email, advance to the credential step
// urls.login           POST — password login
// urls.magic           POST — send the magic link / code
// urls.otpVerify       POST — verify the six-digit code
// urls.passkeyOptions  POST — begin the passkey ceremony
// urls.passkeyVerify   POST — finish it (full-page POST)
// urls.signup          GET  — the account-creation screen
// urls.switch          GET  — switch account (back to the identifier step)

// A custom mount prefix, passed once:
const urls = interactionUrls(uid, '/entrar/interaction')

// Social login is a full-page navigation to the provider redirect:
const google = oauthRedirectUrl('google', uid) // '/auth/google/redirect/{uid}'
const github = oauthRedirectUrl('github', uid, '/entrar')

OTP_CODE_FIELD is the name of the six-digit-code input the server reads on the otpVerify step. Import it instead of typing the literal, so the screen and your tests cannot disagree.

CanPermission

Renders its children only when the authz endpoint grants permission (optionally on a resource), via POST <canPath>. Built on the useCan hook, which caches and dedupes the answers.

This gates on database-backed permissions, which need a server round-trip — use it for decisions that depend on the resource ("may they edit this post?"). For the global roles already carried in the auth shared-prop, useAuth().hasGlobalRole(...) answers synchronously, with no request at all.

PropTypeDefaultDescription
permissionstringThe permission to check, e.g. posts.update.
resourcestring?Optional resource the permission is evaluated against.
childrenReactNodeRendered when allowed.
loadingFallbackReactNodenullRendered while the check is in flight.
fallbackReactNodenullRendered when denied.
<CanPermission
  permission="posts.update"
  resource={post.id}
  loadingFallback={<Spinner />}
  fallback={<ReadOnlyBadge />}
>
  <EditPostButton post={post} />
</CanPermission>

The gate is fail-closed: if the check cannot be made, children are not rendered. Rendering fallback (rather than nothing) while a decision is pending is a common mistake — that is what loadingFallback is for.

KeyRotation

An admin panel for the JWKS signing key: current key age, the rotation policy and ETA of the next rotation, and a Rotate now button (with an option to retire old keys immediately). It reads and mutates through the admin query hooks, so it must live inside an AuthkitClientProvider and a QueryClientProvider — the admin console already provides both.

PropTypeDefaultDescription
classNamestringAppended to authkit-card authkit-keys.
import { KeyRotation } from '@adonis-agora/authkit-react'

function KeysPanel() {
  return <KeyRotation />
}

See Signing key rotation for what rotation does server-side and the equivalent authkit:keys:rotate command.

Theming

Components carry static authkit-* class names and read their colors and metrics from CSS variables (Adonis-violet defaults). Override the variables — typically on :root — to theme every component at once. These are the variables styles.css defines:

:root {
  --authkit-primary: #5a45ff;
  --authkit-primary-contrast: #ffffff;
  --authkit-danger: #e5484d;
  --authkit-fg: #1a1523;
  --authkit-muted: #6f6e77;
  --authkit-bg: #ffffff;
  --authkit-border: #e4e2e8;
  --authkit-radius: 8px;
  --authkit-gap: 0.5rem;
}

You can also target the class names directly (.authkit-button, .authkit-card, .authkit-userbutton__menu, .authkit-apps__item, …) for finer control, but the variables cover the common cases.

For the provider, useAuth(), headless hooks, and the role helpers these components are built on, see React.

On this page