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 |
- 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 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
Read off node 4853:26511 and checked against an export. ROLE is the Status value; the second column is the ELEMENT. Focused replaces the border with #005CE5 at 2px and Disabled drops it entirely for an #EEF2F9 fill — both are State, not Status. Token paths are indicative; variable bindings are not readable through the plugin.
| Role | Element | Token | Value |
|---|---|---|---|
| Default | Field background | surface/default | #FFFFFF |
| — | Border | border/subtle | #E5EBF4 |
| — | File name | text/placeholder | #90A8D0 |
| — | Thumbnail placeholder | surface/subtle | #EEF2F9 |
| — | Attach icon | — | — |
| — | Label | text/primary | #0A2757 |
| — | Subtext | text/secondary | #6780A9 |
| Uploading | Field background | surface/default | #FFFFFF |
| — | Border | border/subtle | #E5EBF4 |
| — | File name | text/interactive | #005CE5 |
| — | Thumbnail placeholder | surface/subtle | #EEF2F9 |
| — | Attach icon | — | — |
| — | Label | text/primary | #0A2757 |
| — | Subtext | text/secondary | #6780A9 |
| Error | Field background | surface/default | #FFFFFF |
| — | Border | border/error | #D61B2C |
| — | File name | text/interactive | #005CE5 |
| — | Thumbnail placeholder | surface/subtle | #EEF2F9 |
| — | Attach icon | — | — |
| — | Label | text/primary | #0A2757 |
| — | Subtext | text/error | #D61B2C |
| Uploaded | Field background | surface/default | #FFFFFF |
| — | Border | border/subtle | #E5EBF4 |
| — | File name | text/interactive | #005CE5 |
| — | Thumbnail placeholder | surface/subtle | #EEF2F9 |
| — | Attach icon | — | — |
| — | Trash icon | icon/interactive | #005CE5 |
| — | Label | text/primary | #0A2757 |
| — | Subtext | text/secondary | #6780A9 |
iOS — Swift Package Manager
.package(url: "https://github.com/AY-Org/eb-ds-ios", from: "1.0.0")
Android — Gradle (Kotlin DSL)
implementation("com.eastblue.ds:form-elements:1.0.0")
Import
import EastBlueDS import com.eastblue.ds.form.*
| Figma Property | SwiftUI | Compose |
|---|---|---|
| Status = Default | Uploading | Error | Uploaded | status: EBUploadStatus | status = EBUploadStatus.Default |
| State = Default | Disabled | Focused | .disabled(true) · @FocusState | enabled = false · interactionSource |
| hasLabel (boolean) | label: String? | label: String? |
| hasThumbnail (boolean) | showThumbnail: Bool | showThumbnail: Boolean |
| hasLeadingIcon (boolean) | showAttachIcon: Bool | showAttachIcon: Boolean |
| hasSubtext (boolean) | subtext: String? | subtext: String? |
| Thumbnail-Slot (instance swap) | @ViewBuilder thumbnail | thumbnail: @Composable () -> Unit |
| — no Figma property | fileName: String? | fileName: String? |
| — no Figma property | progress: Double (0.0–1.0) | progress: Float |
| — no Figma property | onSelect / onRemove | onSelect / onRemove |
EBUploadField(label: "Label", file: $file) .ebStatus(.default) .ebSubtext("Accepted format: JPEG, PNG, or PDF, Up to 3 MB")
EBUploadField( label = "Label", file = file, status = EBUploadStatus.Default, subtext = "Accepted format: JPEG, PNG, or PDF, Up to 3 MB" )
EBUploadField(label: "Label", file: $file) .ebStatus(.uploading)
EBUploadField( label = "Label", file = file, status = EBUploadStatus.Uploading )
EBUploadField(label: "Label", file: $file) .ebStatus(.uploaded) .ebThumbnail { Image(uiImage: preview) } .onRemove { file = nil }
EBUploadField( label = "Label", file = file, status = EBUploadStatus.Uploaded, thumbnail = { Image(preview, null) }, onRemove = { file = null } )
EBUploadField(label: "Label", file: $file) .ebStatus(.error) .ebSubtext("Maximum file size: 20MB")
EBUploadField( label = "Label", file = file, status = EBUploadStatus.Error, subtext = "Maximum file size: 20MB" )
EBUploadField(label: "Label", file: $file) .disabled(true)
EBUploadField( label = "Label", file = file, enabled = false )
| 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 | UploadField → FileRow → Thumbnail-Slot · FileName · TrailingAction, with a real Trash instance. Two internal container frames — Attach File - Input and input-field — keep their original names as a recorded exception; nothing binds to them. |
| C2 | Variant & Property Naming | Ready | Status = Default | Uploading | Error | Uploaded × State = Default | Disabled | Focused, the two-axis split §6 prescribes, plus four has booleans in lowerCamelCase per §2. The old single axis with "Upload error" and "Uploaded with thumbnail" is gone. |
| C3 | Token Coverage | Needs Refinement | The boder typo is corrected and the thumbnail placeholder is confirmed token-bound at #EEF2F9. Both are attested rather than verified — token names and variable bindings are not readable through the review tooling. |
| C4 | Native Mappability | Ready | Maps to PhotosPicker / DocumentPicker on iOS and GetContent / PickVisualMedia on Android. Thumbnail-Slot is a real Figma Slot, so the preview binds to a ViewBuilder rather than a fixed image. |
| C5 | Interaction State Coverage | Ready | State ships Default, Disabled and Focused on Status=Default and Uploaded. Uploading and Error carry State=Default only — documented as unreachable rather than undrawn: a transfer in flight is not interactive, and a disabled field cannot have failed an upload it never started. |
| C6 | Asset & Icon Quality | Ready | The Attach and Trash glyphs are shared icon instances. The thumbnail is a Slot with a token-bound placeholder. The Lottie dependency behind Uploading is documented, including its failure fallback. |
Status (4) × State (3) describes twelve combinations; eight are built. Uploading and Error ship State=Default only — the four missing pairings are unreachable by design rather than unfinished. The four has booleans are component properties, not variant axes, so they do not multiply the count. Uploading is 19px taller for the progress row.
| Status | State | Dimensions | Node ID |
|---|---|---|---|
| Default | Default | 304 × 148 | 4850:26409 |
| Default | Disabled | 304 × 148 | 6526:105079 |
| Default | Focused | 304 × 148 | 6572:111764 |
| Uploading | Default | 304 × 167 | 4868:26884 |
| Error | Default | 304 × 148 | 4850:26460 |
| Uploaded | Default | 304 × 148 | 4868:26932 |
| Uploaded | Disabled | 304 × 148 | 6572:111324 |
| Uploaded | Focused | 304 × 148 | 6572:111782 |
Status and State as selects, and hasLabel, hasThumbnail, hasLeadingIcon, hasSubtext as toggles. The four booleans were invisible to get_node_info, which returns variant properties only. DocumentedUploading or Error disables Focused and Disabled, marking them “not built”, so the panel cannot produce the four combinations Figma does not contain. Documented6526:105079: the label stays #0A2757 in Disabled while the field contents mute. The thumbnail placeholder is #F6F9FD there, not the value first drawn. Documentedstate axis, "Uploaded with thumbnail" and hasLabel=yes/no are gone. Now maps Status, State, the four booleans and Thumbnail-Slot, plus the file name, progress and callbacks. Documentedtotal: 10 on a “5 state × 2 hasLabel” matrix to the real 8, with the four unreachable pairings explained. codeConnect was an empty array and is now filled. DocumentedAttach File - Input and input-field are internal structural frames that nothing binds to; renaming carried more churn than the consistency was worth. ResolvedUploading and Error have no Focused or Disabled because a transfer in flight is not interactive and a disabled field cannot have failed an upload it never started. Resolvedboder token typo corrected — fixed before Code Connect could bind generated native constants to the misspelling. ResolvedState restructured onto two axes — Status = Default | Uploading | Error | Uploaded beside State = Default | Focused | Disabled, the split §6 of the Property Naming Guidelines prescribes. ResolvedSLOT node, kebab-case per §4, so a consumer swaps their own preview in without detaching. ResolvedFormGroup Header and Subtext Message instances rather than redrawn locally. ResolvedFileRow, FileNameWrapper, FileName, TrailingAction, with a real Trash instance. ResolvedFocused state added. ResolvedState split onto its own axis. Resolvedlabel wrapper renamed FileRow. ResolvedPressed ruled out of scope — the field opens a system picker; the picker owns the press feedback. ResolvedState → Status — the values describe what the system is reporting, not how the user is interacting. Resolved"Upload error" with a space and the orthogonal "Uploaded with thumbnail" both retired from the value list. ResolvedhasLabel=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