iOS Android
Preview AvatarSelect on your device
Download Expo Go, then scan the QR code to preview native components.
pnpm dlx @docyrus/cli add @docyrus/rn-avatar-select
Required Packages (3 packages) pnpm add @shopify/flash-list expo-image-picker (optional) expo-image-manipulator (optional)
The Image tab needs the optional peer expo-image-picker (npx expo install expo-image-picker); without it the tab shows an install hint. imageCrop="editor" additionally uses the native ImageEditor , which needs expo-image-manipulator to apply the crop.
import { AvatarSelect , type AvatarFieldValue } from '@/components/docyrus-native/avatar-select' ;
const [ avatar , setAvatar ] = useState < AvatarFieldValue >({ icon : null , color : null , image : null });
< AvatarSelect
value = { avatar }
onChange = { ( value , payload ) => setAvatar ( value ) }
/>
Records often store the avatar in three columns. The mapping renames the keys of the payload passed to onChange / onCommit — the same contract as web.
import { extractAvatarValue } from '@/lib/docyrus/avatar-utils' ;
< AvatarSelect
value = { extractAvatarValue ( record , { iconField : 'avatar_icon' , colorField : 'avatar_color' , imageField : 'avatar_image' }) }
iconField = "avatar_icon"
colorField = "avatar_color"
imageField = "avatar_image"
onCommit = { ( value , payload ) => updateRecord ( record . id , payload ) }
// payload → { avatar_icon: 'fal rocket', avatar_color: 'indigo-500', avatar_image: null }
/>
uploadImage receives the picked (and cropped) file and returns the stored value. Without it the local file URI is stored as image.signed_url.
< AvatarSelect
value = { avatar }
imageCrop = "editor"
uploadImage = { async ( asset ) => {
const stored = await uploadFile ( asset . uri , asset . fileName , asset . mimeType );
return { id : stored . id , file_name : stored . name , signed_url : stored . url , file_type : asset . mimeType };
} }
onChange = { setAvatar }
/>
imageCrop="system" (default): the OS picker's own square crop (allowsEditing: true, aspect: [1, 1]).
imageCrop="editor": picks without cropping, then shows the native ImageEditor with a 1:1 stencil.
< AvatarSelect value = { avatar } editorDisplay = "inline" size = { 48 } onChange = { setAvatar } />
Tab Emits Icon + Color — pick an icon { icon, color: current color ?? 'indigo-500', image: null }Icon + Color — pick a colour { color, icon: current icon ?? first featured icon, image: null }Emoji { icon: emoji, color: null, image: null }Image { icon: null, color: null, image: uploaded value }Clear (native only) { icon: null, color: null, image: null }
The colour palette is TAILWIND_AVATAR_COLORS — all 22 Tailwind families at levels 200 and 500 ('indigo-500', 'rose-200', …), identical to web, so values round-trip between platforms.
The icon library switch covers Font Awesome (fal *) and Huge Icons (huge *). An empty search shows the featured set; a search matches up to 120 icons, rendered in a 6-column FlashList grid.
The full icon catalog (lib/icon-libraries, ~11k names) is loaded lazily the first time the Icon tab is shown, so importing AvatarSelect does not parse it at start-up.
The emoji list is native-only and searchable by keyword; it starts with web's emoji set.
The initial tab is derived from the value: image → Image, emoji → Emoji, otherwise Icon + Color.
Prop Type Default Description valuePartial<AvatarFieldValue> | null— Current avatar value (normalised with normalizeAvatarValue). sizenumber40Thumbnail size in points (web uses Tailwind spacing units). editorDisplay'inline' | 'popover''popover'inline renders the editor in place; popover opens it in an ActionSheet from the thumbnail.iconFieldstring | null'icon'Payload key for the icon. colorFieldstring | null'color'Payload key for the colour. imageFieldstring | null'image'Payload key for the image. uploadImage(asset: AvatarUploadAsset) => Promise<AvatarImageValue>— Uploads the picked / cropped image and returns the stored value. Without it the local URI is used. imageCrop'system' | 'editor''system'Crop with the OS picker UI or the native ImageEditor. disabledbooleanfalseDisables the trigger and every control. onChange(value: AvatarFieldValue, payload: Record<string, unknown>) => void— Fired on every change with the value and the field-mapped payload. onCommit(value: AvatarFieldValue, payload: Record<string, unknown>) => void— Fired alongside onChange — persist here. classNamestring— Additional classes for the trigger (popover) or the root (inline).
Property Type Description iconstring | nullIcon string ('fal rocket', 'huge star') or an emoji. colorstring | nullTailwind colour name ('indigo-500'), resolved with resolveColorHex. imageAvatarImageValue | nullUploaded image.
Property Type Description idstringFile identifier. file_namestringFile name. signed_urlstring | nullURL used to display the image. file_typestringMIME type. file_sizenumberSize in bytes. sourcestringStorage source. [key: string]unknownAny extra storage fields.
Property Type Description uristringLocal file URI. fileNamestringFile name (<name>-cropped.png after an editor crop). mimeTypestringMIME type. widthnumberWidth in pixels. heightnumberHeight in pixels. fileSizenumber | undefinedSize in bytes, when known.
lib/avatar-utils (installed with the component) is synced with web:
Export Description resolveAvatarFieldMapping(mapping?)Fills missing keys with DEFAULT_AVATAR_FIELDS. normalizeAvatarValue(value?)Trims strings to null, keeps only image-like objects. extractAvatarValue(record, mapping?)Reads a value out of a record using the mapping. buildAvatarPayload(value, mapping?){ [iconField], [colorField], [imageField] }.DEFAULT_AVATAR_FIELDS / EMPTY_AVATAR_VALUEDefaults. TAILWIND_AVATAR_COLORS / TAILWIND_AVATAR_COLOR_LEVELSPalette (22 families × 200 / 500). TAILWIND_COLOR_FAMILIES / TAILWIND_HEXFull Tailwind v4 palette. getTailwindColorLevel(color)'indigo-500' → 500.resolveColorHex(color) / resolveColorCssValue(color)Tailwind name → hex (unknown values pass through). isEmojiIcon(value) / getReadableTextColor(hex)Helpers.
The AI tab is not ported (it is disabled "coming soon" on web too).
size is in points, not Tailwind spacing units.
editorDisplay defaults to 'popover' (an ActionSheet — native's behaviour before the port); web defaults to 'inline'.
uploadImage receives an AvatarUploadAsset instead of a File.
Native adds a Clear action and a searchable emoji list.
ui.common.changeAvatar, ui.common.searchIconPlaceholder, ui.common.noIconsFound, ui.common.color (web keys) plus ui.avatarSelect.title, ui.avatarSelect.iconTab, ui.avatarSelect.emojiTab, ui.avatarSelect.imageTab, ui.avatarSelect.fontAwesome, ui.avatarSelect.hugeIcons, ui.avatarSelect.pickColor, ui.avatarSelect.searchEmojiPlaceholder, ui.avatarSelect.noEmojiFound, ui.avatarSelect.uploadAndCrop, ui.avatarSelect.uploadHint, ui.avatarSelect.pickerMissing, ui.avatarSelect.uploadFailed, ui.avatarSelect.squareAvatar, ui.avatarSelect.clear, ui.avatarSelect.done.
Type Description AvatarSelectPropsProps for the AvatarSelect component. AvatarFieldValueAvatar value (icon, color, image). AvatarImageValueImage file value. AvatarFieldMapping{ iconField?, colorField?, imageField? }.AvatarUploadAssetAsset passed to uploadImage (alias of ImageEditorAsset). AvatarSelectTab'icon' | 'emoji' | 'image'.AvatarSelectEditorDisplay'inline' | 'popover'.AvatarSelectImageCrop'system' | 'editor'.