Skip to content

UserProfile

<UserProfile> is the full self-service account widget. It renders a card with a section sidebar covering the common “manage my account” surfaces:

  • Profile: edit name and profile details, preferred language (shown when more than one is configured), email addresses (add → verify → set-primary), and link/unlink OAuth connected accounts
  • Security: change password, set up two-factor authentication, view and revoke active devices, and delete the account
  • Organizations: the organizations the user belongs to, their role in each, which one is active, and the actions that are theirs to take (make another one active, leave one). Hidden entirely for a user with no memberships, so single-tenant apps never see it
  • Privacy: GDPR data export request
  • Preferences: appearance (only when an appearance preference is wired up; hidden otherwise)

Everything is read off useAuth() and the SDK’s data hooks, so you don’t wire any fetching yourself.

The card can render two ways via the presentation prop: inline (the default, on a dedicated account page) or in a modal. Most apps never set this directly: <UserButton> opens the modal for you (userProfileMode="modal"). Drive presentation yourself only when you have a custom trigger.

Drop it inside <SignedIn> on an account/settings route. For a heading-and-layout version that embeds a <UserButton> too, use <UserDashboard>.

Inline, on an account page:

import { SignedIn, UserProfile } from '@torii-js/torii-react';
function AccountPage() {
return (
<SignedIn>
<UserProfile onUpdateSuccess={(p) => console.log('saved', p)} />
</SignedIn>
);
}

As a modal behind your own trigger:

const [open, setOpen] = useState(false);
<button onClick={() => setOpen(true)}>Account</button>
<UserProfile presentation="modal" open={open} onClose={() => setOpen(false)} />
Prop Type Default Description
presentation 'inline' | 'modal' 'inline' 'inline' renders the bare card; 'modal' wraps it in a dismissible dialog.
open boolean - Dialog visibility. Only used when presentation="modal".
onClose () => void - Called when the modal requests close (backdrop click, Escape, or the ✕ button).
showLanguage boolean auto Show the Language row on the Profile section. Defaults to auto, shown when <ToriiProvider> has more than one languages entry. Pass false to hide it, true to force it.
appearancePreference 'light' | 'dark' | 'system' - Current appearance (theme) choice. Together with onAppearancePreferenceChange, adds a Preferences section with an Appearance selector. Persistence is yours: store it per device (e.g. localStorage) or per user.
onAppearancePreferenceChange (value) => void - Called when the user picks a different appearance.
onActiveOrganizationChange (organizationId: string) => void - Fired after the Organizations section switches the active organization (server switch + token re-mint done). Needed when your routes are organization-scoped: the switch can happen inside the modal, leaving you on a route that belonged to the previous organization.
onOrganizationLeft (organizationId: string) => void - Fired after the Organizations section leaves an organization. Same reasoning as above.
onUpdateSuccess (profile: UserProfileData) => void - Fired after a successful profile update.
onUpdateError (error: ToriiError) => void - Fired on a profile-update error. Branch on error.code; show error.message.
labels Partial<ToriiSignupLabels> - Per-instance label overrides. Falls through to ToriiProvider labels.
className string - Extra class names for the card.
  • Progressive disclosure: each row renders collapsed (a summary plus a single accent action like “Update profile” / “+ Add email address”); clicking the action expands an inline form card in place with Cancel/confirm. Per-item actions (make primary, resend verification, remove, unlink, sign out of device) live in a “⋯” menu on the item row.
  • Sign out of all other devices: the update-password card includes a checkbox (checked by default) that revokes every other session after a successful password change.
  • Requires a provider: reads profile, sessions, identities, and email addresses from <ToriiProvider> context.
  • Email addresses: the Profile section includes an email-address manager (wrapping useEmailAddresses). Users add an address, verify it via the emailed link, then set it primary; any verified address can sign in. The primary address can’t be removed.
  • OAuth link errors: if the user returns from a failed connected-account link (error captured from the URL fragment), the card opens the Profile section so the alert isn’t missed.
  • Privacy section: hosts the GDPR data export request.
  • Organizations section: lists the memberships already on the session payload, so opening it costs no extra request. Choosing another organization calls the same switch as <OrganizationSwitcher> (re-mints the access token with the new active organization) and fires onActiveOrganizationChange. Leave asks for confirmation first, and a refusal from the server (for example, the last administrator of an organization cannot leave it) is shown inline under the list rather than pre-disabling the button: whether the user is the last administrator is not something the client can know without asking.
  • Language row: appears on the Profile section when the provider has more than one language configured (or showLanguage={true}). The preference is synced to the user (user.locale), so it follows them across devices. The selector applies immediately: it switches the live UI language and persists the preference, with no Save step.
  • Appearance preference: pass appearancePreference + onAppearancePreferenceChange and a Preferences section appears with an Appearance selector (System / Light / Dark). Appearance is device-local by design: the component only renders the control; applying and persisting the theme is your app’s job. See applying the appearance preference. <UserDashboard> forwards both props, so the drop-in account page gets the same section.
  • Modal: locks body scroll, traps Tab focus, closes on Escape / backdrop click / ✕, and restores focus to the opener on close. Honours prefers-reduced-motion.

The selector hands you a value and stops there. Applying it is two lines, because each theme preset ships a light and a dark token set and the SDK scopes the dark one to a .dark ancestor (the shadcn / Tailwind convention): put dark on <html> and every Torii surface follows, along with your own chrome if it uses the same class.

import { useEffect, useState } from 'react';
import { SignedIn, UserProfile } from '@torii-js/torii-react';
type Appearance = 'light' | 'dark' | 'system';
function prefersDark() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
export function AccountPage() {
const [appearance, setAppearance] = useState<Appearance>(
() => (localStorage.getItem('appearance') as Appearance) ?? 'system',
);
useEffect(() => {
const dark = appearance === 'system' ? prefersDark() : appearance === 'dark';
document.documentElement.classList.toggle('dark', dark);
document.documentElement.style.colorScheme = dark ? 'dark' : 'light';
localStorage.setItem('appearance', appearance);
}, [appearance]);
return (
<SignedIn>
<UserProfile
appearancePreference={appearance}
onAppearancePreferenceChange={setAppearance}
/>
</SignedIn>
);
}

Store it per device (localStorage, as above) rather than on the user: a preference tied to the account would follow someone from their dark laptop to a shared light-mode machine. Add a matchMedia change listener if you want system to track the OS setting live rather than only at mount.

Honours the appearance prop on <ToriiProvider>. In modal mode the backdrop, panel, and close button expose the modalBackdrop, modalContent, and modalCloseButton element slots. See theming and elements.

import type { UserProfileProps } from '@torii-js/torii-react';