Profile
The Profile section of an AI product's settings. Crop a photo into a circle, pick a username that's checked as you type, confirm a new email with a 6-digit code, add links that recognize the site, and see a live card of how others see you.
ProfileSettings is the Profile page from a product like Claude or Cursor, built to drop into any SettingsShell section. Give it the saved profile and an onSave, and it handles the rest: a draft that only saves when you say so, field errors from your checks or your server, and the floating save bar from Settings with ⌘S.
At the top, a card shows how others see your profile and changes as you type: photo, name, handle, bio and links. Turn off Show my profile and the card dims and says only you can see it. Copy link puts the profile URL on the clipboard.
Pick a photo, or drop one on the row, and it opens in a crop dialog. Drag to move it, zoom with the slider, the wheel or a trackpad pinch, and the photo is cropped to a square in the browser before it reaches onSave as a data URL. Files of the wrong type, too large or under 128 pixels are refused in the row with the reason.
The username is checked as you type, without slowing typing. Checks wait 350ms after the last key, stale checks are cancelled through an AbortSignal, and names you already checked answer instantly. A taken name shows your suggestions as one-tap fixes. If you save before a check finishes, the save waits for it.
Email isn't part of the draft. Change opens a two-step dialog: enter the new address, then the 6-digit code sent to it, which checks itself when the last digit goes in. A wrong code shakes and clears, Resend unlocks after 30 seconds, and the row updates once the code is confirmed. An unverified address shows Resend link.
Links take whatever people paste: github.com/you, a full URL or a www address. Known sites get their icon, anything else its first letter. On save they're normalized to https URLs and empty ones are dropped. Addresses that aren't links or repeat another are marked one by one.
Add the Pro registry to components.json
components.json Add your token
Create a token on your account page and put it in
.env.localasHEXTAUI_PRO_TOKEN.Add the block
pnpm dlx shadcn@latest add @hextaui-pro/profile
Wire it to your API
Pass the saved profile and an onSave that returns field errors or throws. checkUsername gets an AbortSignal, so stale checks are cancelled as people type.
Change and verify email
Add onRequestEmailCode and onVerifyEmailCode to turn on the Change button. Return a message from onRequestEmailCode to refuse an address, and false from onVerifyEmailCode for a wrong code.
Anatomy
The parts you compose, from the outside in.
| Part | Description |
|---|---|
ProfileSettings | The section: preview card, photo, name, what the assistant calls you, username, bio, public profile, links and email. |
ProfileCard | The live preview of the public profile, with Copy link. Usable on its own. |
AvatarCropDialog | Crops an image to a square with drag, zoom and keyboard, and returns a data URL. |
ChangeEmailDialog | The two-step email change: new address, then a 6-digit code. |
useUsernameCheck | Debounced, cancellable, cached availability checks for a username. |
ProfileSettings
Use inside a SettingsSection.
| Prop | Type | Default |
|---|---|---|
profileWhat's saved now: { avatar, name, nickname, username, bio, links, public }. When it changes and there are no edits, the draft follows it. | ProfileValues | – |
onSaveCalled with trimmed values and normalized links. Return { field: message } for field errors, or throw to show the message in the save bar. | (values) => void | errors | Promise<void | errors> | – |
emailThe sign-in address. | string | – |
emailVerifiedShows Verified, or Not verified with Resend link. | boolean | true |
checkUsernameChecks if a username is free. Gets the lowercased name and an AbortSignal. Without it, only the format is checked. | (username, signal) => Promise<{ available, message?, suggestions? }> | – |
onRequestEmailCodeSends a code to a new address. Return a message to refuse the address. With onVerifyEmailCode, turns on Change. | (email) => Promise<void | string> | – |
onVerifyEmailCodeChecks the code. Return false for a wrong code, or throw to show your message. | (email, code) => Promise<boolean> | – |
onEmailChangeCalled once the new address is confirmed. | (email) => void | – |
onResendVerificationAdds Resend link while the address is unverified. | () => Promise<unknown> | – |
profileUrlThe public profile URL, shown under the username and copied by Copy link. | (username) => string | https://hexta.app/@username |
assistantNameUsed in “What should Hexta call you?”. | string | "Hexta" |
maxAvatarBytesLargest photo accepted. | number | 5 MB |
maxLinksHow many links a profile can have. | number | 5 |
maxBioBio length, with a counter. | number | 160 |
| Prop | Type | Default |
|---|---|---|
profileWhat to show. | { avatar, name, username, bio, links, public } | – |
urlThe profile URL to show and copy. | string | – |
onCopiedCalled after the link is copied. | () => void | – |
| Prop | Type | Default |
|---|---|---|
sourceThe image to crop. Open while set. | { url, width, height } | null | – |
onOpenChangeClear source when it closes. | (open) => void | – |
onApplyGets the cropped square as WebP, or PNG where WebP isn't supported. | (dataUrl) => void | – |
outputSizeLargest side of the output in pixels. Never upscales past the crop. | number | 512 |
maxZoomHow far you can zoom in. | number | 4 |
| Prop | Type | Default |
|---|---|---|
openWhether it's open. | boolean | – |
currentEmailShown in the intro and refused as the new address. | string | – |
onRequestCodeSends the code. Return a message to refuse. | (email) => Promise<void | string> | – |
onVerifyCodeChecks the code. | (email, code) => Promise<boolean> | – |
onChangedCalled after the code is confirmed, before the dialog closes. | (email) => void | – |
resendAfterSeconds before Resend code unlocks. | number | 30 |
useUsernameCheck(username, options)
Returns { status, result, problem, retry, resolve }. status is idle, invalid, checking, available, taken or error.
| Prop | Type | Default |
|---|---|---|
currentThe saved username, which is never checked. | string | – |
checkYour availability check. | (username, signal) => Promise<UsernameCheck> | – |
delayMilliseconds to wait after typing stops. | number | 350 |
| Key | Action |
|---|---|
| Enter | In the last link, adds another link and moves to it. |
| ←→↑↓ | In the crop dialog, move the photo. Shift moves farther. |
| +− | In the crop dialog, zoom in and out. 0 resets. |
| ⌘S | Saves while something is unsaved. Ctrl+S on Windows and Linux. |
| Esc | Closes a dialog and returns focus to the button that opened it. |
- Every field is named by its row, and buttons that repeat a word say what they act on: Upload photo, Change email, Remove link 2, github.com/you.
- Username results, copied links, photo changes, removed links and a changed email are announced politely. Checking itself isn't announced, so typing stays quiet.
- Field errors mark only the field at fault, and focus moves to the first one when a save is refused. The email code announces checking, confirmed and wrong.
- Removing a link moves focus to the next link, or to Add link when none are left. Removing the photo moves focus to Upload.
- The crop area is focusable and works with arrow keys and plus and minus, and the zoom slider says its value as a percentage.
- On touch screens, inputs use 16px text so iOS doesn't zoom, and icon buttons have 44px targets.
Code
6 files, added to components/blocks/profile.