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

Media

Image Cropper

Media

Image Cropper React component for Tailwind CSS v4

Image compareImage Gallery
Live props
Appearance
cropShape
showGrid
overlayColor
size
tone
color
radius
Behavior
minZoom1×
maxZoom4×
zoomStep0.1×
rotatable
flippable
restrictPosition
Layout
objectFit
height360px
toolbar
State
disabled
Code
import { ImageCropper } from "@/components/ui/image-cropper"

<ImageCropper />
  1. Home
  2. Components
  3. Media
  4. Image Cropper
Pro

Image Cropper React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React image cropper for photos before upload: aspect presets, pinch or wheel zoom, rotation, a thirds grid and a JPEG, PNG or WebP export.

Drag the photo with a finger or the mouse to move it under the frame; pinch with two fingers or turn the mouse wheel to zoom, and the image always keeps covering the frame. While you move it a rule-of-thirds grid fades in and a small pill on the stage shows the zoom and the angle; a first-run hint says how to move and zoom and disappears after the first touch. Pick a ratio (Free, 1:1, 4:3, 3:4, 16:9, 9:16) from the segmented switch on wide layouts or the scrolling chip rail on phones, where every chip is a 44px target. In Free mode the frame starts at the photo's own ratio and its four corner brackets resize it with a drag (the frame stays centered). The zoom row has minus and plus buttons that glide the zoom on a short tween around a slider; the rotation row has a 90° left turn that animates, a slider from −45° to 45° with a center mark for straightening, and an optional horizontal flip that turns like a card. Reset puts position, zoom, rotation and flip back. Tab to the crop frame (it shows a white focus ring): arrow keys move the photo by 10px (Shift+arrows by 2px for fine nudges), + and − zoom, r turns 90° left, f flips when flippable, 0 resets and Escape cancels a corner drag. The sliders take arrows, Home and End, and read their value as 150% or −12°; aspect changes, turns, flips and resets are announced. On phones the stage caps at 60% of the screen height and the toolbar stacks. The export crops in a canvas with the rotation and flip, steps down large photos in halves for a sharp result, and can mask a circle with transparent corners.

Image Cropper is a Pro React component in the React media components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add image-cropper, together with Button, Slider, Segmented Control, Tooltip, Skeleton and 6 more and the npm packages react-easy-crop, 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 image-cropper

Image Cropper 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 ImageCropper from @/components/ui/image-cropper.

import { useState } from "react"
import { Button } from "@/components/ui/button"
import { ImageCropper, type ImageCropperApi } from "@/components/ui/image-cropper"

export function ListingPhoto({ onSave }: { onSave: (file: Blob) => void }) {
  const [api, setApi] = useState<ImageCropperApi | null>(null)
  const [saving, setSaving] = useState(false)

  const crop = async () => {
    if (!api) return
    setSaving(true)
    try {
      onSave(await api.toBlob({ type: "image/jpeg", quality: 0.9, maxSize: 1600 }))
    } finally {
      setSaving(false)
    }
  }

  return (
    <div className="flex max-w-xl flex-col gap-4">
      <ImageCropper
        src="/demo/apartment.jpg"
        defaultAspect={4 / 3}
        setApi={setApi}
        labels={{
          aspect: "Ratio",
          hint: "Drag to move, scroll to zoom",
          touchHint: "Drag to move, pinch to zoom",
        }}
      />
      <Button loading={saving} onClick={crop}>
        Crop
      </Button>
    </div>
  )
}

Props

  • src*
    string | Blob | File

    The photo: a URL, a data URL or a File / Blob from an input (its object URL is created and revoked for you). Modern browsers apply EXIF orientation to both the preview and the export.

  • value
    CropValue ({ area: CropArea; zoom: number; rotation: number; aspect: number | null; flipH?: boolean })

    The controlled crop. area is { x, y, width, height } in source pixels of the rotated photo. A value from outside that differs from the last one reported restores that exact crop.

  • defaultValue
    CropValue

    The uncontrolled start, for example a crop saved earlier. Without it the frame starts centered at the largest area of the ratio.

  • onValueChange
    (value: CropValue) => void

    Fires when an interaction ends (a drag, a pinch, the wheel settling, a slider released, a key released, a tween finished), never per frame. Also once when the photo is ready, with the starting crop.

  • onCropChange
    (value: CropValue) => void

    Every frame while the crop moves. Rarely needed; prefer onValueChange.

  • aspect
    number | null

    The controlled ratio (width / height). null is a free crop that starts at the photo's own ratio and has corner handles.

  • defaultAspectdefault "null"
    number | null

    The uncontrolled starting ratio, e.g. 4 / 3 for a listing photo.

  • onAspectChange
    (aspect: number | null) => void

    A ratio was picked from the presets.

  • aspectPresetsdefault "DEFAULT_ASPECT_PRESETS"
    AspectPreset[] | false ({ key: string; label: ReactNode; value: number | null; icon?: ReactNode })

    The ratios offered: Free, 1:1, 4:3, 3:4, 16:9, 9:16 by default, each with a small outline of its shape. false hides the picker (a fixed ratio). The default Free label follows labels.free. Exported as DEFAULT_ASPECT_PRESETS, so you can spread it and add your own.

  • cropShapedefault "rect"
    "rect" | "round"

    round draws a circular frame for avatars and logos, forces the ratio to 1 and hides the presets; the export masks the circle.

  • showGriddefault "interacting"
    "always" | "interacting" | "never"

    The rule-of-thirds grid inside the frame. interacting fades it in while you drag, pinch, zoom or rotate.

  • minZoomdefault 1
    number

    The smallest zoom. At 1 the photo just fits the stage. With a resized free frame the zoom also stops where the photo would stop covering it.

  • maxZoomdefault 4
    number

    The largest zoom for the slider, the buttons, the wheel and pinch.

  • zoomStepdefault 0.1
    number

    The slider step and the + / − key step. The zoom buttons move five steps with a short glide.

  • rotatabledefault true
    boolean

    Shows the rotation row: a 90° left turn and a fine slider from −45° to 45° with a center mark.

  • flippabledefault false
    boolean

    Adds a horizontal flip button (and the f key). The export mirrors the photo too.

  • restrictPositiondefault true
    boolean

    Keeps the photo covering the whole frame. Off, the photo can move past the frame and the uncovered part exports as the background.

  • objectFitdefault "contain"
    "contain" | "cover"

    How the photo sits in the stage at zoom 1: contain shows all of it, cover fills the stage.

  • heightdefault 360
    number

    The stage height in px. Phones cap it at 60dvh so the toolbar stays on screen.

  • toolbardefault true
    boolean

    The controls under the stage. Off, the photo still moves, pinches, zooms with the wheel and answers the keys; drive the rest through setApi.

  • exportdefault "{ type: "image/jpeg", quality: 0.9, maxSize: 2048 }"
    CropExportOptions ({ type?: ImageType; quality?: number; maxSize?: number; width?: number; height?: number; background?: string; round?: boolean })

    Defaults for toBlob / toCanvas. maxSize caps the longest side (never upscales), width / height set an exact size, round masks a circle (on by default for a round crop), background fills behind (JPEG gets white). cropImage (see Parts) runs the same export without the component.

  • overlayColordefault "rgba(0, 0, 0, 0.55)"
    string

    The dim outside the frame. Any CSS color.

  • sizedefault "md"
    "sm" | "md"

    The toolbar controls: sm 32px buttons and slim sliders, md 40px (44px on touch).

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

    The slider fills, the selected ratio chip and the segmented thumb.

  • colordefault ""
    string

    Any CSS color; replaces the tone for the sliders and the selected ratio.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg"

    The stage corners: 0 / 12 / 16 / 24px.

  • disableddefault false
    boolean

    Dims the stage and the toolbar and ignores every gesture and key.

  • setApi
    (api: ImageCropperApi) => void

    Receives { toBlob(options?), toCanvas(options?), reset(), rotateBy(degrees), zoomTo(zoom) } after mount. toBlob rejects with labels.corsError when a remote photo has no CORS headers (the preview still works).

  • onError
    (error: Error) => void

    The photo failed to load, or an export failed. Load errors also show a retry on the stage for URLs.

  • labelsdefault "DEFAULT_IMAGE_CROPPER_LABELS"
    Partial<ImageCropperLabels>

    Every string: aspect, free, zoom, zoomIn, zoomOut, rotate, rotateLeft, flip, reset, area (the frame's name for screen readers), loading, error, retry, hint, touchHint, corsError, exportError, and the announcements aspectChanged(label), rotated(degrees, formatted), flipped, resetDone. English defaults in DEFAULT_IMAGE_CROPPER_LABELS.

  • classNames
    Partial<Record<"stage" | "area" | "grid" | "handle" | "toolbar" | "aspects" | "zoom" | "rotate" | "reset" | "pill", string>>

    Extra classes for the inner parts.

  • className
    string

    Extra classes for the root, merged last.

Parts

  • cropImage

    function

    Runs a saved crop without the component, e.g. on submit or in avatar-upload: rotation and flip aware, two-step downscaling for large photos, and a round mask with transparent PNG or WebP corners. Browser only.

    • source*
      string | Blob

      The image URL or file.

    • value*
      CropValue

      The crop from onValueChange: area, zoom, rotation, aspect, flipH.

    • options
      CropExportOptions

      type, quality, maxSize, width, height, background, round.

    • returns
      Promise<Blob>

      The cropped image, ready to upload.

  • DEFAULT_ASPECT_PRESETS

    constant

    The ratios the picker offers: Free, 1:1, 4:3, 3:4, 16:9 and 9:16. Spread it and add your own { key, label, value }.

  • DEFAULT_IMAGE_CROPPER_LABELS

    constant

    Every English string: toolbar buttons, hints, loading and error states, and the screen reader announcements. Spread it to translate.

  • CropValue

    type

    Everything needed to redo a crop later: { area, zoom, rotation, aspect, flipH? }. Store it, pass it back as value or to cropImage.

  • ImageCropperApi

    type

    The handle setApi hands you: toBlob(options?), toCanvas(options?), reset(), rotateBy(degrees), zoomTo(zoom).

Examples

Cover image

16:9 only, exported as WebP at up to 1920px.

Round logo

cropShape="round" with a 512px PNG export: the corners stay transparent.

Your own photo

A file input hands a File to src; the object URL is created and released for you.

Free aspect

Drag the frame's corners to resize it; the grid stays on.

Scanned document

Custom A4 presets, fine rotation and a flip button.

Phone editor

A 360px screen: stacked toolbar, scrolling ratio chips, pinch to zoom.

Brand tone

tone="brand", size sm, a warmer overlay.

Dependencies

react-easy-cropmotionlucide-reactbuttonslidersegmented-controltooltipskeletonfile-exportuse-controllable-stateuse-media-querymotiontonesui-config

Files

  • components/ui/image-cropper.tsx

FAQ

Is the React Image Cropper component free?

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

How do I install Image Cropper?

Run npx wingo-ui@latest add image-cropper. It also adds the Wingo UI items it builds on (Button, Slider, Segmented Control, Tooltip, Skeleton and 6 more) and the npm packages react-easy-crop, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/image-cropper.

How does Image Cropper behave on phones?

Pick a ratio (Free, 1:1, 4:3, 3:4, 16:9, 9:16) from the segmented switch on wide layouts or the scrolling chip rail on phones, where every chip is a 44px target. On phones the stage caps at 60% of the screen height and the toolbar stacks.

See also

File UploadImage LightboxFile exportlibSlider

Used in

Avatar UploadOnboarding Flow

More React media components

Location MapAudio PlayerExcel ViewerDOCX ViewerCSV ViewerDOCX Editor
All 17 React media components

Articles using this component

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

    Component guides · Oct 19, 2026 · 9 min read

  • React File Upload: Drag and Drop, Paste, Camera, Progress

    Component guides · Oct 16, 2026 · 11 min read

PreviousImage compareNext Image Gallery

Listing photo

2-bedroom apartment, Park Slope, Brooklyn · listing cover

Loading image

JPEG, up to 2048 px on the long side, 90% quality.

value: none · restrict true · 1 to 4× · step 0.1