File Input
An input component for selecting and uploading files
Unified Component (Mode-Based)
A single component with different modes for various use cases
Drag & Drop Mode
Drop images or PDFs here
Max 10MB per file
Regular Input Mode
Button-Only Mode
1
2<script lang="ts">
3 import { FileInput } from "@kareyes/aether";
4
5 let dragDropFiles: FileList | null = $state(null);
6 let regularFiles: FileList | null = $state(null);
7 let buttonFiles: FileList | null = $state(null);
8
9 function handleFilesChange(name: string) {
10 return (files: FileList | null) => {
11 console.log(`${name} files changed:`, files ? Array.from(files).map(f => f.name) : 'No files');
12 };
13 }
14
15 function handleError(name: string) {
16 return (error: string) => {
17 console.error(`${name} error:`, error);
18 };
19 }
20</script>
21
22<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
23 <div class="space-y-2">
24 <h3 class="text-sm font-medium">Drag & Drop Mode</h3>
25 <FileInput
26 mode="drag-drop"
27 validation={{
28 maxSize: 10 * 1024 * 1024,
29 acceptedTypes: ['image/*', '.pdf']
30 }}
31 onFilesChange={(files) => {
32 dragDropFiles = files;
33 handleFilesChange('Drag & Drop')(files);
34 }}
35 onError={handleError('Drag & Drop')}
36 dragDropProps={{
37 label: "Drop images or PDFs here",
38 description: "Max 10MB per file",
39 showFileList: true
40 }}
41 />
42 </div>
43
44 <div class="space-y-2">
45 <h3 class="text-sm font-medium">Regular Input Mode</h3>
46 <FileInput
47 mode="regular"
48 validation={{
49 maxFiles: 3,
50 acceptedTypes: ['.doc', '.docx', '.txt']
51 }}
52 onFilesChange={(files) => {
53 regularFiles = files;
54 handleFilesChange('Regular')(files);
55 }}
56 onError={handleError('Regular')}
57 regularProps={{
58 placeholder: "Select up to 3 files...",
59 showFileCount: true
60 }}
61 />
62 </div>
63
64 <div class="space-y-2">
65 <h3 class="text-sm font-medium">Button-Only Mode</h3>
66 <FileInput
67 mode="button-only"
68 validation={{
69 maxFiles: 1,
70 maxSize: 5 * 1024 * 1024,
71 acceptedTypes: ['image/*']
72 }}
73 onFilesChange={(files) => {
74 buttonFiles = files;
75 handleFilesChange('Button')(files);
76 }}
77 onError={handleError('Button')}
78 buttonProps={{
79 buttonText: "Upload Avatar",
80 variant: "filled",
81 size: "lg",
82 showCount: true
83 }}
84 />
85 </div>
86</div>
87Dedicated Components
Each component optimized for specific use cases with sleek UI
FileInputDragDrop
Best for large file uploads with visual feedback
Drop media files here
Images and videos only
FileInputRegular (Sleek UI)
Input-group based design with clear/browse actions
FileInputButton
Minimal footprint for inline forms
1
2<script lang="ts">
3 import { FileInputDragDrop, FileInputRegular, FileInputButton } from "@kareyes/aether";
4
5 function handleFilesChange(name: string) {
6 return (files: FileList | null) => {
7 console.log(`${name} files changed:`, files ? Array.from(files).map(f => f.name) : 'No files');
8 };
9 }
10
11 function handleError(name: string) {
12 return (error: string) => {
13 console.error(`${name} error:`, error);
14 };
15 }
16</script>
17
18<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
19 <div class="space-y-2">
20 <h3 class="text-sm font-medium">FileInputDragDrop</h3>
21 <p class="text-xs text-muted-foreground">Best for large file uploads with visual feedback</p>
22 <FileInputDragDrop
23 validation={{
24 maxSize: 10 * 1024 * 1024,
25 acceptedTypes: ['image/*', 'video/*']
26 }}
27 onFilesChange={handleFilesChange('Direct Drag Drop')}
28 onError={handleError('Direct Drag Drop')}
29 label="Drop media files here"
30 description="Images and videos only"
31 multiple={true}
32 />
33 </div>
34
35 <div class="space-y-2">
36 <h3 class="text-sm font-medium">FileInputRegular (Sleek UI)</h3>
37 <p class="text-xs text-muted-foreground">Input-group based design with clear/browse actions</p>
38 <FileInputRegular
39 validation={{
40 maxFiles: 5,
41 acceptedTypes: ['.csv', '.xlsx', '.json']
42 }}
43 onFilesChange={handleFilesChange('Direct Regular')}
44 onError={handleError('Direct Regular')}
45 placeholder="Choose spreadsheets..."
46 showFileCount={true}
47 multiple={true}
48 />
49 </div>
50
51 <div class="space-y-2">
52 <h3 class="text-sm font-medium">FileInputButton</h3>
53 <p class="text-xs text-muted-foreground">Minimal footprint for inline forms</p>
54 <FileInputButton
55 validation={{
56 maxFiles: 1,
57 maxSize: 2 * 1024 * 1024,
58 acceptedTypes: ['.png', '.jpg', '.jpeg', '.gif']
59 }}
60 onFilesChange={handleFilesChange('Direct Button')}
61 onError={handleError('Direct Button')}
62 buttonText="Choose Image"
63 variant="ghost"
64 showCount={false}
65 showFileList={true}
66 />
67 </div>
68</div>Regular Input - Sleek UI Showcase
Enhanced with input-group component for a modern, polished experience
Single File Upload
Multiple Files with Count
Without File List
Required Field
1
2<script lang="ts">
3 import { FileInputRegular } from "@kareyes/aether";
4
5
6 function handleFilesChange(name: string) {
7 return (files: FileList | null) => {
8 console.log(`${name} files changed:`, files ? Array.from(files).map(f => f.name) : 'No files');
9 };
10 }
11
12 function handleError(name: string) {
13 return (error: string) => {
14 console.error(`${name} error:`, error);
15 };
16 }
17</script>
18
19<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
20 <div class="space-y-2">
21 <h3 class="text-sm font-medium">Single File Upload</h3>
22 <FileInputRegular
23 validation={{
24 maxFiles: 1,
25 maxSize: 5 * 1024 * 1024,
26 acceptedTypes: ['image/*', '.pdf']
27 }}
28 onFilesChange={handleFilesChange('Single File')}
29 onError={handleError('Single File')}
30 placeholder="Select an image or PDF..."
31 showFileCount={false}
32 showFileList={true}
33 multiple={false}
34 />
35 </div>
36
37 <div class="space-y-2">
38 <h3 class="text-sm font-medium">Multiple Files with Count</h3>
39 <FileInputRegular
40 validation={{
41 maxFiles: 10,
42 acceptedTypes: ['.jpg', '.png', '.gif', '.webp']
43 }}
44 onFilesChange={handleFilesChange('Multiple Images')}
45 onError={handleError('Multiple Images')}
46 placeholder="Select up to 10 images..."
47 showFileCount={true}
48 showFileList={true}
49 multiple={true}
50 />
51 </div>
52
53 <div class="space-y-2">
54 <h3 class="text-sm font-medium">Without File List</h3>
55 <FileInputRegular
56 validation={{
57 acceptedTypes: ['.doc', '.docx', '.txt']
58 }}
59 onFilesChange={handleFilesChange('Documents')}
60 onError={handleError('Documents')}
61 placeholder="Choose document files..."
62 showFileCount={true}
63 showFileList={false}
64 multiple={true}
65 />
66 </div>
67
68 <div class="space-y-2">
69 <h3 class="text-sm font-medium">Required Field</h3>
70 <FileInputRegular
71 validation={{
72 maxFiles: 1,
73 acceptedTypes: ['.pdf']
74 }}
75 onFilesChange={handleFilesChange('Resume')}
76 onError={handleError('Resume')}
77 placeholder="Upload your resume (PDF only)..."
78 showFileCount={false}
79 showFileList={true}
80 multiple={false}
81 required={true}
82 />
83 </div>
84</div>Style Variants
Different button styles for the file input trigger
Default Variant
Filled Variant
Ghost Variant
1
2<script lang="ts">
3 import { FileInput } from "@kareyes/aether";
4</script>
5
6<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
7 <FileInput mode="button-only" buttonProps={{ buttonText: "Default Button", variant: "default", size: "default" }} />
8 <FileInput mode="button-only" buttonProps={{ buttonText: "Filled Button", variant: "filled", size: "default" }} />
9 <FileInput mode="button-only" buttonProps={{ buttonText: "Ghost Button", variant: "ghost", size: "default" }} />
10</div>Size Options
Available button sizes for the file input
Small
Default
Large
1
2<script lang="ts">
3 import { FileInput } from "@kareyes/aether";
4</script>
5
6<div class="flex flex-wrap gap-4 items-end">
7 <FileInput mode="button-only" buttonProps={{ buttonText: "Small", size: "sm" }} />
8 <FileInput mode="button-only" buttonProps={{ buttonText: "Default", size: "default" }} />
9 <FileInput mode="button-only" buttonProps={{ buttonText: "Large", size: "lg" }} />
10</div>Error State
Pass an error prop to show a validation message below the input. Errors can be
set statically or driven reactively via onError.
Regular — validation error
Drag & Drop — static error
Drop image here
or click to select files
Button — static error
1
2<script lang="ts">
3 import { FileInputRegular, FileInputDragDrop, FileInputButton } from "@kareyes/aether";
4
5 let regularError = $state("");
6</script>
7
8<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
9 <!-- Regular — error triggered by validation failure -->
10 <div class="space-y-2">
11 <h3 class="text-sm font-medium">Regular — validation error</h3>
12 <FileInputRegular
13 validation={{ maxSize: 1, acceptedTypes: [".pdf"] }}
14 onError={(err) => (regularError = err)}
15 onFilesChange={() => (regularError = "")}
16 placeholder="Upload a PDF..."
17 error={!!regularError}
18 />
19 </div>
20
21 <!-- Drag & Drop — static error prop -->
22 <div class="space-y-2">
23 <h3 class="text-sm font-medium">Drag & Drop — static error</h3>
24 <FileInputDragDrop
25 validation={{ maxSize: 5 * 1024 * 1024, acceptedTypes: ["image/*"] }}
26 error
27 label="Drop image here"
28 />
29 </div>
30
31 <!-- Button — static error prop -->
32 <div class="space-y-2">
33 <h3 class="text-sm font-medium">Button — static error</h3>
34 <FileInputButton
35 validation={{ maxFiles: 1 }}
36 error
37 buttonText="Choose File"
38 />
39 </div>
40</div>Wrapped in Field
Nest any file input inside <Field> to get a label, description, required
indicator, and error message for free — all properly associated with the control.
PDF or Word doc, max 5 MB.
JPEG or PNG, max 2 MB.
Drop your photo here
JPEG or PNG only
Any file type, max 10 MB.
Uploads are disabled for read-only profiles.
1
2<script lang="ts">
3 import { FileInputRegular, FileInputDragDrop, FileInputButton, Field } from "@kareyes/aether";
4
5 let resumeError = $state("");
6</script>
7
8<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
9 <!-- Regular inside Field — label, description, required, and error -->
10 <Field label="Résumé" description="PDF or Word doc, max 5 MB." required error={resumeError}>
11 <FileInputRegular
12 validation={{ maxFiles: 1, maxSize: 5 * 1024 * 1024, acceptedTypes: [".pdf", ".doc", ".docx"] }}
13 onError={(err) => (resumeError = err)}
14 onFilesChange={() => (resumeError = "")}
15 placeholder="Select your résumé…"
16 />
17 </Field>
18
19 <!-- Drag-drop inside Field -->
20 <Field label="Cover Photo" description="JPEG or PNG, max 2 MB.">
21 <FileInputDragDrop
22 validation={{ maxFiles: 1, maxSize: 2 * 1024 * 1024, acceptedTypes: ["image/jpeg", "image/png"] }}
23 label="Drop your photo here"
24 description="JPEG or PNG only"
25 />
26 </Field>
27
28 <!-- Button inside Field -->
29 <Field label="Attachment" description="Any file type, max 10 MB.">
30 <FileInputButton
31 validation={{ maxFiles: 1, maxSize: 10 * 1024 * 1024 }}
32 buttonText="Attach File"
33 showFileList
34 />
35 </Field>
36
37 <!-- Disabled via Field prop -->
38 <Field label="Locked Upload" description="Uploads are disabled for read-only profiles." disabled>
39 <FileInputRegular disabled placeholder="Uploads disabled" />
40 </Field>
41</div>File Status
Current file selection state from the examples above
Drag & Drop Files
No files selected
Regular Input Files
No files selected
Button Files
No files selected
FileInput Component
A flexible file upload component with three display modes, built-in file validation, and full form integration support.
Table of Contents
- Features
- Modes
- Basic Usage
- Props Reference
- Examples
- Direct Component Usage
- Validation
- Event Handling
- With Field Component
- SchemaForm Integration
- Utilities
Features
- 3 display modes: drag-drop, regular input, button-only
- File validation: type checking, size limits, max file count
- Blur event:
onblurfires when the interactive element loses focus — integrates with form touched state - Drag & drop: visual feedback with intelligent drag-leave detection
- File management: built-in file list with per-file remove buttons
- Bindable files: two-way
bind:filesor one-wayonFilesChangecallback - Error state:
errorprop drives visual error styling - Keyboard accessible: Enter / Space triggers the file picker in drag-drop mode
- TypeScript: complete type safety for all modes and props
Modes
| Mode | Prop value | Description |
|---|---|---|
| Regular | "regular" (default) |
Traditional styled input with Browse button |
| Drag & Drop | "drag-drop" |
Full drop-zone with visual drag feedback |
| Button Only | "button-only" |
Minimal button trigger |
Basic Usage
<script lang="ts">
import { FileInput } from "@kareyes/aether";
let files = $state<FileList | null>(null);
</script>
<!-- Default: regular mode -->
<FileInput bind:files />
<!-- Drag & drop -->
<FileInput mode="drag-drop" bind:files />
<!-- Button only -->
<FileInput mode="button-only" bind:files />
Props Reference
Shared Props
These apply to all three modes via the FileInput wrapper.
| Prop | Type | Default | Description |
|---|---|---|---|
mode |
'regular' | 'drag-drop' | 'button-only' |
'regular' |
Display mode |
files |
FileList | null |
null |
Selected files — bindable |
validation |
FileValidationConfig |
{} |
File validation rules (see below) |
onFilesChange |
(files: FileList | null) => void |
— | Called when selection changes |
onError |
(error: string) => void |
— | Called when validation fails |
onblur |
(event: FocusEvent) => void |
— | Called when the interactive element loses focus |
accept |
string |
— | HTML accept attribute (e.g. "image/*", ".pdf,.docx") — alternative to validation.acceptedTypes |
multiple |
boolean |
false |
Allow multiple file selection |
disabled |
boolean |
false |
Disable the input |
required |
boolean |
false |
Mark as required for native form submission |
error |
boolean |
false |
Show error visual state |
id |
string |
— | Input element id |
name |
string |
— | Input element name |
form |
string |
— | Associated form id |
class |
string |
"" |
Additional CSS classes |
dragDropProps |
Partial<DragDropFileInputProps> |
{} |
Props forwarded to drag-drop sub-component |
regularProps |
Partial<RegularFileInputProps> |
{} |
Props forwarded to regular sub-component |
buttonProps |
Partial<ButtonFileInputProps> |
{} |
Props forwarded to button-only sub-component |
Drag & Drop Mode Props
Pass via dragDropProps or use FileInputDragDrop directly.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
"Drag and drop files here" |
Primary text inside the drop zone |
description |
string |
"or click to select files" |
Secondary text below label |
showFileList |
boolean |
true |
Show selected files below the drop zone |
height |
string |
— | Custom min-height CSS value for the drop zone |
Regular Mode Props
Pass via regularProps or use FileInputRegular directly.
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder |
string |
"Choose files..." |
Placeholder text when no file is selected |
showFileCount |
boolean |
true |
Show file count in the input field |
showFileList |
boolean |
true |
Show selected files below the input |
Button-Only Mode Props
Pass via buttonProps or use FileInputButton directly.
| Prop | Type | Default | Description |
|---|---|---|---|
buttonText |
string |
"Choose Files" |
Button label text |
variant |
'default' | 'filled' | 'ghost' | 'danger' |
'default' |
Button variant |
size |
'sm' | 'default' | 'lg' |
'default' |
Button size |
showCount |
boolean |
true |
Show selected file count in the button label |
showFileList |
boolean |
true |
Show selected files below the button |
Validation Config
Passed as the validation prop object.
interface FileValidationConfig {
maxFiles?: number; // Maximum number of files allowed
maxSize?: number; // Maximum size per file in bytes
acceptedTypes?: string[]; // MIME types or extensions
}
Validation runs in this order: max files → file types → file size. The first failure triggers onError and briefly shows an error state (auto-clears after 3 seconds).
Note:
accept(the HTML attribute string) andvalidation.acceptedTypes(the array) both restrict the file picker. They are independent — you can use either or both.
Examples
Drag & Drop — Image Upload
<FileInput
mode="drag-drop"
bind:files
accept="image/png,image/jpeg,image/webp"
validation={{ maxFiles: 1, maxSize: 5 * 1024 * 1024 }}
dragDropProps={{
label: "Drop your profile photo here",
description: "PNG, JPG, or WebP — max 5 MB"
}}
onError={(err) => console.error(err)}
/>
Regular Mode — Multiple Documents
<FileInput
mode="regular"
bind:files
multiple
accept=".pdf,.doc,.docx"
validation={{
maxFiles: 5,
maxSize: 10 * 1024 * 1024,
acceptedTypes: [".pdf", ".doc", ".docx"]
}}
regularProps={{ placeholder: "Choose documents..." }}
/>
Button Only — Minimal Attachment
<FileInput
mode="button-only"
bind:files
multiple
buttonProps={{
buttonText: "Attach Files",
variant: "ghost",
size: "sm"
}}
/>
Custom Drop Zone Height
<FileInput
mode="drag-drop"
bind:files
dragDropProps={{ height: "200px", showFileList: false }}
/>
Callback Pattern (no bind:)
<FileInput
mode="regular"
onFilesChange={(files) => {
if (files) console.log("Selected:", Array.from(files).map(f => f.name));
}}
onError={(err) => console.warn("Validation error:", err)}
/>
Direct Component Usage
When you know the display mode upfront, import the sub-components directly instead of going through the FileInput wrapper. Props are passed flat — no dragDropProps/regularProps/buttonProps nesting needed.
<script lang="ts">
import {
FileInputDragDrop,
FileInputRegular,
FileInputButton,
} from "@kareyes/aether";
</script>
FileInputDragDrop
Full drop-zone with drag-over visual feedback. All shared props plus:
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string |
"Drag and drop files here" |
Primary text inside the drop zone |
description |
string |
"or click to select files" |
Secondary text below label |
showFileList |
boolean |
true |
Show selected files below the drop zone |
height |
string |
— | Custom min-height CSS value for the drop zone |
<FileInputDragDrop
bind:files
label="Drop your avatar here"
description="PNG or JPG — max 2 MB"
height="180px"
accept="image/png,image/jpeg"
validation={{ maxFiles: 1, maxSize: 2 * 1024 * 1024 }}
onError={(err) => console.error(err)}
/>
FileInputRegular
Traditional styled input with a Browse button and optional file list. All shared props plus:
| Prop | Type | Default | Description |
|---|---|---|---|
placeholder |
string |
"Choose files..." |
Placeholder text when no file is selected |
showFileCount |
boolean |
true |
Show file count in the input field |
showFileList |
boolean |
true |
Show selected files below the input |
<FileInputRegular
bind:files
multiple
placeholder="Choose documents..."
showFileCount={true}
accept=".pdf,.doc,.docx"
validation={{ maxFiles: 5, maxSize: 10 * 1024 * 1024 }}
onError={(err) => console.error(err)}
/>
FileInputButton
Minimal button trigger with an optional file list. All shared props plus:
| Prop | Type | Default | Description |
|---|---|---|---|
buttonText |
string |
"Choose Files" |
Button label text |
variant |
'default' | 'filled' | 'ghost' | 'danger' |
'default' |
Button visual variant |
size |
'sm' | 'default' | 'lg' |
'default' |
Button size |
showCount |
boolean |
true |
Show selected file count next to the button label |
showFileList |
boolean |
true |
Show selected files below the button |
<FileInputButton
bind:files
multiple
buttonText="Attach Files"
variant="ghost"
size="sm"
showCount={true}
validation={{ maxFiles: 5, acceptedTypes: ["image/*", ".pdf"] }}
onError={(err) => console.error(err)}
/>
When to use direct components vs FileInput
| Situation | Recommendation |
|---|---|
| Mode is known at author time | Use FileInputDragDrop / FileInputRegular / FileInputButton directly — cleaner props, no nesting |
| Mode is dynamic (driven by a variable) | Use FileInput with mode prop |
Used inside SchemaForm |
Always FileInput — the schema renderer uses it internally |
Validation
Built-In Validation (via validation prop)
<FileInput
bind:files
validation={{
maxFiles: 3,
maxSize: 2 * 1024 * 1024, // 2 MB per file
acceptedTypes: ["image/*", ".pdf"] // MIME types or extensions
}}
onError={(err) => (errorMsg = err)}
/>
Accepted Types Formats
// MIME types
acceptedTypes: ["image/*", "video/*", "application/pdf"]
// File extensions
acceptedTypes: [".jpg", ".png", ".pdf", ".docx"]
// Mixed
acceptedTypes: ["image/*", ".pdf", ".txt"]
Using commonFileTypes and commonSizeLimits
<script>
import { commonFileTypes, commonSizeLimits } from "@kareyes/aether";
</script>
<FileInput
bind:files
validation={{
maxFiles: 5,
maxSize: commonSizeLimits.LARGE, // 10 MB
acceptedTypes: commonFileTypes.DOCUMENTS // [".pdf", ".doc", ".docx", ".txt"]
}}
/>
Available commonFileTypes keys: IMAGES, DOCUMENTS, SPREADSHEETS, PRESENTATIONS, VIDEOS, AUDIO, ARCHIVES, CODE.
Available commonSizeLimits: SMALL (1 MB), MEDIUM (5 MB), LARGE (10 MB), XLARGE (50 MB).
Event Handling
onblur
Fires when the primary interactive element loses focus:
- drag-drop → when the drop zone div loses focus
- regular → when the trigger button loses focus
- button-only → when the button loses focus
Useful for marking fields as touched in form libraries:
<FileInput
bind:files
onFilesChange={(f) => (value = f)}
onblur={() => (touched = true)}
/>
onFilesChange
Called with the current FileList | null whenever files are added or cleared.
<FileInput
onFilesChange={(files) => {
selectedFiles = files;
}}
/>
onError
Called with a string message when validation fails. Error visual state auto-clears after 3 seconds.
<FileInput
onError={(err) => {
toast.error(err);
}}
/>
With Field Component
Wrap with Field to get consistent labels, descriptions, and error display:
<script lang="ts">
import { FileInput, Field } from "@kareyes/aether";
let files = $state<FileList | null>(null);
let error = $state("");
</script>
<Field
label="Resume"
description="PDF only, max 5 MB"
required
error={error || undefined}
>
<FileInput
mode="regular"
bind:files
validation={{ maxFiles: 1, maxSize: 5 * 1024 * 1024, acceptedTypes: [".pdf"] }}
error={!!error}
onError={(err) => (error = err)}
onblur={() => { if (!files) error = "Please upload your resume"; }}
/>
</Field>
Complete Upload Form
<script lang="ts">
import { FileInput, Field, Button } from "@kareyes/aether";
let resume = $state<FileList | null>(null);
let coverLetter = $state<FileList | null>(null);
let portfolio = $state<FileList | null>(null);
let errors = $state({ resume: "", coverLetter: "", portfolio: "" });
function handleSubmit() {
if (!resume) { errors.resume = "Required"; return; }
if (!coverLetter) { errors.coverLetter = "Required"; return; }
console.log("Submit:", { resume, coverLetter, portfolio });
}
</script>
<div class="space-y-6">
<Field label="Resume" required error={errors.resume || undefined}>
<FileInput
mode="drag-drop"
bind:files={resume}
validation={{ maxFiles: 1, maxSize: 5 * 1024 * 1024, acceptedTypes: [".pdf"] }}
error={!!errors.resume}
onFilesChange={() => (errors.resume = "")}
onError={(err) => (errors.resume = err)}
dragDropProps={{ label: "Drop your resume here", description: "PDF only — max 5 MB" }}
/>
</Field>
<Field label="Cover Letter" required error={errors.coverLetter || undefined}>
<FileInput
mode="regular"
bind:files={coverLetter}
validation={{ maxFiles: 1, acceptedTypes: [".pdf", ".doc", ".docx"] }}
error={!!errors.coverLetter}
onFilesChange={() => (errors.coverLetter = "")}
onError={(err) => (errors.coverLetter = err)}
/>
</Field>
<Field label="Portfolio" description="Optional — images or PDFs">
<FileInput
mode="button-only"
bind:files={portfolio}
multiple
validation={{ maxFiles: 5, acceptedTypes: ["image/*", ".pdf"] }}
onError={(err) => (errors.portfolio = err)}
buttonProps={{ buttonText: "Attach Portfolio Files", variant: "ghost" }}
/>
</Field>
<Button onclick={handleSubmit}>Submit Application</Button>
</div>
SchemaForm Integration
When using the SchemaForm component, declare file fields using inputType: "file" in withField. Use RequiredFile or requiredFile() for required fields — Schema.Any accepts null and should only be used for optional file fields.
import {
withField, withFormLayout,
RequiredFile, requiredFile,
FormController
} from "@kareyes/aether/forms";
import { Schema, pipe } from "effect";
const UploadSchema = pipe(
Schema.Struct({
// Required — default message: "Please select a file"
avatar: pipe(
RequiredFile,
withField({
label: "Profile Photo",
inputType: "file",
accept: "image/png,image/jpeg",
// fileMode defaults to "drag-drop" inside SchemaForm
})
),
// Required — custom message
resume: pipe(
requiredFile("Please upload your resume"),
withField({
label: "Resume",
inputType: "file",
fileMode: "regular",
accept: ".pdf",
})
),
// Optional — null is valid
attachments: pipe(
Schema.Any,
withField({
label: "Attachments",
inputType: "file",
fileMode: "button-only",
multiple: true,
description: "Optional"
})
),
}),
withFormLayout({ columns: 1 })
);
const controller = new FormController(UploadSchema, { validateOnBlur: true });
File-specific withField options
| Option | Type | Description |
|---|---|---|
inputType |
"file" |
Renders a FileInput component |
fileMode |
'drag-drop' | 'regular' | 'button-only' |
Display mode (default: 'drag-drop') |
multiple |
boolean |
Allow multiple file selection |
accept |
string |
HTML accept attribute string |
RequiredFile vs Schema.Any
// ✅ Required — fails validation when no file is selected
avatar: pipe(RequiredFile, withField({ ... }))
// ✅ Required — custom error message
resume: pipe(requiredFile("Upload your CV"), withField({ ... }))
// ✅ Optional — null (no file) is valid
attachment: pipe(Schema.Any, withField({ ... }))
RequiredFilechecksvalue instanceof FileList && value.length > 0and includes atypeof FileList !== "undefined"guard for SSR safety.
Handling file data in onSubmit
File fields store a FileList | null as their value — not a URL or base64 string. Upload the files in your onSubmit handler:
<SchemaForm
{controller}
onSubmit={async (data) => {
const formData = new FormData();
if (data.avatar) formData.append("avatar", data.avatar[0]);
if (data.resume) formData.append("resume", data.resume[0]);
await fetch("/api/upload", { method: "POST", body: formData });
}}
/>
Visual States
| State | Description |
|---|---|
| Default | Ready for interaction |
| Drag Over | Blue highlight while files are dragged over the drop zone |
| Error | Destructive border/text on validation failure — auto-clears after 3 seconds |
| Disabled | Reduced opacity, non-interactive |
Accessibility
role="button"andtabindexon the drag-drop zone — activates with Enter or Spacearia-disabledon the drag-drop zone when disabledaria-describedbylinks the trigger to the file list when files are selectedaria-labelon all remove buttonssr-onlyhidden<input type="file">receives focus for screen readers