FileInput

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.

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

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.

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.

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.

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.

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.

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.

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

NameTypeDefaultDescription
valuereadonly File[]—Controlled: the chosen files. Uncontrolled, the input keeps its own and starts empty.

Shown in: Several files, With an error, Require a file before sending and 1 more

invalidboolean—Marks the input as invalid. FormField sets it itself as soon as it carries an error - by hand only necessary without FormField.

Shown in: With an error

refRef<HTMLInputElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Events

NameTypeDefaultDescription
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.

Shown in: Several files, With an error, Require a file before sending and 1 more

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

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index