Skip to content

PlFilePicker

A box files are chosen into. It checks what arrives against accept, maxSize and maxFiles, and tells you about everything it turned away.

React
tsx
import { PlFilePicker } from 'plass-ui';

<PlFilePicker label="Attachments" multiple maxFiles={4} value={files} onFilesChange={setFiles} />;
dart
import 'package:plass_ui/plass_ui.dart';

PlFilePicker(
  label: const Text('Attachments'),
  multiple: true,
  maxFiles: 4,
  value: files,
  onBrowse: () async => myPickerPlugin.pick(),
  onFilesChanged: (List<PlFile> next) => setState(() => files = next),
);

The picker does not pick. This package has no dependencies, and reaching the file system is a plugin's job in every Flutter app that does it, so onBrowse is where the app's own picker runs. What the component owns is everything after that: the rules, the list, the removal, and the box itself.

Props

PropTypeDefaultDescription
variantshared'solid' | 'glass' | 'ghost''glass'What the box is made of. All three take a dashed edge, because that is the established sign for an area that accepts a drop
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The box's padding and the type scale of the text inside 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
elevationshared0 | 1 | 2 | 30Drop shadow depth. 0 means no shadow at all
acceptstringWhich files the browser's dialog offers ('image/*,.pdf'). Dropped files are checked against it too, which the attribute alone does not do
multiplebooleanfalseWhether more than one file may be chosen
maxSizenumberThe largest a single file may be, in bytes
maxFilesnumberHow many files may be held at once — checked against what is already chosen, not against one drop
valuereadonly File[]The chosen files. Use with onFilesChange for a controlled picker
defaultValuereadonly File[]The initially chosen files
onFilesChange(files: File[]) => voidCalled with the new list
onReject(rejections: PlFileRejection[]) => voidCalled with everything turned away and why. Without it a rejected file disappears silently, which is the worst thing a dropzone does
label · description · error · invalidReactNode · ReactNode · ReactNode · booleanThe label above the box, the helper text below it, and the error. The error also turns the picker invalid
titleReactNode'Drop files here, or click to browse'The line inside the box
hintReactNodeThe line under it — what is accepted, how big, how many
iconReactNodeThe glyph above the title. Pass null for a box with no picture in it
showListbooleantrueLists the chosen files under the box, each with a way to remove it
removeLabel(name: string) => string`Remove {name}`Accessible name of a file's remove button
fullWidthbooleantrueStretches to the width of the container
readOnlybooleanfalseThe files are shown but cannot be added to or removed
disabledbooleanfalseUnavailable
name · required · idstring · boolean · stringFor a native form submission and for wiring a label
PropTypeDefaultDescription
value * List<PlFile>The chosen files. Use with onFilesChange for a controlled picker
onFilesChangedValueChanged<List<PlFile>>?Called with the list that should be held next — a file added, or one removed from the list
onBrowseFuture<List<PlFile>> Function()?Runs the app's own file picker and hands back what it found. What it returns is checked against the rules, and what survives is reported through onFilesChanged
onRejectedValueChanged<List<PlFileRejection>>?Called with everything turned away and why. Without it a rejected file disappears silently, which is the worst thing a dropzone does
acceptString?Which files are kept ('image/*,.pdf'). Applied to whatever onBrowse hands back: a rule the component states and does not enforce is not a rule
multipleboolfalseWhether more than one file may be chosen
maxSizeint?The largest a single file may be, in bytes
maxFilesint?How many files may be held at once — checked against what is already chosen, not against one drop
draggingboolfalseWhether a file is over the box. There is no OS-level drag in Flutter without a plugin, so an app that has one tells it and the box lights the way it should
label · description · error · invalidWidget? · Widget? · Widget? · bool?The label above the box, the helper text below it, and the error. The error also turns the picker invalid
titleWidget?Text('Choose files')The line inside the box
hintWidget?The line under it — what is accepted, how big, how many
iconWidget?The glyph above the title. The upload mark if it is left out
showIconbooltrueDraws a glyph at all. Dart has no value that is neither null nor a widget, so "take it away" gets its own name
showListbooltrueLists the chosen files under the box, each with a way to remove it
removeLabelString Function(String name)'Remove {name}'Accessible name of a file's remove button
variantsharedPlassVariantPlassVariant.glassWhat the box is made of. All three take a dashed edge, because that is the established sign for an area that accepts a drop
sizesharedPlassSizePlassSize.mdThe box's padding and the type scale of the text inside it
colorsharedPlassColorPlassColor.primarySemantic colour role. Arbitrary colour values are not accepted
densitysharedPlassDensityPlassDensity.standardPadding only — never the height, never the type scale
elevationsharedint0Drop shadow depth. 0 means no shadow at all
fullWidthbooltrueStretches to the width of the container
readOnlyboolfalseThe files are shown but cannot be added to or removed
disabledboolfalseUnavailable

Every native <div> attribute passes straight through to the wrapper. color, defaultValue, title and children are excluded because all four are Plass props here.

formatFileSize is exported alongside the component, so a caller writing their own list can print sizes in the same units.

Controlled: value and onFilesChanged are how a picker is driven, always.

PlFile

PlFile is not in the React package yet.

PropTypeDefaultDescription
name * StringWhat it is called, extension and all
size * intHow many bytes it is
mimeTypeString?Its kind — image/png. Left out, only the extension is checked against accept
sourceObject?Whatever the app's own picker handed over. The picker never looks at it
readableSizeString1.4 MB, in the units a person reading a file list expects. Base 1000 rather than 1024
matchesbool Function(String accept)Whether it matches an accept string — all three forms: .ext, type/subtype, type/*

A PlFile is deliberately not a dart:io File and not an abstraction over one. What the box draws is a name and a size, and what its rules read is a name, a size and a kind, so that is what it asks for; source carries the app's own object through untouched, so the caller gets it back on the other side.

readableSize prints 1.4 MB in the units a person reading a file list expects, and matches is the accept check, so a caller writing their own list can use both.

What the shared axes (variant size color density elevation) mean across the library is in prop conventions.

Examples

variant

All three take a dashed edge, and it is the one place the library draws a line that is not solid. It is not decoration: a dashed rectangle is the established sign for "this area accepts a drop", and a dropzone that looks like a PlCard is a PlCard nobody tries to drop on.

The edge is neutral at rest and takes the colour family only once the pointer is on it. The same arrangement a glass PlButton has.

React

accept · maxSize · maxFiles

`accept` is set on the input **and** applied to drops. The browser enforces the attribute on its own dialog and on nothing else, so a dropzone that only sets it accepts anything the moment a file arrives by drag.`accept` is applied to whatever `onBrowse` hands back, whether or not the plugin that found it was told the same thing. A rule the component states and does not enforce is not a rule.

maxFiles is counted against what is already held rather than against one drop. The difference between "you may drop five files" and "you may end up with five files", and only the second is what the prop means.

onRejectonRejected is where a refusal goes. Without it a rejected file disappears silently, which is the single worst thing a dropzone does.
React

One file at a time

Without multiple the box holds exactly one file, and a new one replaces it rather than being turned away for count. That is what an avatar picker wants.

React

size

Moves the box's padding and the text inside it. The padding has its own ladder rather than the sheet's, because a dropzone is sized by the gesture it has to catch: a target the height of one line of text is a target you miss.

React

disabled · error

React

Accessibility

  • The pressable area is a real <button>, so it is in the tab order and answers Enter and Space. Drag-and-drop is an addition to that, never the only way in.
  • The <input type="file"> stays in the DOM, clipped off-screen rather than display: none. The latter is unfocusable in some browsers and would take the input out of native form validation.
  • description and error are wired to the button with aria-describedby; the error also sets aria-invalid.
  • The file list is a real <ul> outside the browse button, because a remove button cannot be nested inside another button.
  • Each remove button carries an accessible name that includes the file it removes, so a screen reader hears three different buttons rather than three called "Remove".
  • The zone does not move under the pointer while a file is over it. Colour and edge change; nothing grows or lifts, because a target that moves while you are aiming at it is a target you miss.
  • The box is announced as a button, so it is in the focus order and answers Enter and Space. Whatever drop handling an app adds is an addition to that, never the only way in.
  • The file list is outside the box, because a remove button inside a button is a press that fires twice.
  • Each remove button carries a name that includes the file it removes, so a screen reader hears three different buttons rather than three called "Remove".
  • The box does not move while a file is over it. Colour and edge change; nothing grows or lifts, because a target that moves while you are aiming at it is a target you miss.
  • error re-points the whole family at danger, so the edge, the ring and the message all turn over together.

Differences from the React build

ReactFlutterWhy
opens the file dialog itselfonBrowse runs the app's pickerThere is no file dialog in Flutter without a plugin, and this package has no dependencies. The rules stay here; the picker is the app's.
drag and dropdragging, which the app setsThere is no OS-level drag either. The look of the state is the component's; the detection is the app's.
FilePlFileA name, a size, a kind and the app's own object carried through. The package opens nothing.
formatFileSize, exportedPlFile.readableSizeThe same number, on the thing that has it.
value / defaultValue / onFilesChangevalue / onFilesChangedFlutter's own controls are controlled, and its name for the callback.
icon={null}showIcon: falseDart has no value that is neither null nor a widget, so "take it away" gets its own name.
the hidden input, name, requiredThere is no native form submission to be part of.
id, aria-describedby, aria-invalidNothing points at anything by id here; the label and the messages are part of the component.
className, style, native attributesThere is no class list and no style attribute to pass through.

Released under the MIT License