← All proposals

Dropzone

A net-new component, `Dropzone`:

Net-new componentOpen in Storybook

proposals/Dropzone/HANDOFF.md78 lines · View on GitHub

Handoff: Dropzone

What and why

A compact area to add files by dragging them in or by browsing, from the designer's reference: a dashed rounded outline holding an upload icon and one line of text, "Drop an image or brief, or browse". PDS has the File Upload, a form field with a "Choose file" button and no drag and drop, so this is a new component, not an extension of it. The Chat Panel and Chat Input accept drops but have no visible area to drop on.

  • Figma / Jira: none. Source: the designer's reference image (2026-10-21).
  • Context: context.md

Exploration and decisions

  • New component, not a File Upload variant: different anatomy and purpose (a form field with a validation message and a chosen file name, versus a compact drop area).
  • The sentence is two props, label and browseLabel, both required, so each half can be translated and browse is a real button.
  • Behavior: a drop or the dialog calls onFilesDrop(files); the files are filtered by accept (extensions, types and type/* wildcards, as on a file input) and cut to one unless multiple. While a file is over the zone the outline turns solid with the interactive focus color and the selected background. The hidden file input is reset after each change so the same file can be chosen again. The zone shows no files: the screen owns the list.
  • Drag counting: dragenter and dragleave fire for each child the pointer crosses, so they are counted (as in the Chat Panel).
  • Keyboard: dropping is a pointer shortcut; keyboard and screen reader users press browse. The zone itself is not focusable.

Full list: decisions.md.

API

DropzoneProps extends the props of a div (except onDrop):

Prop Type Default Purpose
accept string none Which files to accept, as on a file input (image/*,.pdf). Applies to the dialog and the drop.
browseLabel string required Text of the browse button ("browse"). Translated.
className string none Additional class names.
disabled boolean false Drops and the browse button do nothing; the zone is faded.
label string required The text before the browse button ("Drop an image or brief, or"). Translated.
multiple boolean false Allow more than one file at once.
onFilesDrop (files: File[]) => void none Called with the files from a drop or the dialog (filtered by accept).

Variants and states: default, drag over, disabled; the browse button has hover, active and focus. Stories: Default, Disabled, Interactive.

Tokens used

  • --pds-animation-transition-input
  • --pds-border-radius-default, --pds-border-radius-input
  • --pds-border-width-default, --pds-border-width-outline
  • --pds-color-border-default
  • --pds-color-foreground-default-secondary
  • --pds-color-interactive-background-selected, --pds-color-interactive-focus
  • --pds-color-interactive-link-active, --pds-color-interactive-link-default, --pds-color-interactive-link-hover
  • --pds-spacing-2xs, --pds-spacing-5xs, --pds-spacing-s, --pds-spacing-xs
  • --pds-typography-ff-default, --pds-typography-fw-semibold, --pds-typography-size-s

Impact

From impact.md: no existing PDS component is reached and no --pds-* token is redefined. No other proposal depends on it.

Reused vs net-new

  • Reused: the local Icon wrapper (upload), the visually-hidden class, the pds-copy-s class.
  • Net-new: .pds-dropzone with --drag-over, --disabled, __browse, __icon and __label; all props. See custom.md (no custom variables, no raw values).

Accessibility

  • Browse is a real button that opens the file dialog, with a visible focus ring; the file input is visually hidden and out of the tab order.
  • Dropping is a pointer-only shortcut. The zone has no role; the text and the button are what is announced.
  • Not handled: announcing that files were added (the screen owns the list and should announce it), and a drag-over announcement.
  • Contrast: the secondary text and the link color on the surface; check both in light and dark. The a11y panel has not been run, and dark mode was not checked by eye in a session.

Decisions and open questions

  • A validation or error state (a rejected type or size)? Not built; the screen can show a message below.
  • A size or file-count limit?
  • A taller, multi-line version for pages where the dropzone is the main content?
  • Should the drag-over highlight reuse the Chat Panel's drop overlay look?

Port notes

  • New component (npm run generate:component), probably next to the File Upload in src/components/inputs/. Swap the local Icon wrapper (the package has upload) and mergeClasses for the real ones. The matchesAccept helper is local; the port may move it to a util.

Preview

npm run storybook, then Component Proposals/Dropzone. Figma: none yet.