On this pageOverview
FileDrop
A file drop zone that accepts files via both drag-and-drop and a hidden <input type="file">. FileDrop is headless — the component owns drag state and file-arrival events; your toView callback owns the visual.
FileDrop uses the Submodel pattern — initialize with FileDrop.init(), delegate in your parent update via FileDrop.update(), and render with FileDrop.view(). The update function returns [Model, Commands, Option<OutMessage>] — the OutMessage fires when files arrive (drop or change). Pattern-match on it to process the files.
A multi-file drop zone. Drag files on or click to browse — the component exposes data-drag-over on the root while a drag hovers, so you can style the highlighted state with data-[drag-over]:* utilities.
// Pseudocode walkthrough of the Foldkit integration points. Each labeled
// block below is an excerpt. Fit them into your own Model, init, Message,
// update, and view definitions.
import { Effect, Match as M, Option } from 'effect'
import { Command, File, Ui } from 'foldkit'
import { m } from 'foldkit/message'
import { evo } from 'foldkit/struct'
import { Class, input, label, p, span } from './html'
// Add the FileDrop Submodel to your Model, plus a list of accepted files:
const Model = S.Struct({
uploader: Ui.FileDrop.Model,
uploadedFiles: S.Array(File.File),
// ...your other fields
})
// Initialize both fields:
const init = () => [
{
uploader: Ui.FileDrop.init({ id: 'uploader' }),
uploadedFiles: [],
// ...your other fields
},
[],
]
// Embed FileDrop's Message in your parent Message:
const GotFileDropMessage = m('GotFileDropMessage', {
message: Ui.FileDrop.Message,
})
// Inside your update function's M.tagsExhaustive({...}), delegate to
// FileDrop.update and pattern-match on the OutMessage it emits when files
// arrive (via drop or input change):
GotFileDropMessage: ({ message }) => {
const [nextUploader, commands, maybeOutMessage] = Ui.FileDrop.update(
model.uploader,
message,
)
const nextFiles = Option.match(maybeOutMessage, {
onNone: () => model.uploadedFiles,
onSome: M.type<Ui.FileDrop.OutMessage>().pipe(
M.tagsExhaustive({
ReceivedFiles: ({ files }) => [...model.uploadedFiles, ...files],
}),
),
})
return [
evo(model, {
uploader: () => nextUploader,
uploadedFiles: () => nextFiles,
}),
commands.map(
Command.mapEffect(Effect.map(message => GotFileDropMessage({ message }))),
),
]
}
// Render the drop zone. The `toView` callback receives attribute groups.
// Spread `root` onto a <label> so clicking opens the picker, and spread
// `input` onto a hidden <input type="file"> nested inside. Style the
// drag-over state via `data-drag-over`.
Ui.FileDrop.view({
model: model.uploader,
toParentMessage: message => GotFileDropMessage({ message }),
multiple: true,
accept: ['application/pdf', '.doc', '.docx'],
toView: attributes =>
label(
[
...attributes.root,
Class(
'flex cursor-pointer flex-col items-center gap-2 rounded-xl border-2 border-dashed border-gray-300 p-8 text-center hover:border-accent-400 data-[drag-over]:border-accent-500 data-[drag-over]:bg-accent-50',
),
],
[
p([], ['Drop files or click to browse']),
span([Class('text-sm text-gray-500')], ['PDF, DOC, or DOCX']),
input(attributes.input),
],
),
})FileDrop is headless. Your toView callback composes a <label> with the root attributes and a <input> with the input attributes. Wrap the input inside the label so native click-to-browse works. Use data-[drag-over]:* and data-[disabled]:* utilities to style state variants.
| Attribute | Condition |
|---|---|
data-drag-over | Present on the root while a drag is hovering over the zone. |
data-disabled | Present on the root when isDisabled is true. |
The hidden <input type="file"> stays in the DOM but visually hidden via the sr-only class so keyboard users can tab to it and trigger the native file picker. Wrapping the input in a <label> (via attributes.root) means clicking anywhere on the drop zone opens the picker.
Configuration object passed to FileDrop.init().
| Name | Type | Default | Description |
|---|---|---|---|
id | string | - | Unique ID for the file-drop instance. Assigned to the hidden <input type="file"> for label association. |
Configuration object passed to FileDrop.view().
| Name | Type | Default | Description |
|---|---|---|---|
model | FileDrop.Model | - | The file-drop state from your parent Model. |
toParentMessage | (childMessage: FileDrop.Message) => ParentMessage | - | Wraps FileDrop Messages in your parent Message type for Submodel delegation. |
toView | (attributes: FileDropAttributes) => Html | - | Callback that receives attribute groups for the root drop-zone element and the hidden file input. |
accept | ReadonlyArray<string> | - | List of accepted MIME types or file extensions (e.g. ["application/pdf", ".doc"]). Joined with commas and forwarded to the hidden input's accept attribute. Omit or pass an empty array to accept any file type. |
multiple | boolean | false | When true, the hidden input accepts multiple files per selection. Drag-and-drop always accepts multiple files. |
isDisabled | boolean | false | Strips drag handlers from the root and disables the input. Styling can react via data-disabled on the root. |
Attribute groups provided to the toView callback.
| Name | Type | Default | Description |
|---|---|---|---|
root | ReadonlyArray<Attribute<Message>> | - | Spread onto the outer drop-zone element (typically a <label>). Includes drag handlers (dragenter/dragleave/dragover/drop) and data attributes (data-drag-over, data-disabled). |
input | ReadonlyArray<Attribute<Message>> | - | Spread onto a hidden <input type="file"> nested inside the root. Includes the id, type, multiple, accept, sr-only class, and the file-change handler. |
The third element of the update tuple ([Model, Commands, Option<OutMessage>]). Pattern-match in your parent update handler to process arriving files.
| Name | Type | Default | Description |
|---|---|---|---|
ReceivedFiles | { files: ReadonlyArray<File> } | - | Emitted when the user drops files on the zone or selects them via the hidden input. Pattern-match on the OutMessage in your parent update to process the files (validate, upload, store in Model). |