A drag-or-browse file picker, the upload wizard's first step. It owns only selection and drag-over visuals; the caller decides what happens to the files.
The canon behind it. docs/design-language/04-loading-and-states.md §A1 (04-loading-and-states.md:15, a spinner is legitimate only paired with a short verb phrase, which is why loading pairs with a present-tense headline) and docs/design-language/05-cards-and-surfaces.md §A1 (05-cards-and-surfaces.md:15, Card is the universal content wrapper, the rule this doc's own "dropzone IS the card" exception weighs against).
Toolkit sibling. packages/toolkit/src/download.ts (downloadDataAsFile, downloadUrlAsFile) and file.ts (checkIsExcelFile, sanitizeFilename) are the blob and file half of the upload-and-export round trip this component opens; the toolkit README's module table names this doc as where file and download belong (packages/toolkit/README.md, "Belongs with").
When to reach for it#
Reach for Dropzone any time a user hands the app files by dragging or browsing: a job's source files, a catalog import, an attachment. It does no network work itself; both shipped call sites read the onFiles callback and drive their own upload, validation, or staging from there (NewJobForm.tsx:551-560, ImportParts.tsx:305-320).
Do not wrap it in a Card by default. One shipped call site treats the dropzone itself as the framed surface and explicitly skips the wrapper: "the dropzone IS the card: the mock gives it the surface and the dashed edge, and wrapping it put a panel inside a panel" (ImportParts.tsx:302-303). The other call site does nest it inside a Card titled "Files" (NewJobForm.tsx:541-546), because that page's dropzone sits alongside other file-list content the card needs to frame as one unit. Decide per surface whether the dropzone is the whole card or one piece of a larger one; neither placement is the default.
Contract#
Files:
acceptforwards to the native file input'sacceptattribute. Both shipped call sites scope it to spreadsheet formats:.csv,.tsv,.xlsx,.xls,.xml(NewJobForm.tsx:553) and.csv,.xls,.xlsx,.xml(ImportParts.tsx:307).multipleis on at both shipped call sites; single-file selection has no shipped usage found.maxSizeis a client-side byte cap; files over it are dropped before reachingonFilesand reported below the zone as a count, not a per-file reason (dropzone.tsx:74-80). No shipped usage found in either app; both call sites accept files of any size and let the server reject.onFiles(files: File[])is the value-first callback, called with the already-filtered files on every drop, browse, or picker pick.nameandautoSubmitput the control into native-form mode: the hidden file input posts undername, andautoSubmitcallsrequestSubmit()on the surrounding form as soon as files land. No shipped usage found; both real call sites go throughonFilesand drive their own submit instead. The one placename/autoSubmitappear in either app is commented out (speedway/app/routes/workspaces/files.tsx:412-413,speedway/app/routes/workspaces/new.tsx:120), so treat native-form mode as implemented but unproven in production.
Presentation:
Icondefaults to"CloudUpload"; either anIconRenderkey or a raw JSX element. One shipped call site overrides it with a custom rendered element, a green "+" tile, rather than a registry key (ImportParts.tsx:309-313), which theIconRenderunion supports directly.headlineandhintaccept a string or JSX;hintcan be set tonullto omit the instruction line entirely when the headline's own JSX already carries it (ImportParts.tsx:323, headline is a two-line JSX block at314-322andhint={null}follows immediately after).browseLabeldefaults to"Browse Files"; one shipped call site renames it to"Add files"to match a drop target the user returns to repeatedly rather than a first-time picker (ImportParts.tsx:324).compacttightens padding and shrinks the icon for nested placements. One shipped call site sets it conditionally, tightening only once files already exist in the list below it (NewJobForm.tsx:560,compact={usable.length > 0 || pending.length > 0}); the other sets it unconditionally as part of its denser card-less layout (ImportParts.tsx:308).
State:
disabledandloadingboth gate interaction (click, drag, keyboard) through the same internalisDisabledcheck;loadingadditionally shows a spinner on the Browse button and can pair with aheadlinethat names the wait, for example "Reading your files" (NewJobForm.tsx:555-558). No shipped usage found fordisabledon its own.erroracceptstruefor a generic message or a string/node for a specific one; it renders alongside, not instead of, the internalmaxSizerejection note, since the two report different failures. The error paragraph isbasis-full, so it takes its own line below the row even when a consumer lays the box out as a wrapping row (the compact pattern's shape; owner P5, 2026-08-16); no override is needed at the call site. No shipped usage found in either app; both call sites let the surrounding form or page report errors elsewhere.skeletonrenders a dropzone-shaped placeholder box instead of the control. No shipped usage found.
Sanctioned combinations#
| Combination | Produces | Where used | Why |
|---|---|---|---|
loading={busy} paired with a headline that switches to a present-tense verb phrase while busy | A single control that reads as "working" instead of a spinner bolted beside a static label | NewJobForm.tsx:555-558 | Matches the canon's micro-process rule: a spinner pairs with a short verb phrase (04-loading-and-states.md §A1) |
compact bound to whether the caller already has files to show, not a static prop | A dropzone that shrinks once it stops being the only thing on the page | NewJobForm.tsx:560 | The zone competes for space with the file list it grows into; a static compact would either waste space early or crowd the list later |
No surrounding Card, hint={null}, custom className overriding the zone's own row/gap layout | The dropzone acting as the page's entire framed surface rather than content inside one | ImportParts.tsx:302-327 | Source comment states the reasoning directly: wrapping it in a Card produced "a panel inside a panel" |
A raw JSX element passed as Icon instead of a registry key | A custom accent tile (color, shape, glyph) the registry has no single key for | ImportParts.tsx:309-313 | IconRender accepts an element as one of its variants specifically for cases like this; see icon.md |
Banned combinations#
Do not pass both name (form mode) and rely on onFiles to also drive an upload. In form mode the hidden input holds the selection until the surrounding form submits; a second, parallel upload path from onFiles would race the form post. Neither shipped call site uses form mode, so there is no real example to follow if you do; read dropzone.tsx:53-61 (commitToForm) before combining the two.
Do not omit hint without setting it to null explicitly when headline already carries the instruction. The default hint text ("Click here or drag here to upload") is independent of headline; leaving both sides in without checking for the redundant instruction produces a zone that repeats itself, the exact case the shipped hint={null} call site avoids (ImportParts.tsx:323).
Before you adopt this#
Five questions to answer before reaching for Dropzone.
- Does the shell, a parent layout, or a global provider already render this? Not applicable, Dropzone is page content; no shell already renders it.
- Does this app already ship a local implementation of the same thing? Not applicable, no local Dropzone clone is documented in either app.
- Does this app's kit pin reach the version this component or prop landed in? Not applicable, no version-landed Dropzone prop is noted in this doc.
- Does the component derive its own accessible name and keyboard path, or must the call site supply them? Not applicable, the Browse button names itself from
browseLabel; no gap documented. - Which canon §A rules bind this surface, and which does the composition break? §A1 pairs
loadingwith a present-tenseheadline, never a bare spinner.
Travels with#
Button, internally, for the Browse control, no caller composition needed (dropzone.tsx:159-172).
RenderIcon, internally, to dispatch whatever Icon holds, string key or element (dropzone.tsx:146, render-icon.tsx). See icon.md for the full IconRender contract this accepts.
Card, as an optional outer frame, decided per surface rather than by default; see When to reach for it above.
Snippet#
// walmart-mvp/frontend/src/pages/ImportParts.tsx:303-322 (trimmed)<Dropzone multiple accept=".csv,.xls,.xlsx,.xml" compact Icon={ <span className="bg-success text-success-content grid size-[42px] flex-none place-items-center rounded-[11px] text-2xl leading-none"> + </span> } headline={ <span className="flex min-w-[200px] flex-1 flex-col text-left"> <span className="text-[15px] font-bold">Drop your files, or click to browse</span> <span className="text-base-content/65 mt-0.5 text-[13px] font-normal"> Add as many as you like: parts, attributes, fitment, images. We join them on part number. </span> </span> } hint={null} browseLabel="Add files" onFiles={addFiles}/>