Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Inputs

Avatar Upload

Inputs

Avatar Upload React component for Tailwind CSS v4

Toggle GroupCalendar
Live props
Content
name
label
description
error
showHint
Appearance
variant
size
shape
tone
color
ringColor
State
required
invalid
disabled
readOnly
Behavior
crop
camera
accept
maxFileSize10485760 B
outputSize512px
outputType
outputQuality0.9
responsive
locale
inputName
Code
import { AvatarUpload } from "@/components/ui/avatar-upload"

<AvatarUpload />
  1. Home
  2. Components
  3. Inputs
  4. Avatar Upload
Pro

Avatar Upload React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React avatar upload for profile pictures: pick or shoot a photo, crop it in a round frame and watch a ring fill while it uploads.

Tap or click the avatar (it dips on a quick spring; with a mouse or on keyboard focus a dark scrim says Change photo) to open the source menu: Choose a photo, Take photo and Remove photo, a bottom sheet on phones. With only one entry it goes straight to the file picker. After picking, the crop step opens in a dialog (a sheet on phones) with a round frame: drag to move, pinch or scroll to zoom, the zoom and rotation sliders and a 90° turn below, then Save exports a square WebP at 512px. The new picture crossfades in at once while a ring around the avatar fills with the upload progress (it spins when the server reports none); when it finishes a green check pops over the camera badge for a moment and Photo updated is announced. If the upload fails the old picture comes back, the ring turns red, the badge becomes a retry arrow, the error shows under the avatar and Try again sits at the top of the menu. Remove runs at once (a returned promise shows the spinning ring and rolls back on rejection). Drag a photo onto the avatar or the dropzone (it grows a little and turns the tone color), or paste an image while it is focused. Take photo uses the front camera on phones; on desktops it opens a mirrored live camera with a round shutter, and a refused permission shows a message with Try again and Choose a photo. Keyboard: Tab to the avatar, Enter or Space opens the menu (arrows move, Escape closes and focus returns), Delete or Backspace removes the picture, and inside the crop step arrows nudge, + and - zoom and r rotates. The row look puts Change photo and Remove buttons with the hint beside the avatar; the dropzone look is a dashed tile with the avatar inside. Read-only shows the picture without a badge or menu.

Avatar Upload is a Pro React component in the React input components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add avatar-upload, together with Image Cropper, Avatar, Button, Dialog, Dropdown Menu and 12 more and the npm packages motion and lucide-react.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add avatar-upload

Avatar Upload is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import AvatarUpload from @/components/ui/avatar-upload.

import { useState } from "react"
import { AvatarUpload } from "@/components/ui/avatar-upload"

export function ProfilePhoto() {
  const [avatar, setAvatar] = useState<string | null>("/demo/ana-pop.jpg")

  return (
    <AvatarUpload
      name="Anna Price"
      value={avatar}
      onValueChange={setAvatar}
      onUpload={async (blob, { signal, onProgress }) => {
        const body = new FormData()
        body.append("avatar", blob, "avatar.webp")
        onProgress(0.3)
        const response = await fetch("/api/profile/avatar", { method: "POST", body, signal })
        if (!response.ok) throw new Error("The upload failed")
        onProgress(1)
        const { url } = (await response.json()) as { url: string }
        return url
      }}
      onRemove={() => fetch("/api/profile/avatar", { method: "DELETE" })}
      labels={{
        change: "Update photo",
        cropTitle: "Adjust your photo",
        uploaded: "Looking good! Photo updated",
      }}
    />
  )
}

Props

  • value
    string | null

    The controlled picture url; null shows the initials. It becomes the url your onUpload resolves, or a local object url when it resolves nothing.

  • defaultValuedefault "null"
    string | null

    The uncontrolled starting picture (a url); null shows the initials. The playground is controlled through value instead.

  • onValueChange
    (value: string | null) => void

    Fires with the new url after an upload succeeds, with null on remove, and with the old url when a remove is rolled back.

  • namedefault ""
    string

    The person's name: initials and a stable color while there is no picture, and part of the button's accessible name (Change photo, Anna Price).

  • onUpload
    (blob: Blob, context: { file: File | null; signal: AbortSignal; onProgress: (fraction: number) => void }) => Promise<string | void>

    Gets the cropped square. Call onProgress(0 to 1) to fill the ring (without calls it spins) and resolve the new url. A rejection rolls the picture back and offers Try again. The signal aborts when the component unmounts or a newer upload starts; a result that arrives after the value changed from outside is ignored. Without onUpload the blob becomes a local object url.

  • onRemove
    () => (void | Promise<unknown>)

    The Remove action, shown only when set and there is a picture. It runs at once without a confirm (offer undo in a toast); a returned promise shows the spinning ring and rolls back on rejection.

  • onError
    (error: Error) => void

    A wrong file type, a file over maxFileSize, a failed export, upload or remove.

  • variantdefault "avatar"
    "avatar" | "row" | "dropzone"

    avatar: the picture with a camera badge, the whole avatar is the button. row: the picture with Change photo and Remove buttons and the hint beside it, for settings lists. dropzone: a dashed tile with the avatar inside, for logos and onboarding.

  • sizedefault "lg"
    "md" | "lg" | "xl"

    The avatar: 64 / 96 / 128px. The badge and the check scale with it (24 / 32 / 36px, a 44px hit area on touch).

  • shapedefault "circle"
    "circle" | "rounded"

    The avatar and the crop frame. circle exports with transparent corners (unless JPEG); rounded suits logos and crops a plain square.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The camera badge, the progress ring, the drag-over tint and the Save button. A failed upload always turns the ring red.

  • colordefault ""
    string

    Any CSS color; replaces the tone.

  • ringColordefault ""
    string

    The thin ring that separates the badge and the check from the avatar. Set it to the surface behind the avatar (the page background by default).

  • labeldefault ""
    ReactNode

    The field label above the control (W2 field anatomy).

  • descriptiondefault ""
    ReactNode

    Helper text under the control.

  • errordefault ""
    ReactNode

    Replaces the description, sets aria-invalid and marks the dropzone red.

  • showHintdefault true
    boolean

    The hint (JPG, PNG or WebP, up to 10 MB) under the buttons in the row look and inside the dropzone.

  • requireddefault false
    boolean

    Marks the label with an asterisk and sets aria-required.

  • invaliddefault false
    boolean

    Invalid without a message.

  • disableddefault false
    boolean

    Dims the control; no menu, no drops.

  • readOnlydefault false
    boolean

    Shows the picture without a badge, a menu or drops.

  • progress
    number | null

    External progress (0 to 1) for uploads you run yourself: the ring shows at that share, null spins it, undefined hides it. The playground's fake upload drives the ring itself.

  • cropdefault true
    boolean

    The crop step. false uploads the largest centered square at once, without the dialog.

  • cameradefault true
    boolean

    The Take photo entry: the front camera through the file input on phones (capture="user"), a live camera dialog on desktops with getUserMedia, hidden where neither works.

  • acceptdefault "image/jpeg,image/png,image/webp"
    string

    Allowed types for the picker, drops and pastes; other files get labels.wrongType. Safari converts iPhone HEIC photos to JPEG on pick when JPEG is listed; other browsers reject HEIC.

  • maxFileSizedefault 10485760
    number

    Bytes of the chosen file (10 MB); bigger files get labels.tooLarge with the limit formatted. 0 means no limit. The export itself is small.

  • outputSizedefault 512
    number

    Width and height of the exported square, in px.

  • outputTypedefault "image/webp"
    "image/webp" | "image/jpeg" | "image/png"

    The exported format. JPEG has no transparency, so a round crop fills the corners white.

  • outputQualitydefault 0.9
    number

    0 to 1, for WebP and JPEG.

  • responsivedefault true
    boolean

    The source menu, the crop step and the camera become bottom sheets below 768px.

  • localedefault ""
    string

    Formats the size limit in the error (10 MB). Empty uses UIProvider's locale, then the browser's.

  • inputNamedefault ""
    string

    Renders a hidden input with this name and the current url, for plain form posts. (name is the person's name here.)

  • id
    string

    The trigger's id; the label and the messages point at it.

  • cropperProps
    Partial<Omit<ImageCropperProps, "src">>

    Extra props for the crop step's ImageCropper, e.g. { labels } to translate it, { height: 360 } or { flippable: true }.

  • fieldProps
    Partial<FieldProps>

    Anything else for the Field wrapper: optional, labelAction, orientation, labelWidth, hideLabel.

  • labels
    Partial<AvatarUploadLabels>

    Every visible and announced string: change, choose, takePhoto, remove, cropTitle, cameraTitle, save, cancel, uploading, uploaded, removed, failed, removeFailed, tooLarge(max), wrongType, cameraDenied, cameraStarting, shutter, hint, retry, dropTitle, dropTitleTouch, dropHere. Pass only the keys you translate.

  • classNames
    Partial<Record<"root" | "trigger" | "avatar" | "badge" | "progress" | "check" | "actions" | "hint" | "message" | "dialog" | "camera" | "field" | "label" | "description" | "error" | "counter", string>>

    Extra classes for the inner parts.

  • className
    string

    Extra classes for the root (the Field when a label, description or error is set).

Parts

  • DEFAULT_AVATAR_UPLOAD_LABELS

    constant

    The English copy ("Change photo", "Crop your photo", "Take photo"...). Spread it into labels when you translate only a few keys.

  • AvatarUploadHandler

    type

    The onUpload signature: (blob, { file, signal, onProgress }) => Promise<string | void>. Resolve with the new url; call onProgress(0 to 1) to fill the ring (without calls it spins); signal aborts on unmount or a newer upload.

Examples

Row in a settings list

Change and Remove beside the avatar, with the hint under them.

Company logo dropzone

A rounded square exported as PNG; drag a file onto the tile.

Upload fails, then retry

The old picture comes back, the ring turns red and Try again tops the menu.

Quick upload without cropping

crop={false}: the centered square goes straight up.

Sizes

64, 96 and 128px; the badge and the check scale along.

Initials only

No picture and no onRemove: the menu has only the sources.

Required in a form

Field label, description and error, a brand tone and a hidden input.

Dependencies

motionlucide-reactimage-cropperavatarbuttondialogdropdown-menuprogressfieldfile-uploadfile-typesfile-exportuse-controllable-stateuse-localeuse-media-queryhapticsmotiontonesui-config

Files

  • components/ui/avatar-upload.tsx

FAQ

Is the React Avatar Upload component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Avatar Upload?

Run npx wingo-ui@latest add avatar-upload. It also adds the Wingo UI items it builds on (Image Cropper, Avatar, Button, Dialog, Dropdown Menu and 12 more) and the npm packages motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/avatar-upload.

How does Avatar Upload behave on phones?

The source menu, the crop step and the camera become bottom sheets below 768px. The responsive prop controls this and is on by default.

See also

AvatarImage CropperFile UploadFile exportlib

Used in

Onboarding FlowProfile PageSettings Page

More React input components

Color PickerCalendarSliderSwitchMasked InputSegmented Control
All 40 React input components

Articles using this component

  • Shadcn Profile Page in React: Follow, Tabs and Edit Sheet

    Build it: screens and apps · Nov 6, 2026 · 9 min read

  • React Image Cropper: Crop Before Upload, Zoom, Rotate

    Component guides · Oct 19, 2026 · 9 min read

PreviousToggle GroupNext Calendar
Profile

Your Summit CRM account

Anna PriceAdministrator · Summit Software Inc.
No export yetPress the avatar and pick a photo

WEBP 512px q0.9 · max 10 MB · crop · camera