External Libraries Assessment
This document evaluates existing React component libraries and their fit for implementing the image upload UI components. The assessment covers the project’s established stack — ShadCN/ui (with Radix UI primitives), Tailwind CSS, and AG Grid — plus specialized image libraries needed to fill gaps.
For the component specifications that these libraries support, see UI Components.
Established Stack
Section titled “Established Stack”ShadCN/ui + Radix UI Primitives
Section titled “ShadCN/ui + Radix UI Primitives”ShadCN/ui provides copy-paste, project-owned React components built on Radix UI
headless primitives and styled with Tailwind CSS. The project already uses
ShadCN stock primitives in canary/primitives/ with custom Arda atoms wrapping
them. Radix primitives provide WAI-ARIA-compliant accessibility (focus trapping,
keyboard navigation, screen reader announcements) out of the box.
| Our Component | ShadCN/Radix Primitives | Coverage | Gaps |
|---|---|---|---|
ImageCellDisplay | Avatar (image + fallback with delayMs), AspectRatio, Skeleton (loading shimmer), Tooltip | High | Error badge overlay is custom; shimmer animation needs a custom @keyframes in @theme or use of Skeleton component |
ImageInspectorOverlay | Dialog (modal, portal, overlay, focus trap, Escape/click-outside dismiss) | Very High | Image zoom/pan gestures within the viewer are not provided |
ImageDropZone | VisuallyHidden (accessible hidden file input), Label, Input (URL text field), Button (file picker trigger) | Low | Drag-and-drop, clipboard paste, camera capture, and input classification are entirely custom. Radix has no FileUpload primitive (open request: radix-ui/primitives#1327). See react-dropzone below |
ImagePreviewEditor | Slider (zoom control with keyboard), Toolbar (roving tabindex for edit controls), Toggle / ToggleGroup (crop/pan mode), AspectRatio (preview container), Tooltip (toolbar button labels) | Medium | Image cropping, rotation, and canvas manipulation require a dedicated library. See react-easy-crop below |
ImageComparisonLayout | Tabs (mobile tabbed view with keyboard nav, ARIA roles) | Medium | Desktop side-by-side layout is plain CSS flexbox. Responsive switching is a media query concern, not a component library concern |
CopyrightAcknowledgment | Checkbox (controlled, required, hidden native input), Label | Very High | Timestamp logging is application logic only |
ImageUploadDialog | Dialog (modal mode: focus trap, portal, overlay), Progress (upload progress bar), AlertDialog (Warn state discard confirmation) | Medium | State machine orchestration (useReducer or XState), validation logic, presigned POST workflow, and auto-compression are all custom |
ImageRemoveConfirmation | AlertDialog (purpose-built for confirm/cancel interruptions: Action + Cancel buttons, no click-outside dismiss, ARIA alert dialog pattern) | Complete | None — AlertDialog covers this entirely |
EntityImageField | Label (form field), Avatar (current image display), Tooltip (action button labels), Dialog (composed via ImageUploadDialog), AlertDialog (composed via ImageRemoveConfirmation) | Medium | Form field layout, integration with form libraries (react-hook-form), and field-level validation are custom |
When to use Radix directly vs. ShadCN/ui: For ImageInspectorOverlay,
CopyrightAcknowledgment, and ImageRemoveConfirmation, the ShadCN wrapper
adds only default Tailwind styling over the Radix primitive — using Radix
directly is viable if custom styling is preferred. For more complex compositions
(ImageUploadDialog, EntityImageField), ShadCN’s pre-styled Dialog, Tabs, and
Button components save time without adding behavioral value beyond Radix.
Community extensions worth evaluating:
- Dice UI FileUpload — Radix-patterned file upload component
- Dice UI Cropper — Radix-patterned image cropper
- sadmann7/file-uploader — ShadCN-compatible file uploader
Tailwind CSS
Section titled “Tailwind CSS”The project uses Tailwind CSS v4 with CSS-first configuration (@import 'tailwindcss', @theme inline) and tw-animate-css for entry/exit
animations. The cn() utility (clsx + tailwind-merge) is the standard
class-merging pattern.
Tailwind covers 100% of the visual styling needs across all components and 0% of the behavioral logic (drag-and-drop events, image manipulation, state machines).
| Our Component | Key Tailwind Utilities | Notes |
|---|---|---|
ImageCellDisplay | aspect-square, object-cover, object-center, animate-[shimmer] (custom @keyframes in @theme), absolute (error badge) | Shimmer requires a custom keyframe definition |
ImageInspectorOverlay | fixed inset-0 z-50 bg-black/80, max-w-full max-h-[90vh] object-contain, tw-animate-css fade/zoom | Fully covered |
ImageDropZone | border-2 border-dashed rounded-lg, dynamic cn() toggle for drag-over highlight (no native drag-over: variant) | Drag-over state requires JS class toggle |
ImagePreviewEditor | aspect-square overflow-hidden relative, transition-transform for zoom/rotate feedback | Crop overlay and canvas ops are outside Tailwind scope |
ImageComparisonLayout | hidden md:flex md:flex-row md:gap-4 (desktop), flex flex-col md:hidden (mobile tabs) | md: breakpoint handles responsive switching |
CopyrightAcknowledgment | flex items-start gap-3, data-[disabled]:opacity-50 | Simplest component |
ImageUploadDialog | border rounded-lg p-4 (inline), Dialog overlay styles (modal), text-destructive (errors), tw-animate-css transitions | State-dependent content is React logic |
ImageRemoveConfirmation | bg-destructive text-destructive-foreground (action button) | Fully covered via AlertDialog styling |
EntityImageField | relative group rounded-lg, group-hover:opacity-100 transition-opacity (hover affordances), disabled:pointer-events-none | Fully covered |
No additional Tailwind plugins needed. aspect-square and aspect-[1/1]
are native in v4. @tailwindcss/forms is not needed (ShadCN primitives handle
form styling). @tailwindcss/typography is not needed (no prose rendering).
AG Grid
Section titled “AG Grid”AG Grid provides the data grid in which ImageCellDisplay renders and from
which ImageInspectorOverlay and ImageUploadDialog (modal mode) are
triggered. All required features are available in AG Grid Community (MIT
license).
Custom Cell Renderer (ImageCellDisplay):
- Register via
cellRenderer: ImageCellDisplayin the column definition - Pass click handlers via
cellRendererParams deferRender: truedefers thumbnail rendering until scrolling stops, showing aloadingCellRenderer(skeleton shimmer) meanwhileReact.lazy()is natively supported for cell renderers- DOM virtualization (default
rowBuffer: 10) means only visible rows render — use<img loading="lazy">, fixed dimensions, andReact.memofor performance in large grids
Click vs. double-click distinction (inspect vs. edit):
- Set
suppressClickEdit: trueon the image column - Use a timer-based debounce:
onCellClickedsets a 250ms timeout;onCellDoubleClickedclears it and opens the edit dialog - Alternatively, handle click/double-click inside the cell renderer itself on the thumbnail element
- Enter key: use
onCellKeyDownto detect Enter and trigger the edit dialog
Modal overlay (preventing grid interaction while ImageUploadDialog is
open):
- AG Grid’s built-in overlays do not block pointer events (known issue ag-grid#1755)
- Recommended: external overlay
<div>over the grid container withpointer-events: all, toggled when the modal opens/closes - Focus trapping (from Radix Dialog) prevents Tab from reaching grid cells
Cell editor API — using a cell editor for the upload dialog is not
recommended for the main upload workflow. Cell editors are tightly coupled to
the grid’s value-in/value-out lifecycle, which does not map cleanly to an async
upload workflow with multiple interaction states. However, an ImageCellEditor
wrapper is provided for AG Grid API consistency — it triggers the external
ImageUploadDialog and updates the row via api.getRowNode(id).setDataValue()
on confirm.
Image-Specific Libraries
Section titled “Image-Specific Libraries”These libraries fill the gaps that ShadCN/Radix/Tailwind/AG Grid do not cover: image cropping, drag-and-drop file handling, and client-side compression.
react-easy-crop
Section titled “react-easy-crop”Recommended for ImagePreviewEditor.
| Version | 5.5.6 |
| License | MIT |
| npm weekly downloads | ~888K |
| Bundle size | 7.1 KB gzip / 23.8 KB min |
| TypeScript | Built-in types |
| Last published | Nov 2025 |
Fit assessment:
| Requirement | Support | Notes |
|---|---|---|
| Locked aspect ratio (1:1) | Built-in | aspect prop — set to 1 |
| Zoom | Built-in | zoom, minZoom, maxZoom, onZoomChange — pair with Radix Slider |
| Rotate | Built-in | rotation + onRotationChange — declarative, controlled |
| 90-degree increments | Trivial | Button that increments rotation state by 90 |
| Pan | Built-in | Drag to reposition within crop area |
| Reset | Trivial | Reset state values for crop, zoom, rotation |
| File + URL sources | Built-in | Accepts image URL strings, base64, or blob URLs |
| Touch/mobile | Built-in | Touch gestures for pan and pinch-to-zoom |
Gap: No built-in “get cropped image” output — requires a canvas helper function to produce the final cropped Blob. This is a well-documented pattern (~15 lines of code).
Alternatives considered:
react-image-crop(825K downloads, 4.3 KB gzip) — no built-in zoom or rotation; poor fitreact-cropper(215K downloads) — wraps Cropper.js v1; imperative API, last published Apr 2023 (stale)Cropper.js v2(794K downloads) — powerful but no official React wrapper; requires Web Components integration
react-dropzone
Section titled “react-dropzone”Recommended as the foundation for ImageDropZone.
| Version | 15.0.0 |
| License | MIT |
| npm weekly downloads | ~5.4M |
| Bundle size | 16.3 KB gzip / 59.9 KB min |
| TypeScript | Built-in types |
| Last published | Feb 2026 |
Fit assessment:
| Requirement | Support | Notes |
|---|---|---|
| Drag-and-drop | Built-in | Visual feedback via isDragActive state |
| File picker (click) | Built-in | open() function or click-to-open |
| File type validation | Built-in | accept prop with MIME types |
| File size limits | Built-in | maxSize, minSize props |
| Clipboard paste | Not built-in | Add custom paste event listener (~15 lines); extract from clipboardData.files or parse HTML for image URLs |
| URL text input | Not built-in | Standard <Input> component alongside the drop zone |
| Camera capture | Not built-in | <input type="file" accept="image/*" capture="environment"> for mobile |
| Headless/unstyled | Yes | Full control over UI — compose with ShadCN/Tailwind |
Alternatives considered:
FilePond / react-filepond(56K downloads) — built-in paste, URL, camera; but opinionated UI conflicts with custom design systems; plugin-heavy architectureUppy / @uppy/react(206K downloads) — most feature-complete (Dashboard, Webcam, ImageEditor plugins); massive overkill for a focused drop zone; heavy bundle cost
browser-image-compression
Section titled “browser-image-compression”Recommended for client-side auto-compression in ImageUploadDialog.
| Version | 2.0.2 |
| License | MIT |
| npm weekly downloads | ~533K |
| Bundle size | 19.6 KB gzip / 51.7 KB min |
| TypeScript | Built-in types |
| Last published | Mar 2023 |
Fit assessment:
| Requirement | Support | Notes |
|---|---|---|
| JPEG quality (85%) | Built-in | Quality control option |
| Max dimension (2048px) | Built-in | maxWidthOrHeight option |
| Non-blocking | Built-in | Runs in Web Worker |
| EXIF orientation fix | Built-in | Auto-corrects rotated photos |
| Returns File/Blob | Built-in | Direct input to presigned POST |
Gap: No HEIC support — cannot read HEIC files. Requires a separate conversion step if HEIC is accepted.
heic2any (supplemental, lazy-loaded)
Section titled “heic2any (supplemental, lazy-loaded)”Required only if HEIC format support is confirmed in scope.
| Version | 0.0.4 |
| License | MIT |
| npm weekly downloads | ~230–500K |
| Bundle size | ~1.15 MB gzip (uses libheif WASM) |
| TypeScript | No built-in types |
| Last published | Mar 2023 |
Must be lazy-loaded. Detect HEIC by file extension / MIME type, dynamically
import('heic2any') only when needed. The 1.15 MB gzip cost is acceptable only
as a conditional, on-demand load — never in the main bundle.
Pipeline: detect HEIC → convert with heic2any → compress with browser-image-compression → proceed to preview.
react-compare-slider (optional)
Section titled “react-compare-slider (optional)”Potentially useful for ImageComparisonLayout if an interactive slider
comparison UX is desired.
| Version | 4.0.0 |
| License | MIT |
| npm weekly downloads | ~55K |
| Bundle size | 3.4 KB gzip / 9.2 KB min |
| TypeScript | Built-in types |
| Last published | Mar 2026 |
However, the current ImageComparisonLayout spec calls for side-by-side
(desktop) or tabbed (mobile) views — not a slider overlay. This is a layout
concern addressable with CSS flexbox + ShadCN Tabs, adding zero dependency
weight. The library is worth revisiting if the UX evolves toward an interactive
slider comparison.
Summary
Section titled “Summary”Reusability Summary
Section titled “Reusability Summary”| Component | ShadCN/Radix | Tailwind | AG Grid | External Library | Custom Code |
|---|---|---|---|---|---|
ImageCellDisplay | Avatar, Skeleton, AspectRatio | Styling | Cell renderer, deferRender | — | Error badge |
ImageInspectorOverlay | Dialog | Styling | Event trigger | — | — |
ImageDropZone | VisuallyHidden, Label, Input, Button | Styling (no drag-over:) | — | react-dropzone | Clipboard paste, URL input, camera, input classification |
ImagePreviewEditor | Slider, Toolbar, Toggle | Styling | — | react-easy-crop | Canvas crop output helper |
ImageComparisonLayout | Tabs | Responsive layout | — | — | Responsive switching logic |
CopyrightAcknowledgment | Checkbox, Label | Styling | — | — | Timestamp logging |
ImageUploadDialog | Dialog, Progress, AlertDialog | Styling, transitions | — | browser-image-compression, heic2any (lazy) | State machine, validation, presigned POST |
ImageRemoveConfirmation | AlertDialog | Styling | — | — | — |
EntityImageField | Label, Avatar, Tooltip | Styling | — | — | Form integration |
Recommended Library Additions
Section titled “Recommended Library Additions”| Library | Purpose | Bundle Impact (gzip) | Covers |
|---|---|---|---|
react-easy-crop | Image crop/zoom/rotate/pan | 7.1 KB | ImagePreviewEditor |
react-dropzone | Drag-and-drop + file picker | 16.3 KB | ImageDropZone (foundation) |
browser-image-compression | Client-side auto-compression | 19.6 KB | ImageUploadDialog (validation pipeline) |
heic2any | HEIC-to-JPEG conversion | ~1.15 MB (lazy-loaded only) | ImageUploadDialog (HEIC support, if confirmed) |
Total baseline bundle impact: ~43 KB gzip (excluding heic2any, which is lazy-loaded on demand).
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved