East Blue
KeepReady
Upload File Component link

A file-upload field with a tappable upload affordance, file-name display, and progress/error states.

Keep — all findings resolved
Rebuilt on node 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.
In Context

Contexts are illustrative. Final screens will reference actual GCash patterns. Upload File appears in forms requiring document proof (KYC, insurance claims, verification).

Upload Docs Attach file / photo ID_proof.jpg Submit
Live Preview
Attach file / photoAccepted format: JPEG, PNG, or PDF, Up to 3 MB
Properties
state
hasLabel
DS Health
Reusable
Pass
Used in KYC flows, insurance claims, profile setup — anywhere a user uploads a document or photo. Status covers the full upload lifecycle — Default, Uploading, Uploaded and Error.
Self-contained
Pass
Carries its own bg, border, padding and radius, and composes shared 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.
Consistent
Pass
The property model is exemplary — 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.
Composable
Pass
The thumbnail is a real Figma 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.
Behavior
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
Issues
  • Property naming cleaned up. v2.0: Rebuilt on node 4853:26511 in the 2026 Working File. Upload error → Error (no space), Uploaded with thumbnail split so thumbnail presence is no longer folded into the state value, and hasLabel is gone — the label is now an always-present FormGroup Header instance. (C2) C2 · Variant & Property Naming
  • Label and subtext scaffolding reused. v2.0: Every variant composes a shared FormGroup Header above and a Subtext Message below, 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, Uploaded and Error are process outcomes rather than interaction states, so Status is 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 Status axis rather than a separate State axis; see open issues for the consequence. (C5) C5 · Interaction State Coverage
  • Error variant structure normalised. v2.1: Status=Error now wraps its input-field in an Attach File - Input frame (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
  • State split onto its own axis. v2.2: Status = Default | Uploading | Uploaded | Error × State = Default | Disabled, authored as a deliberately sparse sparse matrix. Disabled × Uploading and Disabled × Error are 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, and Pressed / Focused have somewhere to live. (C2 · Property) C2 · Variant & Property Naming
  • Disabled + Uploaded variant authored. v2.2: 6572:111324 covers 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 Placeholder is now Thumbnail-Slot, a real SLOT node (6572:111570) carrying an icon-placeholder rectangle 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=Error no 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
  • label wrapper renamed to FileRow. v2.2: The frame holding the thumbnail, attach icon and filename is now FileRow rather than label, 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 went name → FileNameWrapper. Layer naming is now clean apart from container. The asset behind progress-fill is a separate finding — see open issues. (C1) C1 · Layer Structure & Naming
  • Focused state added. v2.4: State = Default | Focused | Disabled, with Focused authored against Status=Default (6572:111764) and Status=Uploaded (6572:111782) — the same two pairings Disabled carries, and for the same reason. Eight variants in the sparse matrix. Completes interaction-state coverage, since Pressed is deliberately out of scope. (C5) C5 · Interaction State Coverage
  • Layer naming complete across all eight variants. v2.5: The wrapper frame is now exactly UploadField in all eight variants — verified by exact string match, with no trailing whitespace and no leftover container. 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 with FileRow, FileName, FileNameWrapper, ProgressValue and Thumbnail-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 the boder misspelling 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-fill exists so the Figma variant previews motion, not as a handoff asset. Native implementations use the platform progress primitive (SwiftUI ProgressView(value:), Compose LinearProgressIndicator(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
  • Pressed ruled out of scope. v2.2: Closed by owner decision — Upload File will not carry a Pressed variant. 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. Focused remains in scope; it carries accessibility weight that Pressed does 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 genuine SLOT node, 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 Header instance and the row below is a Subtext Message instance — 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, FileName and TrailingAction all read as PascalCase semantic names, and the trailing control is a real Trash icon instance rather than a drawn glyph. (C1) C1 · Layer Structure & Naming
  • Container frame names accepted as-is. v2.5: Attach File - Input and input-field keep 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. Uploading has no Focused or Disabled because a field mid-transfer is not interactive — the control is busy, not available. Error has no Focused or Disabled because the error state is what a user focuses to fix, so it collapses back to Default the moment they do, and a disabled field cannot have failed an upload it was never able to start. Only Default and Uploaded carry the full interaction range. Native implementations should treat the missing combinations as unreachable rather than undrawn. (C5) C5 · Interaction State Coverage
  • boder token 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-Slot fills #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 Uploading status 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 to Uploaded or Error. 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
Styles
Upload File
LabelAttach file / photoAccepted format: JPEG, PNG, or PDF, Up to 3 MB
Properties
Status
State
hasLabel
hasThumbnail
hasLeadingIcon
hasSubtext
Properties
Status Default
State Default
hasLabel True
hasThumbnail True
hasLeadingIcon True
hasSubtext True
Colors
Field background #FFFFFF
Border #E5EBF4
File name #90A8D0
Thumbnail placeholder #EEF2F9
Attach icon —
Trash icon –
Label #0A2757
Subtext #6780A9
Typography
Label —
File name —
Subtext —
Layout
Height 100px
Width 304px
Radius 6px
Padding H 16px
Padding V 24px
Gap 4px
Alignment —
Colors by Status

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
Installation Planned API

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.*
Property Mapping
Figma PropertySwiftUICompose
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
Usage Snippets Planned API
Empty field
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"
)
Uploading — indeterminate progress
EBUploadField(label: "Label", file: $file)
    .ebStatus(.uploading)
EBUploadField(
    label = "Label",
    file = file,
    status = EBUploadStatus.Uploading
)
Uploaded — with a thumbnail
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 }
)
Error
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"
)
Disabled
EBUploadField(label: "Label", file: $file)
    .disabled(true)
EBUploadField(
    label = "Label",
    file = file,
    enabled = false
)
Accessibility
RequirementiOSAndroid
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
Usage Guidelines

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.

Criteria Scorecard
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.
Variants Inventory (8 total)

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.

StatusStateDimensionsNode ID
DefaultDefault304 × 1484850:26409
DefaultDisabled304 × 1486526:105079
DefaultFocused304 × 1486572:111764
UploadingDefault304 × 1674868:26884
ErrorDefault304 × 1484850:26460
UploadedDefault304 × 1484868:26932
UploadedDisabled304 × 1486572:111324
UploadedFocused304 × 1486572:111782
2.6 — September 2026Minor
Style + Code tabs rebuilt against node 4853:26511
Style tab rebuilt as a single card — one spec card driven by a panel mirroring the Figma property panel: 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. Documented
Style
State control constrained to real variants — selecting Uploading or Error disables Focused and Disabled, marking them “not built”, so the panel cannot produce the four combinations Figma does not contain. Documented
Style
Booleans reflow rather than hide — turning off the thumbnail collapses 52px and shifts the file name left; the leading icon collapses 28px; the label and subtext blocks remove their rows and the card shrinks. Documented
Style
Disabled label corrected — the preview had muted it. Verified on 6526:105079: the label stays #0A2757 in Disabled while the field contents mute. The thumbnail placeholder is #F6F9FD there, not the value first drawn. Documented
Style
Property mapping corrected — the retired single state 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. Documented
Code
Variants inventory corrected — from total: 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. Documented
Code
2.5 — September 2026Minor
Naming completed and remaining items closed
Layer naming complete across all eight variants. Resolved
C1
Container frame names accepted as-is — Attach File - Input and input-field are internal structural frames that nothing binds to; renaming carried more churn than the consistency was worth. Resolved
C1
Unsupported variant combinations documented — Uploading 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. Resolved
C5
boder token typo corrected — fixed before Code Connect could bind generated native constants to the misspelling. Resolved
C3
Thumbnail placeholder confirmed token-bound at the shared surface value. Resolved
C3
Lottie dependency documented — indeterminate by design, replaced rather than stopped on completion, and falling back to the platform progress indicator if it fails to load. Resolved
C6
2.4 — September 2026Major
State property split onto two axes
State restructured onto two axes — Status = Default | Uploading | Error | Uploaded beside State = Default | Focused | Disabled, the split §6 of the Property Naming Guidelines prescribes. Resolved
C2
Thumbnail is a real Figma Slot — a genuine SLOT node, kebab-case per §4, so a consumer swaps their own preview in without detaching. Resolved
C1
Label and subtext scaffolding reuses the shared components — FormGroup Header and Subtext Message instances rather than redrawn locally. Resolved
C4
File row layer naming cleaned up — FileRow, FileNameWrapper, FileName, TrailingAction, with a real Trash instance. Resolved
C1
Focused state added. Resolved
C5
Token namespace resolved — generic tokens applied. Resolved
C3
Progress-bar GIF confirmed preview-only — a Figma preview device, not a shipped asset. Resolved
C6
2.3 — September 2026Patch
Progress-bar naming
Progress-bar layer names cleaned up. Resolved
C1
2.2 — September 2026Minor
Structure normalised
State split onto its own axis. Resolved
C2
Disabled + Uploaded variant authored. Resolved
C5
Thumbnail converted to a Figma Slot. Resolved
C1
Error progress bar removed — a failed upload has no progress to show. Resolved
C5
label wrapper renamed FileRow. Resolved
C1
Pressed ruled out of scope — the field opens a system picker; the picker owns the press feedback. Resolved
C5
2.1 — September 2026Minor
Axis renamed and variants filled in
Axis renamed State → Status — the values describe what the system is reporting, not how the user is interacting. Resolved
C2
Disabled variant added. Resolved
C5
Error variant structure normalised. Resolved
C1
2.0 — September 2026Major
Rebuilt on node 4853:26511 — 2026 Working File
Property naming cleaned up — "Upload error" with a space and the orthogonal "Uploaded with thumbnail" both retired from the value list. Resolved
C2
Label and subtext scaffolding reused from the shared Form Elements components. Resolved
C4
1.0.0 — April 2026Major
Initial Assessment · node 18482:35064
Component assessed — 10 variants (5 state × 2 hasLabel). Lottie progress bar, thumbnail placeholder. Documented
Initial
Property naming issues — hasLabel=yes/no, state="Upload error" has a space, "Uploaded with thumbnail" is orthogonal to the state axis. Open
C2 Open
Token typo — All border tokens spelled boder. Library-level rename needed. Open
C3 Open
Thumbnail bg hardcoded — #0057E4 @ 5% not tokenized. Open
C3 Open
Missing states — No disabled, pressed, or focused. Open
C5 Open
Thumbnail placeholder + Lottie dependency — Thumbnail is not a slot; Lottie requires asset bundling. Open
C6 Open
Code Connect mappings — Not registered. Open
C7 Open