Skip to main content
On this page

File Field

A field for choosing files to add, with a list showing each file and how it is getting on.

When to use​

  • When users add one or more files as part of a journey, such as a bill, a statement or a photo of a meter
  • When you want to show each chosen file, and optionally its progress or result, while your app processes it
  • When users need to remove a file again, or retry one that failed

When not to use​

  • Choosing from a set of existing options - use Select Field, Combobox Field, Radio or Checkbox. File Field is for adding new files.
  • Showing a file the user cannot change - use Summary List to show the name and let them download it.
  • A form that must send the files itself - File Field does not put its files in the form. Your app sends them, from the list the field reports. See What gets submitted.

Anatomy​

  1. Label
  2. Hint text (optional)
  3. Drop zone: the upload icon, the prompt, the Select files button and the built-in hint, built from acceptedTypes, maxSize and maxFiles
  4. File list: one card per file, with its name, size, progress bar, status, description and buttons
  5. Error text (optional)

Basic usage​

File Field keeps the list of files the user chooses, and shows each one with a button to remove it. Without multiple it holds one file, and choosing another replaces it.

Loading...
Edit live code
<FileField id="documents" label="Documents" />

File Field does not upload or process anything. To do that, see Processing files.

Variants and features​

Several files​

Add multiple to let users choose more than one file at a time.

Loading...
Edit live code
<FileField
  id="statements"
  label="Statements"
  hint="Add one for each account"
  multiple
/>

Limiting the number of files​

maxFiles is the most files the field holds in total, counting those already chosen, and needs multiple. The built-in hint states it ("Up to 3 files."), and a file over the limit is not added.

Loading...
Edit live code
<FileField id="bills" label="Recent bills" multiple maxFiles={3} />

Limiting file types​

acceptedTypes takes MIME types ('application/pdf'), wildcards ('image/*') or extensions ('.docx'). The built-in hint lists them ("Accepted files: PDF, PNG."), and a file of another type is not added.

Loading...
Edit live code
<FileField
  id="proof"
  label="Proof of address"
  multiple
  acceptedTypes={['application/pdf', 'image/png']}
/>

Browsers sometimes leave a file's type blank (a .csv, for example), and a blank type matches no MIME entry. List the extension too for any such type.

Limiting file size​

maxSize is the largest size of one file, in bytes, so write megabytes as a multiplication: 5 * 1024 * 1024 is 5 MB. Keep the multiplier in a named constant if you use it more than once.

const MB = 1024 * 1024;

<FileField id="photo" label="Photo of the meter" maxSize={2 * MB} />;

The built-in hint shows the size in the largest unit that fits, counting 1024 bytes to a KB and 1024 KB to a MB, so maxSize={1.5 * MB} reads "1.5 MB".

Loading...
Edit live code
<FileField
  id="photo"
  label="Photo of the meter"
  acceptedTypes={['image/*']}
  maxSize={2 * 1024 * 1024}
/>

Processing files​

File Field does not upload or process anything. It keeps the list and shows the state you give it for each file. Start the work from onAdd, which is called once for each new file, and report how each one is getting on through fileState, keyed by the file's id.

Loading...
Edit live code
() => { // ignore - for live code example only.
  const Example = () => {
    const [files, setFiles] = useState([]);
    const [fileState, setFileState] = useState({});

    const update = (id, next) =>
      setFileState((current) => ({ ...current, [id]: next }));

    // Stands in for your upload: reports progress, then completes.
    const start = (selected) => {
      const tick = (progress) => {
        if (progress >= 100) {
          update(selected.id, { status: 'complete' });
          return;
        }
        update(selected.id, { status: 'in-progress', progress });
        setTimeout(() => tick(progress + 25), 500);
      };
      tick(0);
    };

    return (
      <FileField
        id="documents"
        label="Documents"
        hint="Bills, statements or meter photos"
        multiple
        maxFiles={4}
        acceptedTypes={['application/pdf', 'image/png']}
        maxSize={5 * 1024 * 1024}
        files={files}
        onFilesChange={setFiles}
        fileState={fileState}
        onAdd={start}
        onRemove={({ id }) =>
          setFileState(({ [id]: _removed, ...rest }) => rest)
        }
      />
    );
  };

  return <Example />;
}

Showing progress and results​

fileState is optional and is never worked out by the component. A file with no entry is a plain row with just Remove. Each entry has a status (pending, in-progress, complete, warning, failed or cancelled), an optional progress from 0 to 100 (leave it out for an indeterminate bar), a description for the reason or next step, and one extra action, such as Retry on a failed file.

Loading...
Edit live code
() => { // ignore - for live code example only.
  const files = [
    { id: 'a', file: null, name: 'bill.pdf', size: 120 * 1024 },
    { id: 'b', file: null, name: 'statement.pdf', size: 2.4 * 1024 * 1024 },
    { id: 'c', file: null, name: 'scan.pdf', size: 12 * 1024 * 1024 },
    { id: 'd', file: null, name: 'meter.png', size: 800 * 1024 },
  ];
  const fileState = {
    a: { status: 'complete' },
    b: { status: 'in-progress', progress: 40 },
    c: {
      status: 'failed',
      description: 'The file is too large. Choose one under 10 MB.',
      action: { id: 'retry', label: 'Retry', onSelect: () => undefined },
    },
    d: { status: 'warning', description: 'Some details could not be read.' },
  };

  return (
    <FileField
      id="documents-states"
      label="Documents"
      multiple
      files={files}
      fileState={fileState}
    />
  );
}

Progress you cannot measure​

Leave progress out (or null) while a file is pending or in-progress, and its card shows an indeterminate bar that keeps moving until you report a status. Use it when your work gives no progress, such as a fetch upload.

Loading...
Edit live code
<FileField
  id="indeterminate"
  label="Documents"
  multiple
  files={[{ id: 'a', file: null, name: 'bill.pdf', size: 120 * 1024 }]}
  fileState={{ a: { status: 'in-progress' } }}
/>

Errors on files and on the field​

An error belongs either to one file or to the field as a whole.

An error on a file is about that file, and sits on its card. Set status: 'failed' with a description that says what went wrong and what to do about it, and an optional action such as Retry. A file the field refuses (the wrong type, too big, or over the limit) never gets a card: a message at the drop zone names it and says why, and stays until the next selection. The field writes that message itself.

An error on the field is about what only your app knows, such as "Add at least one document" or "Add a statement for each account". Pass an error message, or put the field in a Form and give it a name that matches the form error. The message appears between the drop zone and the file list, so it stays next to the button it describes however many files there are. The drop zone gets an error border, and the message is read out with the Select files button.

Use a file's own error for a problem the user can fix on that file, and the field's error for the field as a whole. Do not repeat a file's failure as a field error: the card already says it. The field's error does not clear itself, so remove it once the user has put it right. Both can show at once:

Loading...
Edit live code
<FileField
  id="documents-errors"
  label="Documents"
  hint="Bills, statements or meter photos"
  multiple
  files={[
    { id: 'a', file: null, name: 'bill.pdf', size: 120 * 1024 },
    { id: 'b', file: null, name: 'scan.pdf', size: 12 * 1024 * 1024 },
  ]}
  fileState={{
    a: { status: 'complete' },
    b: {
      status: 'failed',
      description: 'The file is too large. Choose one under 10 MB.',
      action: { id: 'retry', label: 'Retry', onSelect: () => undefined },
    },
  }}
  error="Add a statement for each account"
/>

Cancelling files in flight​

A file that is pending or in-progress shows Cancel only if you pass onCancel. Cancelling does not remove the file: your app decides whether to stop the work and mark the file cancelled, or to remove it. If your processing cannot be cancelled, pass no onCancel. There is then no button until the file settles, so make sure every file does, for example by setting failed after a timeout.

In this example the work can be stopped, so it passes onCancel. Choose a file, then cancel it while it is in progress; the file stays in the list as cancelled, with Remove. The example under Processing files passes no onCancel, so its files have no button until they finish.

Loading...
Edit live code
() => { // ignore - for live code example only.
  const Example = () => {
    const [files, setFiles] = useState([]);
    const [fileState, setFileState] = useState({});
    const [timers] = useState({});

    const update = (id, next) =>
      setFileState((current) => ({ ...current, [id]: next }));

    // Stands in for work that can be stopped.
    const start = (selected) => {
      const tick = (progress) => {
        if (progress >= 100) {
          update(selected.id, { status: 'complete' });
          return;
        }
        update(selected.id, { status: 'in-progress', progress });
        timers[selected.id] = setTimeout(() => tick(progress + 10), 800);
      };
      tick(0);
    };

    const cancel = (selected) => {
      clearTimeout(timers[selected.id]);
      update(selected.id, { status: 'cancelled' });
    };

    return (
      <FileField
        id="cancellable"
        label="Documents"
        multiple
        files={files}
        onFilesChange={setFiles}
        fileState={fileState}
        onAdd={start}
        onCancel={cancel}
        onRemove={({ id }) =>
          setFileState(({ [id]: _removed, ...rest }) => rest)
        }
      />
    );
  };

  return <Example />;
}

Controlled and uncontrolled​

Pass files and onFilesChange together to control the list, as in Processing files. Leave files out and the field keeps the list itself; read it from onFilesChange (once per batch) or onAdd (once per file). onChange is the native change event, called first. The field's own input is never a way to read the files: it is always empty. See What gets submitted.

Uncontrolled: the field keeps the list, and the app reads it from onFilesChange.

Loading...
Edit live code
() => { // ignore - for live code example only.
  const Example = () => {
    const [names, setNames] = useState([]);

    return (
      <Stack spaceBetween={3}>
        <FileField
          id="uncontrolled"
          label="Documents"
          multiple
          onFilesChange={(files) => setNames(files.map((file) => file.name))}
        />
        <P>Chosen: {names.length ? names.join(', ') : 'none'}</P>
      </Stack>
    );
  };

  return <Example />;
}

Controlled: the app keeps the list, so it can change it, here by clearing it.

Loading...
Edit live code
() => { // ignore - for live code example only.
  const Example = () => {
    const [files, setFiles] = useState([]);

    return (
      <Stack spaceBetween={3}>
        <FileField
          id="controlled"
          label="Documents"
          multiple
          files={files}
          onFilesChange={setFiles}
        />
        <SecondaryCTAButton onClick={() => setFiles([])}>
          Clear all files
        </SecondaryCTAButton>
      </Stack>
    );
  };

  return <Example />;
}

What gets submitted​

File Field does not put anything in a form. The native file input inside it is hidden and cleared after every choice, and dropped files never reach it, so a native form post, or new FormData(form), has no files for this field. Build what you send from the list the field reports, as with the other fields in a Form.

With processing that starts as soon as a file is chosen, send what each file produced:

const attachments = files
.map((file) => fileState[file.id]?.payload)
.filter((payload) => payload != null);

If nothing happens until submit, read the File objects from files and build your own request. Either way, stop the form submitting while any file is still being processed:

const isProcessing = Object.values(fileState).some(
(state) => state.status === 'pending' || state.status === 'in-progress',
);

Common form features​

File Field shares validation, hint text, required/optional marking and field width behaviour with other form fields. See the forms pattern guide for guidance on these topics.