Detent web components
By Shane Logsdon

Forms & inputs

Animated File Upload

Implemented

Physical drag-and-drop file uploader with per-file progress morphing

detent-animated-file-upload

Port status
Catalog status
implemented
Origin
upstream
Phase
3
Wave
phase-3-forms
Group
Forms & inputs

8.2 KB gzip for the class module

Variants

compact · images and PDFs, 1 MB, 3 files
standard · seeded queue
standard · disabled

Every upload on this page is simulated: choosing a file starts the documented 180 ms progress simulation and no request leaves the page.

Choose a file to see the events.

Recorded states

  • disabled recorded state; no setter
  • error recorded state; no setter
  • reduced motion recorded state; no setter
  • completed recorded state; no setter

Usage

Import

import '/components/animated-file-upload/define.js';

// or register it yourself
import { DetentAnimatedFileUpload } from '/components/animated-file-upload/animated-file-upload.js';

Attributes

  • disabled
  • drop-subtitle
  • drop-title
  • max-files
  • max-size
  • multiple
  • name
  • required
  • variant

Properties

  • accept
  • initialFiles
  • uploadHandler
  • multiple
  • maxSize
  • maxFiles
  • dropTitle
  • dropSubtitle
  • variant
  • disabled
  • name
  • required
  • onFilesSelected
  • onUploadComplete

Slots

No slots are recorded for this component.

Events

  • detent-files-selected — { files: File[] }
  • detent-upload-complete — { file: UploadFileItem }
  • detent-progress — { progress: number }

Example

Runnable example: site/examples/animated-file-upload.ts. The snippet below is generated from the catalogued attributes.

<detent-animated-file-upload
  disabled
  drop-subtitle="or browse from your device"
  drop-title="Drop files here"
  max-files="10"
  max-size
  multiple
  name
  required
  variant="standard"
></detent-animated-file-upload>

Property-only values

const element = document.querySelector('detent-animated-file-upload');
if (element !== null) {
  // accept: string | string[]
  element.accept = [];
  // onFilesSelected: (files: File[]) => void
  element.onFilesSelected = () => {};
  // onUploadComplete: (file: UploadFileItem) => void
  element.onUploadComplete = () => {};
  // uploadHandler: (file, onProgress) => Promise<void>
  element.uploadHandler = () => {};
}

Source

The pinned upstream source for this row:

  • src/components/ui/AnimatedFileUpload.tsx sha256 63bcb0d4ca27…

Native files

The native module and its artifacts:

  • src/components/animated-file-upload/animated-file-upload.ts
  • src/components/animated-file-upload/styles.css
  • src/components/animated-file-upload/define.ts
  • docs/components/animated-file-upload.md
  • site/examples/animated-file-upload.ts
  • tests/browser/animated-file-upload.spec.ts
  • evidence/components/animated-file-upload.md

API

Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.

Attributes

Attributes
AttributePropertyTypeDefaultNotes
disableddisabledbooleanfalsePresence semantics: present means true, absent means false.
drop-subtitledropSubtitlestring'or browse from your device'Kebab-case attribute; camelCase property.
drop-titledropTitlestring'Drop files here'Kebab-case attribute; camelCase property.
max-filesmaxFilesnumber10Kebab-case attribute; camelCase property.
max-sizemaxSizenumber26214400 (25MB)Kebab-case attribute; camelCase property.
multiplemultiplebooleantruePresence semantics: present means true, absent means false.
name———Recorded in the catalog as an attribute.
required———Recorded in the catalog as an attribute.
variantvariant'standard' | 'compact''standard'Kebab-case attribute; camelCase property.

Properties

Properties
PropertyTypeDefaultNotes
acceptstring | string[]undefinedProperty-only: assign from script; not accepted as an attribute.
initialFiles——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
uploadHandler(file, onProgress) => Promise<void>undefinedProperty-only: assign from script; not accepted as an attribute.
multiplebooleantrueProperty-only: assign from script; not accepted as an attribute.
maxSizenumber26214400 (25MB)Property-only: assign from script; not accepted as an attribute.
maxFilesnumber10Property-only: assign from script; not accepted as an attribute.
dropTitlestring'Drop files here'Property-only: assign from script; not accepted as an attribute.
dropSubtitlestring'or browse from your device'Property-only: assign from script; not accepted as an attribute.
variant'standard' | 'compact''standard'Property-only: assign from script; not accepted as an attribute.
disabledbooleanfalseProperty-only: assign from script; not accepted as an attribute.
name——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
required——Recorded in the catalog without a top-level type; pinned from source when the row is ported.
onFilesSelected(files: File[]) => voidundefinedProperty-only: assign from script; not accepted as an attribute.
onUploadComplete(file: UploadFileItem) => voidundefinedProperty-only: assign from script; not accepted as an attribute.

Events

Events
EventUpstream propPayload typeNotes
detent-files-selectedonFilesSelected(files: File[]) => voidComposed, bubbles; fires only for a user action.
detent-upload-completeonUploadComplete(file: UploadFileItem) => voidComposed, bubbles; fires only for a user action.
detent-progressonProgress(progress: number) => voidComposed, bubbles; fires only for a user action.

Slots

No slots are recorded for this component.

CSS customisation

  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • `accept` is enforced in the component with "File type is not accepted (accept: ...)" (the row records it as a documented constraint; upstream only passed it to the picker), while size rejection keeps the upstream message.
  • `multiple` follows the accepted default-true rule (attribute reflected, `multiple="false"` resolves true, the property opts out).
  • Native form participation added: name, required, reset to `initialFiles`, formStateRestoreCallback, setValidity, external `<label for>`, and a submitted FormData of the queued raw files (or the comma-joined names when only seeded names exist).
  • `initialFiles` is assignable after connection: it replaces the queue and becomes the reset default.
  • Framer Motion drag reaction and status morph are CSS transitions plus text and progress updates; reduced motion removes them.
  • The upstream dark-only hex palette is replaced by theme tokens so both schemes work.
  • `maxFiles` truncation stays silent (upstream `combined.slice(0, maxFiles)`); only size and type rejections message and announce.
  • The upload is a documented 180ms simulation that makes no request; a consumer `uploadHandler` replaces it entirely.

What it does

A real <input type="file"> behind a drag-and-drop zone, a per-file queue with independent Uploading → Processing → Complete stages, rejection with a message, cancellation, retry, and one live region for announcements.

Upstream behaviour
  • Subtle physical dropzone scaling and border reaction without exaggerated AI glow
  • Automatic mime-type detection and contextual file icon attribution
  • Multi-file queue management with independent Uploading → Processing → Complete stages
  • Self-morphing progress bar into checkmark state with non-aggressive error recovery
  • Customizable file constraints (maxSize, maxFiles, accept) with accessible screen reader labels
Differences from upstream
  • React callback props become typed composed CustomEvents; programmatic state assignment emits none.
  • Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
  • React state/hooks replaced by element-owned state with pre-upgrade property preservation.
  • `accept` is enforced in the component with "File type is not accepted (accept: ...)" (the row records it as a documented constraint; upstream only passed it to the picker), while size rejection keeps the upstream message.
  • `multiple` follows the accepted default-true rule (attribute reflected, `multiple="false"` resolves true, the property opts out).
  • Native form participation added: name, required, reset to `initialFiles`, formStateRestoreCallback, setValidity, external `<label for>`, and a submitted FormData of the queued raw files (or the comma-joined names when only seeded names exist).
  • `initialFiles` is assignable after connection: it replaces the queue and becomes the reset default.
  • Framer Motion drag reaction and status morph are CSS transitions plus text and progress updates; reduced motion removes them.
  • The upstream dark-only hex palette is replaced by theme tokens so both schemes work.
  • `maxFiles` truncation stays silent (upstream `combined.slice(0, maxFiles)`); only size and type rejections message and announce.
  • The upload is a documented 180ms simulation that makes no request; a consumer `uploadHandler` replaces it entirely.
Previous: Alert Next: Animated Number Morph

Docs

Components

Project