File drop
StableA file picker whose real control is a “Choose file” button; dropping a file on the zone is a shortcut, never the only way.
The button opens the system file dialog, so it works by keyboard, by voice and on touch screens, where the drop hint is hidden. Type and size are checked the moment a file is picked, and a problem is shown under the label and announced in GOV.UK’s wording: “The selected file must be a CSV or JSON”. preview="image" shows the picked picture in a tile.
Import a file
import { useState } from 'react'import { FileDrop } from '@rchursin/product/forms'
/** * “Choose file” opens the file dialog; dropping a file on the dashed zone does the same. Type and * size are checked at once: try an image to see the error. Nothing is uploaded in this sample. */export default function FileDropImport() { const [file, setFile] = useState<File | null>(null)
return <FileDrop label="Import runs" accept={['text/csv', 'application/json']} maxBytes={5 * 1024 * 1024} value={file} onChange={setFile} className="w-full max-w-md" />}Usage
import { FileDrop, type FileDropProps } from '@rchursin/product/forms'<FileDrop label="Import runs" accept={['text/csv']} maxBytes={5 * 1024 * 1024} value={file} onChange={setFile}/>Examples
Image preview
preview="image" shows the picked picture; fallback fills the tile until then.
import { useState } from 'react'import { FileDrop } from '@rchursin/product/forms'
/** preview="image" shows the picked picture in a tile; `fallback` fills the tile until then. */export default function FileDropAvatar() { const [picture, setPicture] = useState<File | null>(null)
return ( <FileDrop label="Profile picture" optional accept={['image/png', 'image/jpeg', 'image/webp']} maxBytes={2 * 1024 * 1024} buttonLabel="Choose image" preview="image" value={picture} onChange={setPicture} fallback={ <span aria-hidden="true" className="text-xl font-semibold tracking-tight text-fg-2"> AR </span> } className="w-full max-w-md" /> )}An error from the form
Press Import with no file: the zone turns solid, the message appears, and focus moves to Choose file.
import { useId, useState, type FormEvent } from 'react'import { FileDrop } from '@rchursin/product/forms'import { Button } from '@rchursin/product/ui'
/** * A form's own error goes in `error` (it replaces the picker's type and size messages). Press * Import with no file: the zone turns solid, the message appears, and focus returns to the button. */export default function FileDropRequired() { const id = useId() const [file, setFile] = useState<File | null>(null) const [error, setError] = useState<string>()
function submit(e: FormEvent) { e.preventDefault() if (file) return setError(undefined) setError('Select a CSV file to import') document.getElementById(id)?.focus() }
return ( <form noValidate onSubmit={submit} className="flex w-full max-w-md flex-col items-start gap-4"> <FileDrop id={id} label="Usage export" accept={['text/csv']} maxBytes={10 * 1024 * 1024} value={file} onChange={(next) => { setFile(next) setError(undefined) }} error={error} className="w-full" /> <Button type="submit">Import</Button> </form> )}API
File drop props
| Prop | Type | Default | Description |
|---|---|---|---|
labelrequired | ReactNode | – | Names the whole control. |
acceptrequired | string[] | – | Accepted MIME types, e.g. ["text/csv"], written into the hint as “CSV or JSON”. For the common types (images, PDF, CSV, JSON) a matching extension passes too, because Windows with Excel reports a .csv as application/vnd.ms-excel. |
maxBytesrequired | number | – | The largest file allowed, in bytes. Shown in the hint as “up to 5 MB”. |
valuerequired | File | null | – | The picked file. |
onChangerequired | (file: File | null) => void | – | Called with a file that passed the checks, or null after Remove. |
id | string | – | id of the Choose button, so an error summary can link to it. |
buttonLabel | string | 'Choose file' | The button’s words. Say what to pick: “Choose image”. |
preview | 'image' | 'file' | 'file' | image adds a 72 pixel tile with the picked picture; file shows only the name and size. |
fallback | ReactNode | – | Fills the image tile before anything is picked, e.g. the current avatar. |
error | string | – | An error from the form. Shown instead of the picker’s own type and size messages. |
optional | boolean | – | Adds “(optional)” to the label. |
disabled | boolean | – | Dims the zone and turns off the buttons and dropping. |
className | string | – | Classes for the outer group. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| EnterSpace | On Choose file: opens the system file dialog. |
| Tab | Moves from Choose file to Remove once a file is picked. |
- The control is a group named by its label. Choose file is a real button whose aria-describedby points at the hint (types and size) and, when there is one, the error.
- A type or size problem appears as error text with an icon and a hidden “Error:”, and is announced through the AnnouncerProvider at the root of the app. Picking a file announces “<name> selected”.
- Remove carries the file’s name (“Remove runs.csv”); afterwards it announces “File removed” and puts focus back on Choose file.
- The zone shows its state by shape: dashed while waiting, a solid 2 pixel outline while a file is over it, a firmer outline with an error. The drop hint shows only on devices with a fine pointer.
Guidelines
Do
- Accept only the types you can use, and say the size limit (the hint does both).
- Check type and size again on the server.
- Name the button after the thing to pick: “Choose image”.
Don’t
- Don’t make dragging the only way to add a file.
- Don’t upload the moment a file is picked if people can’t undo it; let them press a button.
In use
Related
- Account settingsThe in-app settings form: profile with a picture and time zone, a password change, passkeys and sessions, with an error summary on save.
- Error summaryA “There is a problem” box at the top of a form that takes focus after a failed submit and links each message to its field.
- ButtonThe lit-metal button in five finishes and five sizes, with icons, a loading state and shortcut hints.
Source
The component’s own files, exactly as they run on this site.
/** * File picker with a drop zone. The real control is a "Choose …" button that opens the native * file dialog, so it works by keyboard and screen reader; dropping a file on the dashed zone is * a shortcut for pointer users, never the only way. * * <FileDrop label="Profile picture" accept={['image/png', 'image/jpeg']} maxBytes={2_000_000} * value={file} onChange={setFile} preview="image" fallback={<Avatar … />} /> * * Type and size are checked as soon as a file is picked; the problem is shown under the label * and announced. Wording follows GOV.UK: "The selected file must be a PNG, JPG or WebP". */import { useEffect, useId, useRef, useState, type DragEvent, type ReactNode } from "react"import { cx } from "../../components/cx"import { Icon } from "../icons/Icon"import { useAnnounce } from "../lib/announce"import { Button } from "../ui/button"import { ErrorText } from "../ui/fields"
const typeNames: Record<string, string> = { "image/png": "PNG",