File selector
Picks files with an Upload Files button or by dropping them on the field. The app uploads the files it picks.
Installation
The CLI installs the Radix or Base UI file to match your project, with what it builds on: button.
If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.
When to use
Use it
- People attach a file in a form, like a document on a case or a signed contract on an opportunity.
- People pick files with the Upload Files button, or drop them from their desktop onto the field.
- Only some types are allowed, like PDFs or images. Pass
accept, and dropped files that don't match are rejected. - A whole area should take dropped files, like a Files card or a composer. Use
variant="integrated". - It replaces
lightning-inputwithtype="file", and the file picking oflightning-file-upload.
Don't use it
- You need the upload itself: progress, a server, a record to attach to. The file selector only chooses files. Send them from
onFilesChangeor with the form. - You need to show an uploaded file as a card with a preview. That's SLDS's file card, which this library doesn't have yet.
- Nothing is ever dropped, like a field in a narrow dialog. A plain
Inputwithtype="file"is enough. - People pick a record, not a file. Use Lookup.
Usage
import {
FileSelector,
FileSelectorBody,
FileSelectorButton,
FileSelectorDropzone,
FileSelectorIcon,
FileSelectorInput,
FileSelectorText,
} from "@/components/wisty/file-selector"FileSelector
└── FileSelectorDropzone
├── FileSelectorInput
└── FileSelectorBody
├── FileSelectorIcon
├── FileSelectorButton
└── FileSelectorTextFileSelector holds the input's settings (id, name, accept, multiple, disabled, required) and reports what changes; the other parts only draw. id is the input's, for a FieldLabel's htmlFor. FileSelectorIcon goes in the integrated drop zone's body.
onFilesChange gets the files the selector holds now: a pick replaces them, as a native file input does, and a drop brings its accepted files. onFilesReject gets the files it didn't take and why. To keep a list, pass files and append:
<FileSelector
id={id}
multiple
accept=".pdf"
files={files}
onFilesChange={(chosen) => setFiles((current) => [...current, ...chosen])}
onFilesReject={(rejections) => setError(rejections[0].reason)}
>reason is "accept" for a type accept doesn't allow, and "multiple" for a drop of several files when multiple isn't set; then every file of that drop is rejected. With name, the input submits the files with a form, dropped ones included.
Examples
Several files
Several files in a controlled `files` list: `onFilesChange` appends what's chosen, and each file's remove button takes it out.
248 KB
36 KB
Image
An account logo with `variant="images"`: a larger drop zone with the text under the button.
Drop on a card
A whole Files card as a drop target with `variant="integrated"`, shown while files are dragged, and the card's own selector picks them.
States
A required field showing "Complete this field.", or a type error from `onFilesReject`, and a `disabled` one that takes no drop.
API reference
| Part | Built on |
|---|---|
FileSelector | <div> (opens in a new tab) |
FileSelectorDropzone | <div> (opens in a new tab) |
FileSelectorInput | <input> (opens in a new tab) |
FileSelectorBody | <label> (opens in a new tab) |
FileSelectorButton | <span> (opens in a new tab) |
FileSelectorText | <span> (opens in a new tab) |
FileSelectorIcon | <span> (opens in a new tab) |
FileSelector
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "files" | "images" | "integrated" | "files" | |
id | string | The input's id, for a FieldLabel's htmlFor. | |
name | string | The input's name, to submit the files with a form. | |
accept | string | File types to take, as the native input's accept: .pdf,image/*. | |
multiple | boolean | false | Take several files at once. |
disabled | boolean | false | |
required | boolean | false | |
form | string | The id of a form the input belongs to, outside it. | |
files | File[] | The files the input holds, controlled. | |
onFilesChange | (files: File[], details: FileSelectorChangeDetails) => void | Called with what a pick chose, replacing the input's files, or the accepted files of a drop. | |
onFilesReject | ( rejections: FileSelectorRejection[], details: FileSelectorChangeDetails ) => void | Files a pick or a drop brought that the selector didn't take. |
FileSelectorSourcetype
type FileSelectorSource = "picker" | "drop"FileSelectorChangeDetailstype
| Member | Type | Default | Description |
|---|---|---|---|
source* | FileSelectorSource | Whether the files came from the file picker or a drop. |
FileSelectorRejectiontype
| Member | Type | Default | Description |
|---|---|---|---|
file* | File | ||
reason* | "accept" | "multiple" | "accept": the type doesn't match accept; "multiple": several files came without multiple. |
SLDS mapping
The file selector is SLDS's file selector as lightning-input with type="file" draws it. FileSelector is slds-file-selector with slds-file-selector_files, _images or _integrated. FileSelectorDropzone is slds-file-selector__dropzone, with slds-has-drag-over as data-drag-over and slds-has-drag as data-dragging. FileSelectorInput is slds-file-selector__input slds-assistive-text, and FileSelectorBody is the slds-file-selector__body label. FileSelectorButton is slds-file-selector__button slds-button_neutral with the upload icon, FileSelectorText is slds-file-selector__text, and FileSelectorIcon is slds-file-selector__body-icon.
Known gaps:
- The upload icons are lucide's, where SLDS draws
utility:upload. - No "Dragover with error" look: a drop of the wrong type is only reported when it lands, since a drag doesn't tell the page the files' names.
- No file card (
slds-file) for files already chosen. List them with Item.
| Prop | Value | SLDS | Notes |
|---|---|---|---|
variant | files (added) | slds-file-selector slds-file-selector_files | The default: the Upload Files button with the drop text beside it, in a dashed drop zone. |
variant | images (added) | slds-file-selector slds-file-selector_images | A larger drop zone with the text under the button, for one image. |
variant | integrated (added) | slds-file-selector slds-file-selector_integrated, slds-file-selector__dropzone_integrated, slds-file-selector__body_integrated | An invisible drop zone over its container that shows while files are dragged over the window. Drop only: its input isn't a tab stop. |
(dragging over) | data-drag-over (added) | slds-has-drag-over | On FileSelectorDropzone while files are dragged over it: a 2px solid accent border. |
(dragging in the window) | data-dragging (added) | slds-has-drag | On an integrated FileSelector and its drop zone while files are dragged anywhere over the window. |
disabled | true (added) | slds-file-selector__input[disabled] | The body and button dim, and the drop zone takes no drop. |
aria-invalid | true (added) | slds-has-error (on the form element) | On FileSelectorInput. SLDS draws no error on the drop zone; FieldError shows the message. |
Accessibility
- Name the field with a
FieldLabelwhosehtmlForisFileSelector'sid. The body is a second label, so the input's name is the field label followed by "Upload Files or Drop Files". - The input is the control: Tab reaches it, its focus rings the button, and Space opens the file picker. The drop zone is never focusable, so dropping is never the only way to pick.
- An integrated drop zone is drop only and hidden from assistive technology. Keep a files or images selector, or another way to pick, in its container.
- For an error, set
aria-invalidandaria-describedbyonFileSelectorInput, as for any field. The drop zone doesn't change; the message is the error.
Responsive behavior
- Below 768px the drop text is hidden and only the button shows, as on a phone nothing is dropped.
Rules
- Keep
FileSelectorInputandFileSelectorBodyinsideFileSelectorDropzone, the input first: the button's focus and disabled looks come from the input beside it. - Keep the button a
FileSelectorButtoninside the body. It isn't abutton: the body is the label that opens the picker, and a real button inside a label would take the click. - Check files again where they're uploaded.
acceptonly filters what people pick and drop here. - Never pass
asChildorrender.