PlFilePicker
A box files are chosen into. It checks what arrives against accept, maxSize and maxFiles, and tells you about everything it turned away.
import { PlFilePicker } from 'plass-ui';
<PlFilePicker label="Attachments" multiple maxFiles={4} value={files} onFilesChange={setFiles} />;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
| Prop | Type | Default | Description |
|---|---|---|---|
| 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 |
| elevationshared | 0 | 1 | 2 | 3 | 0 | Drop shadow depth. 0 means no shadow at all |
| accept | string | — | Which files the browser's dialog 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 — checked against what is already chosen, not against one drop |
| value | readonly File[] | — | The chosen files. Use with onFilesChange for a controlled picker |
| defaultValue | readonly File[] | — | The initially chosen files |
| onFilesChange | (files: File[]) => void | — | Called with the new list |
| onReject | (rejections: PlFileRejection[]) => void | — | Called with everything turned away and why. Without it a rejected file disappears silently, which is the worst thing a dropzone does |
| label · description · error · invalid | ReactNode · ReactNode · ReactNode · boolean | — | The label above the box, the helper text below it, and the error. The error also turns the picker invalid |
| title | ReactNode | 'Drop files here, or click to browse' | 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 | `Remove {name}` | Accessible name of a file's remove button |
| fullWidth | boolean | true | Stretches to the width of the container |
| readOnly | boolean | false | The files are shown but cannot be added to or removed |
| disabled | boolean | false | Unavailable |
| name · required · id | string · boolean · string | — | For a native form submission and for wiring a label |
| Prop | Type | Default | Description |
|---|---|---|---|
| value * | List<PlFile> | — | The chosen files. Use with onFilesChange for a controlled picker |
| onFilesChanged | ValueChanged<List<PlFile>>? | — | Called with the list that should be held next — a file added, or one removed from the list |
| onBrowse | Future<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 |
| onRejected | ValueChanged<List<PlFileRejection>>? | — | Called with everything turned away and why. Without it a rejected file disappears silently, which is the worst thing a dropzone does |
| accept | String? | — | 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 |
| multiple | bool | false | Whether more than one file may be chosen |
| maxSize | int? | — | The largest a single file may be, in bytes |
| maxFiles | int? | — | How many files may be held at once — checked against what is already chosen, not against one drop |
| dragging | bool | false | Whether 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 · invalid | Widget? · Widget? · Widget? · bool? | — | The label above the box, the helper text below it, and the error. The error also turns the picker invalid |
| title | Widget? | Text('Choose files') | The line inside the box |
| hint | Widget? | — | The line under it — what is accepted, how big, how many |
| icon | Widget? | — | The glyph above the title. The upload mark if it is left out |
| showIcon | bool | true | Draws a glyph at all. Dart has no value that is neither null nor a widget, so "take it away" gets its own name |
| showList | bool | true | Lists the chosen files under the box, each with a way to remove it |
| removeLabel | String Function(String name) | 'Remove {name}' | Accessible name of a file's remove button |
| variantshared | PlassVariant | PlassVariant.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 | PlassSize | PlassSize.md | The box's padding and the type scale of the text inside it |
| colorshared | PlassColor | PlassColor.primary | Semantic colour role. Arbitrary colour values are not accepted |
| densityshared | PlassDensity | PlassDensity.standard | Padding only — never the height, never the type scale |
| elevationshared | int | 0 | Drop shadow depth. 0 means no shadow at all |
| fullWidth | bool | true | Stretches to the width of the container |
| readOnly | bool | false | The files are shown but cannot be added to or removed |
| disabled | bool | false | Unavailable |
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.
| Prop | Type | Default | Description |
|---|---|---|---|
| name * | String | — | What it is called, extension and all |
| size * | int | — | How many bytes it is |
| mimeType | String? | — | Its kind — image/png. Left out, only the extension is checked against accept |
| source | Object? | — | Whatever the app's own picker handed over. The picker never looks at it |
| readableSize | String | — | 1.4 MB, in the units a person reading a file list expects. Base 1000 rather than 1024 |
| matches | bool 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.
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.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.
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.
disabled · error
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 thandisplay: none. The latter is unfocusable in some browsers and would take the input out of native form validation. descriptionanderrorare wired to the button witharia-describedby; the error also setsaria-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.
errorre-points the whole family atdanger, so the edge, the ring and the message all turn over together.
Differences from the React build
| React | Flutter | Why |
|---|---|---|
| opens the file dialog itself | onBrowse runs the app's picker | There 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 drop | dragging, which the app sets | There is no OS-level drag either. The look of the state is the component's; the detection is the app's. |
File | PlFile | A name, a size, a kind and the app's own object carried through. The package opens nothing. |
formatFileSize, exported | PlFile.readableSize | The same number, on the thing that has it. |
value / defaultValue / onFilesChange | value / onFilesChanged | Flutter's own controls are controlled, and its name for the callback. |
icon={null} | showIcon: false | Dart has no value that is neither null nor a widget, so "take it away" gets its own name. |
the hidden input, name, required | — | There is no native form submission to be part of. |
id, aria-describedby, aria-invalid | — | Nothing points at anything by id here; the label and the messages are part of the component. |
className, style, native attributes | — | There is no class list and no style attribute to pass through. |