A file-upload field with a tappable upload affordance, file-name display, and progress/error states.
4853:26511 in the 2026 Working File, and the headline recommendation has landed: the conflated state property is now Status = Default | Uploading | Error | Uploaded × State = Default | Focused | Disabled, the two-axis split §6 of the Property Naming Guidelines prescribes, with the four unsupported combinations documented as unreachable. Thumbnail-Slot is a real Figma Slot on a token-bound placeholder, the label and subtext rows reuse the shared FormGroup Header and Subtext Message instances, the file row is cleanly named throughout, the boder token typo is fixed, and the Lottie dependency behind the Uploading status is documented including its failure fallback. All four DS Health traits pass; the only item still open is Code Connect, blocked until the native library exists.Contexts are illustrative. Final screens will reference actual GCash patterns. Upload File appears in forms requiring document proof (KYC, insurance claims, verification).
Status covers the full upload lifecycle — Default, Uploading, Uploaded and Error.FormGroup Header and Subtext Message instances for labelling and helper copy. The progress-bar GIF is a Figma-preview device only — native drives the platform progress primitive from real upload percentage, with no asset to bundle.Status and State on separate axes, the split §6 of the Property Naming Guidelines prescribes — with the four unsupported combinations documented as unreachable rather than undrawn. The file row, the Figma Slot and the shared scaffolding instances all follow convention; the two internal container frames keep their original names as a recorded exception.SLOT (Thumbnail-Slot), so product teams can drop in a file preview without detaching, and the component composes shared FormGroup Header and Subtext Message instances for its label and helper rows.| State | iOS | Android | Figma Property | Notes |
|---|---|---|---|---|
| Default | Yes | Yes | state=Default | Empty input with paperclip + "Attach file / photo" label |
| Uploading | Yes | Yes | state=Uploading | Shows file name + Lottie progress bar + percentage |
| Uploaded | Yes | Yes | state=Uploaded | File name + trailing trash icon to remove |
| Uploaded with thumbnail | Yes | Yes | state=Uploaded with thumbnail | 52×52 image preview + truncated file name + trash. Should be orthogonal hasThumbnail prop. |
| Upload error | Yes | Yes | state=Upload error | Red 2px border + red subtext ("Maximum file size: 20MB") |
| Disabled / Pressed / Focused | N/A | N/A | — | Not defined. Engineers must improvise. C5 |
- Code Connect mappings not registered. Blocked — no native library exists yet. The property schema is clean and every layer is semantically named, so mapping is a mechanical step once the library lands. C7 · Code Connect Linkability
- Property naming cleaned up. v2.0: Rebuilt on node
4853:26511in the 2026 Working File.Upload error→Error(no space),Uploaded with thumbnailsplit so thumbnail presence is no longer folded into the state value, andhasLabelis gone — the label is now an always-presentFormGroup Headerinstance. (C2) C2 · Variant & Property Naming - Label and subtext scaffolding reused. v2.0: Every variant composes a shared
FormGroup Headerabove and aSubtext Messagebelow, matching Text Area. The label, helper copy and error message are no longer re-authored per component. (Composition) C1 · Layer Structure & Naming - Axis renamed
State→Status. v2.1:Uploading,UploadedandErrorare process outcomes rather than interaction states, soStatusis the correct axis per the Property Naming Guidelines. Closes the half-applied state restructure. (C2 · Property) C2 · Variant & Property Naming - Disabled variant added. v2.1: A fifth variant covers the read-only case that KYC review and locked submission screens need — previously there was no canonical appearance for it. Added on the
Statusaxis rather than a separateStateaxis; see open issues for the consequence. (C5) C5 · Interaction State Coverage - Error variant structure normalised. v2.1:
Status=Errornow wraps itsinput-fieldin anAttach File - Inputframe (6526:105078) like the other four variants. Previously it was the only one missing that layer, which made the tree inconsistent across the set. Its filename layer was also renamed#name→FileName. (C1) C1 · Layer Structure & Naming -
Statesplit onto its own axis. v2.2:Status = Default | Uploading | Uploaded | Error×State = Default | Disabled, authored as a deliberately sparse sparse matrix.Disabled × UploadingandDisabled × Errorare intentionally left unauthored — an upload in flight is already non-interactive, and an error you can't act on is a dead end. Process outcome and interaction state are now cleanly separated, andPressed/Focusedhave somewhere to live. (C2 · Property) C2 · Variant & Property Naming - Disabled + Uploaded variant authored. v2.2:
6572:111324covers the locked review screen showing an already-attached document — the case the single-axis set couldn't express. Muted container, muted filename, and the subtext dims with it. The trash icon is muted rather than removed, a deliberate choice to signal that deletion returns when the form unlocks. (C5) C5 · Interaction State Coverage - Thumbnail converted to a Figma Slot. v2.2:
Icon Placeholderis nowThumbnail-Slot, a realSLOTnode (6572:111570) carrying anicon-placeholderrectangle as its default content. Product teams can drop a real file preview in without detaching, and the hardcoded placeholder colour is now slot default rather than baked component chrome. Closes the placeholder, slot-adoption and thumbnail-token findings together. (C6 · Slot) C6 · Asset & Icon Quality - Error progress bar removed. v2.2:
Status=Errorno longer renders a 20% progress bar against its "Maximum file size: 20MB" message — a file rejected for size never starts uploading, so the two contradicted. Variant height dropped 167 → 148 to match. (C1) C1 · Layer Structure & Naming -
labelwrapper renamed toFileRow. v2.2: The frame holding the thumbnail, attach icon and filename is nowFileRowrather thanlabel, which had described none of its contents. (C1) C1 · Layer Structure & Naming - Progress-bar layer names cleaned up. v2.3:
🟢 https://app.lottiefiles.com/animation/0a1cb540-…→progress-fill, and the progress percentage text#label→ProgressValue. The filename wrapper also wentname→FileNameWrapper. Layer naming is now clean apart fromcontainer. The asset behindprogress-fillis a separate finding — see open issues. (C1) C1 · Layer Structure & Naming -
Focusedstate added. v2.4:State = Default | Focused | Disabled, withFocusedauthored againstStatus=Default(6572:111764) andStatus=Uploaded(6572:111782) — the same two pairingsDisabledcarries, and for the same reason. Eight variants in the sparse matrix. Completes interaction-state coverage, sincePressedis deliberately out of scope. (C5) C5 · Interaction State Coverage - Layer naming complete across all eight variants. v2.5: The wrapper frame is now exactly
UploadFieldin all eight variants — verified by exact string match, with no trailing whitespace and no leftovercontainer. An intermediate pass had left three different names for the same frame, including five with an invisible trailing space that would have broken name-based override matching. Together withFileRow,FileName,FileNameWrapper,ProgressValueandThumbnail-Slot, every layer in the set now carries a correct, consistent semantic name. (C1) C1 · Layer Structure & Naming - Token namespace resolved — generic tokens applied. v2.4: Closed by owner confirmation. The component-scoped
main/upload-file/color/*namespace was dropped in favour of the shared generic token scale, which retires thebodermisspelling along with it — there is no longer a per-component tier carrying the typo. Same direction Search Field and Text Area took. (C3 · Token) C3 · Token Coverage - Progress-bar GIF confirmed preview-only. v2.4: Closed by owner decision — the animated GIF fill on
progress-fillexists so the Figma variant previews motion, not as a handoff asset. Native implementations use the platform progress primitive (SwiftUIProgressView(value:), ComposeLinearProgressIndicator(progress =)) driven by real upload percentage; the Figma fill is never exported. Worth stating in the handoff notes so an implementer doesn't mistake it for a required asset. (C6) C6 · Asset & Icon Quality -
Pressedruled out of scope. v2.2: Closed by owner decision — Upload File will not carry aPressedvariant. Tapping the field opens the system file picker, and both platforms supply their own touch feedback for that transition, so a DS-defined pressed appearance would add a variant without adding information.Focusedremains in scope; it carries accessibility weight thatPresseddoes not. (C5) C5 · Interaction State Coverage - State property restructured onto two axes. v2.4: Verified on the live node. The single conflated axis is now
Status = Default | Uploading | Error | Uploaded×State = Default | Focused | Disabled— system report and user interaction on separate properties, exactly the split §6 of the Property Naming Guidelines now prescribes. This is the recommendation applied. (C2 · Property) C2 · Variant & Property Naming - Thumbnail is a real Figma Slot. v2.4:
Thumbnail-Slot(6572:111553) is a genuineSLOTnode, not a frame standing in for one, and its name is kebab-case per §4. A consumer swaps their own thumbnail in without detaching, and native handoff has a real content slot to bind. (C1 · Slot) C1 · Layer Structure & Naming - Label and subtext scaffolding reuses the shared components. v2.4: The row above the field is a
FormGroup Headerinstance and the row below is aSubtext Messageinstance — the same two Upload File’s siblings use, rather than redrawn locally. A change to either propagates across Form Elements for free. (C4 · Composition) C4 · Native Mappability - File row layer naming cleaned up. v2.4:
FileRow,FileNameWrapper,FileNameandTrailingActionall read as PascalCase semantic names, and the trailing control is a realTrashicon instance rather than a drawn glyph. (C1) C1 · Layer Structure & Naming - Container frame names accepted as-is. v2.5:
Attach File - Inputandinput-fieldkeep their current names by owner decision. They are internal structural frames rather than slots or text layers, nothing binds to them, and renaming carried more churn than the consistency was worth. Recorded so a later reviewer reads them as a settled exception rather than a missed pass. (C1) C1 · Layer Structure & Naming - Unsupported variant combinations documented. v2.5: Eight of twelve combinations ship, and the four gaps are deliberate.
Uploadinghas no Focused or Disabled because a field mid-transfer is not interactive — the control is busy, not available.Errorhas no Focused or Disabled because the error state is what a user focuses in order to fix, so it collapses back toDefaultthe moment they do, and a disabled field cannot have failed an upload it was never able to start. OnlyDefaultandUploadedcarry the full interaction range. Native implementations should treat the missing combinations as unreachable rather than undrawn. (C5) C5 · Interaction State Coverage -
bodertoken typo corrected. v2.5: Fixed in the token collection on owner confirmation, before Code Connect could bind generated native constants to the misspelling. Attested rather than verified — token names are not readable through the review tooling. (C3 · Token) C3 · Token Coverage - Thumbnail placeholder confirmed token-bound. v2.5: The placeholder inside
Thumbnail-Slotfills#EEF2F9, the shared surface value used across Form Elements rather than a local hex. Confirmed by the owner; variable bindings are not readable through the review tooling, so this is attested. (C3 · Token) C3 · Token Coverage - Lottie dependency documented. v2.5: The
Uploadingstatus is driven by a Lottie animation the static component cannot depict — the file row holds its layout while the animation plays in place of the thumbnail. It loops for the duration of the transfer and is replaced, not stopped, when the status moves toUploadedorError. If the animation fails to load, implementations should fall back to the platform’s indeterminate progress indicator rather than an empty slot, so the field never reads as idle while a transfer is running. Progress is indeterminate by design: the component reports that an upload is happening, not how far along it is. (Docs) C4 · Native Mappability
Empty state with paperclip + "Attach file / photo" placeholder text. 2px border, white bg. Subtext below lists accepted formats.
| Role | Token | Token | Value |
|---|---|---|---|
| Default | bg | main/upload-file/color/default/bg | #FFFFFF |
| — | border | main/upload-file/color/default/boder typo | #E5EBF4 |
| — | leading icon | main/upload-file/color/default/icon-leading | #6780A9 |
| — | trailing icon | main/upload-file/color/default/icon-trailing | #005CE5 |
| — | label | main/upload-file/color/default/label | #0A2757 |
| — | file name | main/upload-file/color/default/label-name | #005CE5 |
| — | progress label | main/upload-file/color/default/progress-label | #0A2757 |
| — | thumbnail bg | — (hardcoded #0057E4 @ 5%) not tokenized | — |
| Error | bg | main/upload-file/color/error/bg | #FFFFFF |
| — | border | main/upload-file/color/error/boder typo | #D61B2C |
| — | leading icon | main/upload-file/color/error/icon-leading | #6780A9 |
| — | label | main/upload-file/color/error/label | #0A2757 |
| — | file name | main/upload-file/color/error/label-name | #005CE5 |
| — | error subtext | main/subtext-message/error/label | #D61B2C |
| Subtext | default label | main/subtext-message/primary/label | #6780A9 |
Shows file name + 5px-tall Lottie progress bar + percentage. Height grows to 91px to accommodate the progress row.
| Role | Token | Value |
|---|---|---|
| Container width | — | 304px |
| Input height (default/uploaded/error) | — | 72px |
| Input height (uploading) | — | 91px (adds progress row) |
| Border width | — | 2px |
| Corner radius | radius/radius-2 | 6px |
| Horizontal padding | — | 16px (12L / 16R for thumbnail) |
| Vertical padding | — | 24px |
| Icon → name gap | space/space-4 | 4px |
| Thumbnail size | — | 52 × 52 |
| Thumbnail → name gap | space/space-8 | 8px |
| Label → input gap | space/space-8 | 8px |
| Input → subtext gap | space/space-8 | 8px |
| Progress bar height | — | 5px |
| Progress bar width | — | 250px |
| Leading / trailing icon size | — | 24 × 24 |
File name (<code>GCash_File.png</code>) + trailing trash icon for removal.
| Role | Token | Spec |
|---|---|---|
| Label | Primary/Label/Light/Small | Proxima Soft Semibold · 14 / 14 · +0.25 |
| File name / placeholder | Primary/Label/Light/Large | Proxima Soft Semibold · 18 / 18 · +0.25 |
| Subtext | Secondary/Bold/Caption | BarkAda Semibold · 12 / 18 |
| Progress percentage | Secondary/Bold/Small Caption | BarkAda Semibold · 10 / 15 |
52×52 thumbnail preview + truncated file name (<code>New_GCash_Fi….jpeg</code>) + trash. Recommended to split into <code>state=uploaded</code> + <code>hasThumbnail: true</code>.
Red 2px border + red error subtext ("Maximum file size: 20MB").
iOS — Swift Package Manager
// In Xcode: File → Add Package Dependencies "https://github.com/AY-Org/eb-ds-ios" // Requires: lottie-ios for progress animation "https://github.com/airbnb/lottie-ios"
Android — Gradle (Kotlin DSL)
dependencies { implementation("com.eastblue.ds:upload-file:1.0.0") // Requires: lottie-compose for progress animation implementation("com.airbnb.android:lottie-compose:6.4.0") }
| Figma Property | SwiftUI | Compose |
|---|---|---|
| state=Default/Uploading/Uploaded/Upload error | state: EBUploadState | state: .default / .uploading / .uploaded / .error |
| state=Uploaded with thumbnail | state=uploaded + hasThumbnail | .hasThumbnail(true) |
| hasLabel=yes/no | label: String? | label: String? |
| — | fileName: String? | fileName: String? |
| — | progress: Double | progress: Double (0.0–1.0) |
| thumbnail placeholder | Figma Slot → ViewBuilder | @ViewBuilder thumbnail |
| — | disabled: Bool | .disabled(true) |
| — | onSelect / onRemove | onSelect / onRemove |
// Default — empty state EBUploadFile(label: "Proof of ID", onSelect: { url in // handle picked file }) // Uploading EBUploadFile(fileName: "GCash_File.png", progress: 0.2) .ebState(.uploading) // Uploaded with thumbnail (Figma Slot) EBUploadFile(fileName: "ID_proof.jpg", onRemove: { ... }) { AsyncImage(url: imageURL) .aspectRatio(contentMode: .fill) .clipShape(RoundedRectangle(cornerRadius: 4)) } .ebState(.uploaded) // Error EBUploadFile(label: "Upload receipt", errorMessage: "Maximum file size: 20MB") .ebState(.error)
// Default — empty state EBUploadFile( label = "Proof of ID", onSelect = { uri -> /* handle picked file */ } ) // Uploading EBUploadFile( state = EBUploadState.Uploading, fileName = "GCash_File.png", progress = 0.2f ) // Uploaded with thumbnail (Figma Slot) EBUploadFile( state = EBUploadState.Uploaded, fileName = "ID_proof.jpg", onRemove = { /* ... */ } ) { AsyncImage( model = imageUrl, contentDescription = null, modifier = Modifier.clip(RoundedCornerShape(4.dp)) ) } // Error EBUploadFile( state = EBUploadState.Error, label = "Upload receipt", errorMessage = "Maximum file size: 20MB" )
| Requirement | iOS | Android |
|---|---|---|
| Role | .accessibilityAddTraits(.isButton) when empty; announce as "Upload" when actionable | Role.Button in semantics |
| File picked announcement | Announce file name after selection via .accessibilityAnnouncement | AccessibilityManager.announce() |
| Progress announcement | .accessibilityValue("\(Int(progress * 100)) percent") | stateDescription = "$percent percent" |
| Error announcement | Include error message in accessibility label; use .isRejected trait | semantics { error(...) } |
| Remove button | Separate accessibility element: .accessibilityLabel("Remove \(fileName)") | contentDescription = "Remove $fileName" |
| Tap target | 72px height > 44pt minimum | > 48dp minimum |
Do
Use the thumbnail slot for image uploads (ID photos, receipts) so users can verify the correct file was picked.
Don't
Show a generic thumbnail placeholder as the final state — either show the real thumbnail or use the plain uploaded state with just the filename.
Do
Always pair the default state with subtext listing accepted formats and size limits so users don't discover constraints only via error state.
Don't
Let users attempt uploads silently only to show an error — preempt format / size violations on the client side.
Do
Use the error state for client-side validation failures (size, format). Show a specific error message indicating what needs to change.
Don't
Use the error state for network failures during upload — those are transient. Show a toast or retry affordance instead.
| ID | Criterion | Status | Notes |
|---|---|---|---|
| C1 | Layer Structure & Naming | Ready | Semantic: input-field, Attach, Trash, Icon Placeholder, upload-file-progress, Subtext Message. |
| C2 | Variant & Property Naming | Requires Rework | hasLabel yes/no, state has "Upload error" with space, "Uploaded with thumbnail" is orthogonal. |
| C3 | Token Coverage | Requires Rework | Library-wide boder token typo. Thumbnail bg hardcoded. |
| C4 | Native Mappability | Ready | Maps to PhotosPicker / DocumentPicker (iOS), GetContent / PickVisualMedia (Android). |
| C5 | Interaction State Coverage | Requires Rework | No disabled, pressed, or focused states. |
| C6 | Asset & Icon Quality | Needs Refinement | Thumbnail is a placeholder; Lottie dependency needs documentation. |
| C7 | Code Connect Linkability | Needs Refinement | Blocked by C2 cleanup. |
5 state × 2 hasLabel = 10 variants. Clean matrix — every combination exists.
| State | hasLabel | Count |
|---|---|---|
| Default | yes + no | 2 |
| Uploading | yes + no | 2 |
| Uploaded | yes + no | 2 |
| Upload error | yes + no | 2 |
| Uploaded with thumbnail | yes + no | 2 |
hasLabel=yes/no, state="Upload error" has a space, "Uploaded with thumbnail" is orthogonal to the state axis. Openboder. Library-level rename needed. Open#0057E4 @ 5% not tokenized. Open