# FileInput

> Package `@umriss-ui/core`, version 0.40.0. Demo page: <https://romanhaendler.github.io/umriss-ui/core/fileinput/>. Every page in one line: [llms.txt](https://romanhaendler.github.io/umriss-ui/core/llms.txt); every page in full: [llms-full.txt](https://romanhaendler.github.io/umriss-ui/core/llms-full.txt).

Choose files from the system's dialog or drop them on the zone (file picker, upload field). The chosen files are listed with their size and can be removed one by one.

```ts
import { FileInput } from "@umriss-ui/core";
```

Demo page: https://romanhaendler.github.io/umriss-ui/core/fileinput/

`accept` holds for the dialog and for a drop alike; a dropped file it refuses is named below the list instead of vanishing.

It only chooses. Reading, checking and uploading the files are the application's.

## Examples

### One file

Set `accept` for the types you take; it holds for the system's dialog and for a file dropped on the zone.

```tsx
import { FileInput, FormField } from "@umriss-ui/core";

export default function OneFile() {
  return (
    <FormField label="Bank statement" hint="The CSV export from online banking." style={{ maxWidth: 420 }}>
      <FileInput accept=".csv" />
    </FormField>
  );
}
```

### Several files

Set `multiple` to take several at once; pass `value` to start from files a record already carries.

```tsx
import { useState } from "react";
import { FileInput, FormField, Grid } from "@umriss-ui/core";

const ATTACHED = [
  new File([new Uint8Array(184_000)], "damaged-corner.jpg", { type: "image/jpeg" }),
  new File([new Uint8Array(1_320_000)], "delivery-note-100829-signed-by-the-consignee-at-the-rear-entrance.pdf", {
    type: "application/pdf",
  }),
];

export default function SeveralFiles() {
  const [attached, setAttached] = useState<File[]>(ATTACHED);

  return (
    <Grid minItemWidth="300px" gap={6}>
      <FormField label="Proof of delivery" hint="Photos or PDF.">
        <FileInput multiple accept="image/*,.pdf" />
      </FormField>
      <FormField label="Attachments" hint="Shipment 100829, failed attempt.">
        <FileInput multiple accept="image/*,.pdf" value={attached} onChange={setAttached} />
      </FormField>
    </Grid>
  );
}
```

### States

An `error` on the `FormField` colours the zone and is read as its description; `disabled` takes neither dialog nor drop.

```tsx
import { FileInput, FormField, Grid } from "@umriss-ui/core";

export default function States() {
  return (
    <Grid minItemWidth="280px" gap={6}>
      <FormField label="Postmortem" error="Attach the postmortem before closing INC-1048.">
        <FileInput accept=".pdf,.md" />
      </FormField>
      <FormField label="Runbook" hint="Locked while the incident is open.">
        <FileInput accept=".pdf,.md" disabled />
      </FormField>
    </Grid>
  );
}
```

### With an error

A `FormField` with an `error` marks the zone invalid by itself. Without one, set `invalid` and tie your message to the input with `aria-describedby`.

```tsx
import { useId, useState } from "react";
import { FileInput, Stack, Text } from "@umriss-ui/core";

export default function WithAnError() {
  const [files, setFiles] = useState<File[]>([]);
  const messageId = useId();
  const error = files.length === 0 ? "Attach the postmortem before closing INC-1048." : undefined;

  return (
    <Stack gap={1} style={{ maxWidth: 420 }}>
      <FileInput
        aria-label="Postmortem"
        accept=".pdf,.md"
        value={files}
        onChange={setFiles}
        invalid={error !== undefined}
        aria-describedby={error ? messageId : undefined}
      />
      {error && (
        <Text id={messageId} size="xs" style={{ color: "var(--u-color-danger-text)" }}>
          {error}
        </Text>
      )}
    </Stack>
  );
}
```

### Require a file before sending

Hold the files with `value` and `onChange`, and turn an empty choice into the field's error on submit.

```tsx
import { useState } from "react";
import type { FormEvent } from "react";
import { Button, FileInput, FormField, Stack, Text } from "@umriss-ui/core";

export default function RequireAFileBeforeSending() {
  const [files, setFiles] = useState<File[]>([]);
  const [tried, setTried] = useState(false);
  const [sent, setSent] = useState<string | null>(null);

  const send = (event: FormEvent) => {
    event.preventDefault();
    setTried(true);
    if (files.length === 0) return;
    setSent(files.map((f) => f.name).join(", "));
  };

  return (
    <form onSubmit={send} noValidate style={{ maxWidth: 420 }}>
      <Stack gap={3}>
        <FormField label="Receipts" required error={tried && files.length === 0 ? "Attach at least one receipt." : undefined}>
          <FileInput multiple accept=".pdf,image/*" value={files} onChange={setFiles} name="receipts" />
        </FormField>
        <Stack direction="row" gap={3} align="center">
          <Button type="submit" size="sm" variant="primary">
            Submit expenses
          </Button>
          {sent && (
            <Text size="sm" tone="muted">
              Would send: {sent}
            </Text>
          )}
        </Stack>
      </Stack>
    </form>
  );
}
```

### Import budgets

The input only chooses; reading the file with `File.text()`, checking it and importing it stay with your application.

```tsx
import { useState } from "react";
import { Alert, Button, Card, CardBody, CardHeader, FileInput, FormField, Stack, Stepper, Text } from "@umriss-ui/core";

interface Reading {
  name: string;
  rows: number;
}

export default function ImportBudgets() {
  const [files, setFiles] = useState<File[]>([]);
  const [reading, setReading] = useState<Reading | null>(null);
  const [imported, setImported] = useState(false);

  const choose = async (next: File[]) => {
    setFiles(next);
    setImported(false);
    const file = next[0];
    if (!file) return setReading(null);
    const lines = (await file.text()).split(/\r?\n/).filter((line) => line.trim() !== "");
    setReading({ name: file.name, rows: Math.max(0, lines.length - 1) }); // the first line is the head
  };

  const current = imported ? 3 : reading ? 1 : 0;
  const count = (rows: number) => (rows === 1 ? "1 cost centre" : `${rows} cost centres`);

  return (
    <Card style={{ maxWidth: 560 }}>
      <CardHeader eyebrow="Budget 2027" title="Import budgets" />
      <CardBody>
        <Stack gap={5}>
          <Stepper aria-label="Import" steps={[{ label: "Choose file" }, { label: "Check" }, { label: "Import" }]} current={current} />
          <FormField label="Budget sheet" hint="CSV, one cost centre per row, with a head line.">
            <FileInput accept=".csv" value={files} onChange={(next) => void choose(next)} />
          </FormField>
          {reading && !imported && (
            <Stack direction="row" gap={3} align="center">
              <Text size="sm">
                {count(reading.rows)} in {reading.name}
              </Text>
              <Button size="sm" variant="primary" disabled={reading.rows === 0} onClick={() => setImported(true)}>
                Import
              </Button>
            </Stack>
          )}
          {imported && reading && (
            <Alert tone="success" title="Imported">
              {count(reading.rows)} from {reading.name} now carry a 2027 budget.
            </Alert>
          )}
        </Stack>
      </CardBody>
    </Card>
  );
}
```

## Keyboard

| Key | Action |
|---|---|
| `Space or Enter` | Opens the system's file dialog. |

## API

### `FileInputProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `value` | `readonly File[]` | — | Controlled: the chosen files. Uncontrolled, the input keeps its own and starts empty.<br>Shown in: [Several files](https://romanhaendler.github.io/umriss-ui/core/fileinput/#several-files), [With an error](https://romanhaendler.github.io/umriss-ui/core/fileinput/#with-an-error), [Require a file before sending](https://romanhaendler.github.io/umriss-ui/core/fileinput/#require-a-file-before-sending) and 1 more |
| `invalid` | `boolean` | — | Marks the input as invalid. `FormField` sets it itself as soon as it carries an `error` - by hand only necessary without `FormField`.<br>Shown in: [With an error](https://romanhaendler.github.io/umriss-ui/core/fileinput/#with-an-error) |
| `ref` | `Ref<HTMLInputElement>` | — | Receives the component's root element. From `RefProp`.<br>Shown in: [A row that holds still (Sizes)](https://romanhaendler.github.io/umriss-ui/core/sizes/#a-row-that-holds-still), [An anchored surface (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#anchored-surface), [Take the width of the trigger (Popover)](https://romanhaendler.github.io/umriss-ui/core/popover/#width-from-anchor) and 2 more |

#### Events

| Prop | Type | Default | Description |
|---|---|---|---|
| `onChange` | `(files: File[]) => void` | — | Reports the files after a choice, a drop or a removal - only those `accept` takes, and one where the input takes one.<br>Shown in: [Several files](https://romanhaendler.github.io/umriss-ui/core/fileinput/#several-files), [With an error](https://romanhaendler.github.io/umriss-ui/core/fileinput/#with-an-error), [Require a file before sending](https://romanhaendler.github.io/umriss-ui/core/fileinput/#require-a-file-before-sending) and 1 more |

Also takes every attribute of `<input>` – without `defaultValue`, `onChange`, `size`, `type`, `value`.

## Known limits

- No upload, no progress and no size limit of its own; those belong to the application sending the files.

What umriss deliberately does not build, and why: [ADR-0032](https://romanhaendler.github.io/umriss-ui/what-umriss-ui-is-not/).
