Docyrus

AvatarSelect

Avatar picker with Icon + Color (Font Awesome / Huge Icons), Emoji and Image tabs, multi-column field mapping and a ready-to-save payload.

iOSAndroid
Preview AvatarSelect on your device

Scan with Expo Go

Download Expo Go, then scan the QR code to preview native components.

Installation

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.

Usage

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)}
/>

Field mapping + payload

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 }
/>

Uploading images

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.

Inline editor

<AvatarSelect value={avatar} editorDisplay="inline" size={48} onChange={setAvatar} />

Behaviour

TabEmits
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.

API Reference

PropTypeDefaultDescription
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).

AvatarFieldValue

PropertyTypeDescription
iconstring | nullIcon string ('fal rocket', 'huge star') or an emoji.
colorstring | nullTailwind colour name ('indigo-500'), resolved with resolveColorHex.
imageAvatarImageValue | nullUploaded image.

AvatarImageValue

PropertyTypeDescription
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.

AvatarUploadAsset

PropertyTypeDescription
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.

Avatar utilities

lib/avatar-utils (installed with the component) is synced with web:

ExportDescription
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.

Differences from web

  • 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.

i18n

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 Exports

TypeDescription
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'.

On this page