File upload

File upload lets people drag and drop or browse for files and see each file's progress. Use it wherever people add files.

  • Beta
  • Accessibility
  • Figma File Upload 57:16134 · File format 4474:61107
  • packages/ui/src/components/FileUpload

Beta until design confirms the drag-over, disabled and invalid states that code adds, and Figma is re-bound to local tokens (C40); the focus indicator (C5) is also open. Everything else on this page is implemented and tested.

Preview

Change the controls, then drop or browse for a file. The preview renders the real dropzone and reports what it received. Nothing on this page uploads: files stay in your browser.

Choose a file or drag & drop it here.PDF, DOCX, or PPTX, up to 20 MB.

tsx
import { FileUpload } from '@nuton/ui'

<FileUpload
  hint="PDF, DOCX, or PPTX, up to 20 MB."
  accept=".pdf,.docx,.pptx"
  multiple
  onFilesSelected={(files) => startUploads(files)}
  onFilesRejected={(files) => explainRejection(files)}
/>

Installation

Availability

@nuton/ui and @nuton/tokens are private workspace packages. They aren't published to a registry yet (D4: registry deferred; D12: distribution waits on the provenance review). Inside the Nuton monorepo, add them as workspace dependencies. The commands below apply once the packages are published.

1. Add the packages

bash
pnpm add @nuton/ui @nuton/tokens @remixicon/react

Peer dependencies: react and react-dom 18 or later. @remixicon/react supplies the upload, status and action icons (D1).

2. Load tokens and component styles once

tsx
// app/layout.tsx (or your app's entry file)
import '@nuton/tokens/tokens.css' // --nu-* design tokens
import '@nuton/ui/styles.css'     // component styles (uses the tokens)

3. Bring your own upload code

The components don't send files anywhere. FileUpload hands you the chosen File objects; your code uploads them and passes each file's status and progress to FileUploadItem. Map --nu-font-body to your loaded Inter font as described on the Button page.

Usage

tsx
import { useState } from 'react'
import { FileUpload, FileUploadItem, FileUploadList } from '@nuton/ui'

type Upload = { id: string; file: File; progress: number; status: 'uploading' | 'completed' | 'error' }

export function LessonMaterials({ upload }: { upload: (file: File, onProgress: (p: number) => void) => Promise<void> }) {
  const [uploads, setUploads] = useState<Upload[]>([])
  const update = (id: string, patch: Partial<Upload>) =>
    setUploads((list) => list.map((u) => (u.id === id ? { ...u, ...patch } : u)))

  const start = (files: File[]) =>
    files.forEach((file) => {
      const id = crypto.randomUUID()
      setUploads((list) => [...list, { id, file, progress: 0, status: 'uploading' }])
      upload(file, (progress) => update(id, { progress }))
        .then(() => update(id, { status: 'completed' }))
        .catch(() => update(id, { status: 'error' }))
    })

  return (
    <>
      <FileUpload hint="PDF, DOCX, or PPTX, up to 20 MB." accept=".pdf,.docx,.pptx" multiple onFilesSelected={start} />
      <FileUploadList aria-label="Lesson materials">
        {uploads.map((u) => (
          <FileUploadItem
            key={u.id}
            fileName={u.file.name}
            fileSize={u.file.size}
            status={u.status}
            progress={u.progress}
            onRemove={() => setUploads((list) => list.filter((x) => x.id !== u.id))}
          />
        ))}
      </FileUploadList>
    </>
  )
}
  • FileUpload only reports files through onFilesSelected. Upload state lives in your code; FileUploadItem just displays it.
  • Always pass a hint that matches accept and your size limit. There's no default, because only you know the rules.
  • accept also filters dropped files. Handle onFilesRejected and tell people why a file wasn't added.
  • The component doesn't check file size. Check it yourself and show the reason with status="error" and statusText.
  • Give every item an onRemove, and an onCancel while uploading if your upload can be stopped. No handler means no button.

Variants and sizes

File upload is four parts: the FileUpload dropzone, a FileUploadItem card per file, FileUploadList to stack them, and the FileFormatIcon inside each card. Items come in two densities; the format icon has types, colours, sizes and two appearances.

Dropzone text and icon

The title and button label have English defaults from Figma. Override them to translate, or to fit a specific upload such as a space cover image.

Add a cover image for your spaceJPEG or PNG, at least 1200 × 600 px, up to 5 MB.

Item text

The size reads from fileSize, or “uploaded of total” when you also pass bytesUploaded. sizeText and statusText replace the defaults, for example with a specific error.

  • cell-structure.pdf484 KB of 1.2 MBUploading...
  • week-3-reading.docx84 KBCompleted
  • field-trip-recording.mp446 MBFile too large. The limit is 20 MB.
  • quiz-answers12 questionsCompleted

These items have no handlers, so they show no buttons. Sizes use Figma's units (B, KB, MB) in 1024 steps. The number is formatted in English by default; pass locale to format the digits for another language.

Translated strings

cours-photosynthese.pdf1,7 MB sur 2,3 MBEnvoi en cours…

Set lang on the item, or a parent, when its strings differ from the page language. The remaining strings (completed, error, retry, cancel, remove) are listed under FileUploadItemLabels.

Compact card

density="compact" is Figma's attached-file card (File Upload Cards [1.1], 117:1055): 10px padding, a red delete button, and only the size once the file is uploaded (“Completed” is still announced to screen readers). Use it for files already attached to a lesson or message. URLs work too: YouTube and web links get their own icons.

  • Design System! Ep. 1.pptx120 KBCompleted
  • https://youtu.be/dQw4w9WgXcQVideo · 12:04Completed
  • https://nuton.app/guides/photosynthesisWeb pageCompleted
  • lab-results.xlsx879 KB of 2.3 MBUploading...

These examples have no handlers, so they show no buttons. With onRemove, the compact card shows Figma's red delete button.

Format icon types

Figma's File Format 2 has seven types: a coloured badge with short text, or an icon for YouTube and web links, in a soft tile: 36px in Figma, 40px by default in code (P4). FileUploadItem picks the type from the file name or URL with getFileFormat; use FileFormatIcondirectly in your own lists. Six more types are code-only, for files Figma doesn't cover.

PDF
PPT
XLS
DOC
TXT
YouTube
Web link
Image
Video
Audio
Archive
Code
Other file

Format icon sizes and appearance

40 is the default (P4) and 36 is Figma's size. 24, 32 and 48 keep the same proportions (badge two-thirds of the tile). appearance="plain"drops the tile and lets the badge fill the box, like Figma's inner “File Format” badge; use it at 24 and 32, where the tile leaves the text too small.

48
40 · default
36 · Figma
32
24
plain 32
plain 24
plain 24

Format icon colours and custom badges

Each type has a colour; color, label and icon override it, for example a quiz or a flashcard deck. Badges hold up to four characters; four-character text is set slightly smaller.

red
crimson
orange
yellow
green
teal
blue
purple
pink
gray
Custom
ColourTokenWhite textUsed by
redred-700 (Figma State/Error/Base, 4.15:1)5.26:1PDF (Figma)
crimsonred-800 (Figma Error/700)6.98:1YouTube (Figma)
orangeorange-800 (Figma State/Warning/Base, 3.44:1)4.93:1PPT (Figma)
greengreen-800 (Figma Success/700, 4.29:1)5.72:1XLS (Figma)
blueState/Primary/Base5.59:1DOC (Figma)
graygray-600 (Figma State/Faded/Base, 4.23:1)6.69:1TXT, web link (Figma); archive, other files
purplepurple-600 (code-only)5.84:1Images
pinkpink-700 (code-only)4.97:1Video
tealteal-800 (code-only)5.54:1Audio, code
yellowyellow-900 (code-only)5.34:1Not used by a type; pass it yourself

The icon is decorative: the file name next to it carries the meaning, so the badge's small white text isn't held to text contrast. Code-only colours use a darker shade so the text stays legible. They, the extra sizes and the plain appearance need design confirmation (FC-21).

Dimensions

PartSizeSpacingShape and type
DropzoneFills its container (400 in Figma); 202px tall with a hintPadding 32, gap 20, text gap 6Radius 12, 1px dashed Stroke/Sub-300; title Label/Small, hint Paragraph/X Small
Upload icon24 × 24—Icon/Sub-600
Browse button32px tallPadding 6 / 12 (8 plus a 4px text inset)Radius 8, Label/Small
Item72px (completed, or error without retry); 94px uploading; 100px error with retryPadding 16 (14 at the start), row gap 12, text gap 4, 16 above the barRadius 12, 1px Stroke/Soft-200 (error: State/Error/Base)
Item action24 × 24, 20px icon—Radius 6
Status icon16 × 164 to its textParagraph/X Small
List—12 between items—
Compact item60px (completed; 56 in Figma with its 36px icon)Padding 10, row gap 12Radius 12, 1px Stroke/Soft-200; red delete icon
Format icon 40 (default, P4)40 × 40 tile, 26.7 × 26.7 badgePadding 6.7Tile radius 8.9; badge radius 4.4; text about 8.5px bold, icons 15.6px
Format icon 36 (Figma)36 × 36 tile, 24 × 24 badgePadding 6Tile Bg/Weak-50 radius 8; badge radius 4; text about 7.7px bold, icons 14px
Format icon 24–48Tile N, badge 2N/3 (plain: badge N)Padding N/6Tile radius 2N/9, badge radius 1/6 of the badge (code-only)

Taken from the component CSS, which follows Figma's geometry. The 12px list gap is a code choice: Figma's 24px is the spacing of its documentation canvas, not a spec (pending a decision).

States and interactions

Dropzone

Choose a file or drag & drop it here.PDF, up to 20 MB.

Default, hover and drag-overPoint at the area for the Weak-50 hover fill. Drag a file over it for drag-over: the same fill with a solid Strong-950 border.
Choose a file or drag & drop it here.PDF, up to 20 MB.

FocusTab to the browse button: the only tab stop. Its focus ring is Figma's button focus effect, below 3:1 (C5).
Choose a file or drag & drop it here.Uploads open when the lesson is published.

DisabledNot in Figma (C40). The button is disabled, drops are ignored, and text uses Disabled-500 (4.23:1, C8).
Choose a file or drag & drop it here.PDF, up to 20 MB.

InvalidNot in Figma (C40). An error border borrowed from the error card. Pair it with an error message, as a Field does.

Drag-over is also a code addition (C40): Figma defines only Default and Hover. Hover styles apply only on devices with hover.

Item

cell-structure.pdf545 KB of 1.2 MBUploading...
UploadingSpinner, “Uploading...” and a progress bar named by the file. The X button cancels; try it, then Undo.
week-3-reading.docx84 KBCompleted
CompletedGreen check and “Completed”. The bin button removes the file.
lab-notes.pptx3 MBConnection lost
ErrorError border and icon, a reason, and “Try again” when onRetry is set. Press it to watch a simulated retry.

Status changes are announced politely. The item action has no focus state in Figma, so code uses the button focus effects (C40, C5). The error card's delete uses Figma's dark-surface style, whose hover is invisible on white; code uses the ghost hover instead (C40).

State tokens

TokenDefaultChanges in
--nu-file-upload-bg / -bg-hoverBg/White / Bg/Weak-50Hover and drag-over use the hover fill
--nu-file-upload-borderStroke/Sub-300 (dashed)Invalid: State/Error/Base. Drag-over: solid Stroke/Strong-950
--nu-file-upload-item-borderStroke/Soft-200Error: State/Error/Base
--nu-file-upload-item-status-iconState/Primary/BaseCompleted: State/Success/Base. Error: State/Error/Base

Figma binds the card's spinner and progress to a remote blue/500; code uses the local State/Primary/Base (blue/600) and the local Progress bar, following C20 (C40).

Composition examples

Lesson materials

The full flow: add files, watch them upload, cancel, remove and retry. Press “Add example files” or pick your own. Every third file fails once at 55%; files over 20 MB fail straight away with a reason and no retry. Uploads are simulated with timers.

Choose a file or drag & drop it here.PDF, DOCX, PPTX, PNG, or MP4, up to 20 MB each.

Removing an item removes the focused button. Move focus somewhere sensible, here back to the browse button, so keyboard users aren't sent to the top of the page.

Inside a Field

Wrap the dropzone in Field for a label, hint and error. The browse button's name becomes “Lab report Browse File”, the hint and error describe it, and an error sets the invalid border. Submit without a file to see the error.

Choose a file or drag & drop it here.

PDF or DOCX, up to 20 MB.

The hidden file input resets after each pick and has no name, so a native form post can't send the file. Upload from your own code (pending a decision).

API reference

FileUpload props

FileUpload props
PropTypeDefaultDescription
titleReactNode'Choose a file or drag & drop it here.'Heading. Part of the browse button’s description.
hintReactNode—Constraint text under the title, added to the button's description. No default: it must match accept.
browseLabelReactNode'Browse File'Label of the browse button.
iconReactNode<RiUploadCloud2Line />Replaces the decorative upload icon. Rendered aria-hidden.
acceptstring—Native accept filter, for example ".pdf,image/*". Also applied to dropped files.
multiplebooleanfalseAllows several files. Without it, only the first dropped file is accepted and the rest are rejected.
disabledbooleanfalseDisables the picker and ignores drops. Inherits from a surrounding Field.
invalidbooleanfalseError border. Inherits from a surrounding Field with an error.
captureboolean | 'user' | 'environment'—Native capture hint for mobile cameras.
onFilesSelected(files: File[]) => void—Called with accepted files from the picker or a drop. Nothing is uploaded.
onFilesRejected(files: File[]) => void—Called with dropped files that fail accept or exceed the multiple limit.
idstring—Id of the browse button (the focusable control). A surrounding Field sets it.
refRef<HTMLDivElement>—Forwarded to the root dropzone element.
…restOmit<HTMLAttributes<HTMLDivElement>, 'title' | 'onDrop' | 'onChange' | 'children'>—Other attributes, passed to the root. onClick and the drag handlers run before the built-in ones; aria-describedby is added to the button's description.

FileUploadItem props

FileUploadItem props
PropTypeDefaultDescription
fileNamerequiredstring—File name. Also names the progress bar and the action buttons.
status'uploading' | 'completed' | 'error''uploading'Upload state (Figma State).
progressnumber0Upload progress, 0–100. Shown only while uploading.
fileSizenumber—Total size in bytes, formatted with formatFileSize.
bytesUploadednumber—Bytes sent so far. With fileSize, the size reads “uploaded of total”.
sizeTextReactNode—Replaces the size text entirely.
statusTextReactNode—Replaces the status text, for example a specific error.
density'default' | 'compact''default'Card layout. compact is Figma 117:1055: 10px padding, red delete, size only once uploaded.
formatTypeFileFormatTypefrom file name or URLFormat icon type, from getFileFormat(fileName).
formatLabelstringfrom getFileFormatFormat badge text (“report.docx” → DOC).
formatColorFileFormatColorfrom getFileFormatFormat badge colour.
iconReactNode<FileFormatIcon />Replaces the format icon, for example with a thumbnail. Rendered aria-hidden.
onCancel() => void—X button while uploading. Falls back to onRemove. No handler, no button.
onRemove() => void—Delete button when completed or failed. No handler, no button.
onRetry() => void—Shows “Try again” in the error state and calls this.
labelsPartial<FileUploadItemLabels>EnglishOverrides any of the strings below.
localestring | string[]'en'Locale for size formatting. English by default so server and browser render the same text; pass your app's locale to localise.
refRef<HTMLDivElement>—Forwarded to the card element.
…restOmit<HTMLAttributes<HTMLDivElement>, 'children'>—Other attributes, passed to the card (className, lang, data-*…).

FileUploadItemLabels

FileUploadItemLabels fields
PropTypeDefaultDescription
uploadingstring'Uploading...'Status text while uploading.
completedstring'Completed'Status text when done.
errorstring'Failed'Status text on failure.
retrystring'Try again'Retry action label.
cancelstring'Cancel upload'Name prefix of the X button; the file name is appended.
removestring'Remove'Name prefix of the delete button; the file name is appended.
sizeOf(uploaded: string, total: string) => string`${uploaded} of ${total}`Joins uploaded and total size.

FileUploadList props

FileUploadList props
PropTypeDefaultDescription
childrenReactNode—The items. Each child is wrapped in an <li>; null and false are skipped.
refRef<HTMLUListElement>—Forwarded to the list.
…restHTMLAttributes<HTMLUListElement>—Other attributes, passed to the <ul>. Give it an aria-label when the page has several lists.

FileFormatIcon props

FileFormatIcon props
PropTypeDefaultDescription
typeFileFormatType'file'Figma Type: pdf, ppt, xls, doc, txt, youtube, link; code-only image, video, audio, archive, code, file. Sets the default text, colour and icon.
labelstringthe type's textBadge text, up to 4 characters, such as “PDF”.
color'red' | 'crimson' | 'orange' | 'yellow' | 'green' | 'teal' | 'blue' | 'purple' | 'pink' | 'gray'the type's colourBadge colour.
iconReactNodeYouTube and globe icons for those typesShows an icon in the badge instead of text.
size24 | 32 | 36 | 40 | 4840Box size in px. 40 is the default (P4), 36 is Figma; the rest keep its proportions.
appearance'tile' | 'plain''tile'tile: the Figma Weak-50 square around the badge. plain: the badge alone, filling the box.
refRef<HTMLSpanElement>—Forwarded to the root.
…restOmit<HTMLAttributes<HTMLSpanElement>, 'color' | 'children'>—Other attributes, passed to the root. It's always aria-hidden, whatever you pass.

Helpers

Plain functions with no React or browser dependencies, so they also run on the server.

FunctionSignatureBehaviour
fileMatchesAccept(file: Pick<File, 'name' | 'type'>, accept?: string) => booleanTrue when the file matches an accept string: extensions (.pdf), wildcards (image/*) and exact MIME types. An empty accept matches everything.
formatFileSize(bytes: number, locale?: string | string[]) => stringLocale-aware size in 1024 steps, one decimal below 10. Negative or non-finite input reads as 0.
getFileExtension(fileName: string) => stringThe extension, upper-cased and cut to 4 characters. Empty when there's none.
getFileFormat(fileNameOrUrl: string) => FileFormatThe type, badge text and colour for a file name or URL. YouTube URLs → youtube, other URLs → link; Figma types use their short text (DOCX → DOC); others show the extension.
getFileFormatColor(fileName: string) => FileFormatColorThe colour part of getFileFormat. Unknown extensions get gray.
FILE_FORMAT_TYPESRecord<FileFormatType, { label, color }>Each type's default text and colour.
tsx
import { fileMatchesAccept, formatFileSize, getFileExtension, getFileFormat } from '@nuton/ui'

fileMatchesAccept({ name: 'notes.pdf', type: 'application/pdf' }, '.pdf,.docx') // true
fileMatchesAccept({ name: 'photo.png', type: 'image/png' }, '.pdf,.docx')       // false
formatFileSize(4_400_000)              // "4.2 MB"
formatFileSize(4_400_000, 'fr')        // "4,2 Mo"
getFileExtension('lesson-plan.docx')   // "DOCX"
getFileFormat('lesson-plan.docx')      // { type: 'doc', label: 'DOC', color: 'blue' }
getFileFormat('https://youtu.be/abc')  // { type: 'youtube', label: '', color: 'crimson' }
Helper results for example file names, computed when this page was built
File namegetFileExtensiongetFileFormatMatches ".pdf,.pptx"
photosynthesis-slides.pptx"PPTX"ppt · "PPT" · orangetrue
lab-safety-checklist.pdf"PDF"pdf · "PDF" · redtrue
leaf-cross-section.png"PNG"image · "PNG" · purplefalse
https://youtu.be/dQw4w9WgXcQ"BE/D"youtube · "" · crimsonfalse
reading-list""file · "" · grayfalse
formatFileSize results, computed when this page was built
BytesformatFileSize(bytes, 'en')
00 B
900900 B
389,000380 KB
4,400,0004.2 MB
2,147,483,6482 GB

Styling hooks

The dropzone renders .nu-file-upload with data-dragging, data-disabled and data-invalid; the item renders .nu-file-upload-item with data-status; the icon renders .nu-file-format-icon with data-color and data-size. Use props rather than styling these. The tokens in State tokens are the supported override points.

Accessibility

Keyboard

KeyBehaviour
TabMoves to the browse button, the dropzone’s only tab stop, then through each item’s Try again and cancel or delete buttons.
Enter / SpaceOn the browse button, opens the native file picker. On an item button, runs it.

Screen readers

The browse button is named by its label (inside a Field: “field label + browse label”) and described by the title, hint and any Field hint or error. Each item's status sits in a polite live region; its progress bar is named by the file name; its buttons read “Cancel upload file” or “Remove file”; Try again is described by the file name. Icons are hidden. The list is a real list.

Focus

The browse button and item buttons use Figma's button focus effects, below 3:1 today (C5); the item button has no Figma focus state (C40). Removing an item doesn't move focus for you.

Contrast and colour

The dashed border is 1.53:1 (the C7 pair) and the card and button borders 1.19:1, below 3:1. The error border and icon are 4.15:1, above 3:1; the Try again and Delete text use Red 700, 5.26:1 (C6). Every format badge is at least 4.5:1 with its white text: Figma's red, orange, green and gray move one step darker (C51). The file name is always text. Status is shown with icon and words, never colour alone.

Touch and pointer

The whole dropzone opens the picker on click or tap. Item buttons are 24 × 24, the WCAG 2.5.8 minimum; Try again is 20px tall (C28). Drag and drop isn't available on most touch screens, so the browse button is the main path. capture can offer the camera.

You must: give a hint that states the allowed formats and size; explain rejected and failed files in words; move focus after removing an item; pass onRemove (and onCancel where possible) so people can undo a choice.

Figma mapping

Figma component · propertyFigma valuesCode
File Upload Area · StateDefault · HoverFileUpload; hover is interactive. Drag-over, disabled and invalid are code additions (C40)
File Upload Area · textTitle, constraint text, Browse Filetitle, hint, browseLabel
File Upload Cards · StateIn Progress · Success · ErrorFileUploadItem status: uploading · completed · error
File Upload Cards · Progress BarInstanceprogress (the local Progress component)
File Upload Cards · Link Button “Try again”Error onlyonRetry
File Upload Cards · Compact Buttonclose-line · delete-bin-lineonCancel, onRemove
File Format 2 · Type (4474:61041)PDF · PPT · XLS · DOCS · TXT · YOUTUBE · WEB LinksFileFormatIcon type: pdf · ppt · xls · doc · txt · youtube · link (plus code-only types)
File Format 2 · tile36 × 36size={36}, appearance="tile" (other sizes and plain are code-only)
File Upload Cards [1.1] (117:1055)Attached-file cardFileUploadItem density="compact"
Image Upload57:16317Not built (C40)

Every binding on these components is remote. Code uses the same-valued local tokens, except the card's blue (see State tokens). The remote library calls green “Primary” and blue “Secondary”, the reverse of local naming, so Figma should be re-bound (C40).

Guidelines and related components

Use File upload for

  • Adding lesson materials, readings or recordings to a space.
  • Students submitting assignments, such as a lab report.
  • Importing a file to work from, such as notes to turn into flashcards.

Don't use File upload for

  • Images that need a preview or crop, such as a profile photo. Image Upload isn't built yet (C40).
  • Pasting a link to a video or article. Use a text field.
  • Showing progress that isn't a file upload. Use Progress.

Do and don't

Choose a file or drag & drop it here.PDF, DOCX, or PPTX, up to 20 MB.

DoState the rules before people choose. A hint with formats and size saves a failed upload.
Choose a file or drag & drop it here.

Don'tDon't leave the hint out. People find the rules only after a file is rejected.
field-trip-recording.mp446 MBFile too large. The limit is 20 MB.
DoSay why a file failed. A specific reason tells people whether to retry or pick another file.
field-trip-recording.mp446 MBFailed
Don'tDon't stop at “Failed”. A bare failure doesn\u2019t say whether trying again can help.
photosynthesis-slides.pptx4.2 MBCompleted
DoKeep real file names. People recognise their own files; the name is also the item's accessible name.
File 14.2 MBCompleted
Don'tDon't replace names with generic labels. “File 1” can't be told apart from “File 2”, on screen or by a screen reader.

Common mistakes

  • Expecting the component to upload, or to post the file with a form. It only reports files; your code sends them.
  • Ignoring onFilesRejected, so a dropped file silently disappears.
  • Setting accept without a matching hint, or a hint that promises formats accept blocks.
  • Removing an item without moving focus, which sends keyboard users back to the top of the page.
  • Wrapping the dropzone in another button or link. The browse button is already the control.