Build a File Upload UI with React and Tailwind CSS v4
Build an accessible file upload interface with drag-and-drop, file list, and upload progress — using Ninna UI components and Tailwind v4.
File upload UI considerations
A file upload UI needs: a drop zone (click to browse + drag-and-drop), a file list with names and sizes, upload progress indicators, and remove buttons. Accessibility requires keyboard-accessible drop zone and screen reader announcements.
1. Drop zone
Create a drop zone with a hidden file input and a styled label:
import { Upload } from "lucide-react";import { VStack } from "@ninna-ui/layout";import { Text } from "@ninna-ui/primitives";
<label className="flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed border-base-300 p-8 cursor-pointer hover:border-primary hover:bg-primary/5 transition-colors" onDragOver={(e) => e.preventDefault()} onDrop={handleDrop}> <Upload className="size-8 text-base-content/40" /> <Text className="text-base-content/60">Drag files here or click to browse</Text> <input type="file" multiple className="sr-only" onChange={handleFileSelect} /></label>2. File list with progress
Display uploaded files with a progress bar and remove button:
import { VStack, HStack } from "@ninna-ui/layout";import { Text, IconButton } from "@ninna-ui/primitives";import { X } from "lucide-react";
<VStack gap="3"> {files.map((file) => ( <div key={file.name} className="rounded-lg border border-base-200 p-3"> <HStack justify="between"> <Text size="sm" weight="medium">{file.name}</Text> <IconButton size="sm" variant="ghost" aria-label={`Remove ${file.name}`} onClick={() => removeFile(file)}> <X className="size-4" /> </IconButton> </HStack> <div className="mt-2 h-1.5 rounded-full bg-base-200"> <div className="h-full rounded-full bg-primary transition-all" style={{ width: `${file.progress}%` }} /> </div> </div> ))}</VStack>3. Accessibility
The drop zone is a <label> wrapping a hidden input, so it is keyboard-accessible (Tab to focus, Enter/Space to open file dialog). Add aria-label to the remove buttons (IconButton does this via aria-label prop). Use role='status' and aria-live='polite' on the file list to announce uploads.