Skip to main content
Use Case

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.

Component Docs

FAQ