FilePicker
A dashed box you drop files on, or press to open the file dialog.
import { FilePicker } from 'neba';
<FilePicker
multiple
label="Attachments"
accept="image/*,.pdf"
maxSize={5_000_000}
maxFiles={4}
onFilesChange={setFiles}
onReject={setRejected}
/>;Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variantshared | 'solid' | 'outline' | 'text' | 'outline' | Weight of the surface. All three share the dashed edge — the one place the library draws a line that is not solid, because a dashed rectangle is the established sign for a drop target |
| sizeshared | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'md' | The box's padding and type scale. A dropzone is sized by the gesture it has to catch, not by what is written in it |
| colorshared | 'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info' | 'primary' | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | 'default' | 'compact' | 'default' | Padding only — never the height, never the type scale |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| accept | string | — | Which files the browser's own picker offers — 'image/*,.pdf'. Dropped files are checked against it too, which the attribute alone does not do |
| multiple | boolean | false | Whether more than one file may be chosen |
| maxSize | number | — | The largest a single file may be, in bytes |
| maxFiles | number | — | How many files may be held at once — counted against what is already chosen, not against one drop |
| value | readonly File[] | — | The chosen files, for a controlled picker |
| defaultValue | readonly File[] | — | The initially chosen files |
| onFilesChange | (files: File[]) => void | — | Called when the list of files changes |
| onReject | (rejections: FileRejection[]) => void | — | Called with everything turned away, and why. Without it a rejected file disappears silently, which is the worst thing a dropzone does |
| title | ReactNode | — | The line inside the box |
| hint | ReactNode | — | The line under it — what is accepted, how big, how many |
| icon | ReactNode | — | The glyph above the title. Pass null for a box with no picture in it |
| showList | boolean | true | Lists the chosen files under the box, each with a way to remove it |
| removeLabel | (name: string) => string | — | Accessible name of a file's remove button |
| label | ReactNode | — | The label, wired to the control by Base UI's Field |
| description | ReactNode | — | Helper text |
| error | ReactNode | — | Error message. Its presence turns the control invalid and re-points the colour family at danger |
| invalid | boolean | !!error | Forces the invalid state without a message, for when a form library owns validity |
| readOnly | boolean | false | Shown but not changeable. Keeps its colour and edge, drains the saturation |
| disabled | boolean | false | Unavailable. Drops the colour family for neutral grey |
| fullWidth | boolean | true | Stretches to the width of the container |
| required | boolean | false | Whether a file must be chosen before the form is submitted |
| name | string | — | Identifies the field when a form is submitted |
Examples
Variants
All three share the dashed edge. It is the one place in the library that draws a line which is not solid, and it is not decoration — a dashed rectangle is the established sign for "this area accepts a drop", and a dropzone that looks like a card is a card nobody tries to drop on.
What it turns away
States
The browser does not apply accept to a dropped file
The accept attribute governs the browser's own file dialog and nothing else. A file that arrived by drag has never been checked against that string. A dropzone that only sets the attribute accepts anything the moment a file comes in by drop.
This component runs the check itself, against the same string, in the three forms the attribute takes: .ext, type/subtype, and type/*.
A drag state that does not flicker
dragenter and dragleave fire for every child of the zone the pointer crosses. A dropzone that toggles a boolean therefore flickers the entire time a file is being dragged over its own contents. Counting the events is the fix, and the only thing that survives a zone with content in it.
maxFiles is not "you may drop this many"
It is "you may end up with this many". It is checked against what is already held, so dropping three files on a maxFiles={3} picker that holds two accepts exactly one of them.
Why there is no Base UI primitive
A dropzone is a <div> listening for four drag events and an <input type="file"> it clicks for you. There is no popup to position, no focus to trap and no roving anything. The fallback the design language allows — plain React and DOM — is the right one here.
What is left is the part hand-rolled dropzones usually get wrong, which is the three sections above.
Accessibility
The shell is a <div> and the pressable area inside it is a real <button>. The file list sits outside that button, because the remove buttons cannot be nested inside the browse button — the same shape Chip and ListItem use.
The real <input type="file"> is moved off-screen rather than hidden. display: none and visibility: hidden both make an input unfocusable in some browsers, and this one still has to be reachable to a form and to a required validation message.
Pass onReject. Without it a rejected file disappears without a word.