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.
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
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>
);
}
| Key | Action |
|---|---|
Space or Enter |
Opens the system's file dialog. |
FileInputProps| Name | Type | Default | Description |
|---|---|---|---|
value | readonly 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 |
invalid | boolean | — | 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 |
ref | Ref<HTMLInputElement> | — | Receives the component's root element. from RefPropShown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more |
| Name | 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.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.
What umriss deliberately does not build, and why: ADR-0032.