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.
When to use
Section titled “When to use”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. |
Behaviour
Section titled “Behaviour”- 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 firesonActiveOrganizationChange. 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+onAppearancePreferenceChangeand 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.
Applying the appearance preference
Section titled “Applying the appearance preference”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.
Theming
Section titled “Theming”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.
TypeScript
Section titled “TypeScript”import type { UserProfileProps } from '@torii-js/torii-react';