Components · Forms & selection
File input
Drag-and-drop file upload zone with file list and upload progress. Split from zetta-form-v1_4.md (removed in v1.5) in v1.4.
Preview
Drop files here or browse
PNG, JPG or PDF up to 10MB
brand-guidelines.pdf
2.4 MB
hero-render.psd
Too large
Build with the skill
No package to install — hand this to your AI to generate or migrate the component in your stack.
Apply the Zetta "File input" component to this project using the zetta-design-md skill.
WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `file-input` spec and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `file-input` spec in this project's stack.
AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.
ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility. Anatomy & rules
╭──────────────────────────────────────╮
│ ↑ upload icon (20px) │ ← dashed border drop zone
│ Drag files here or click to browse │
│ PNG, JPG up to 5 MB │ ← helper text
╰──────────────────────────────────────╯
[📄 report.pdf ×] ← file-input-file-item
[████████░░] 80% ← file-input-progress
[⚠ image.exe ×] ← file-input-file-item-error
| State | Border | Fill | Notes |
|---|---|---|---|
| Default | 1px dashed {colors.border-strong} |
{colors.surface-card} |
— |
| Hover | 1px dashed {colors.border-focus} |
{colors.primary-bg} |
— |
| Focus | 2px solid {colors.border-focus} outline |
{colors.surface-card} |
Keyboard only |
| Drag over | 1px dashed {colors.primary} |
{colors.primary-bg} |
Icon shifts to primary |
| Disabled | 1px dashed {colors.disabled-border} |
{colors.disabled-bg} |
not-allowed |
| Error | 1px dashed {colors.danger} |
{colors.surface-card} |
— |
Do
- State accepted file types and size limit in helper text inside the drop zone: "PNG, JPG up to 5 MB."
- Show upload progress per file once upload starts.
- Show an error file item (red pill) for rejected files with a reason.
- Allow click-to-browse as a fallback for users who cannot drag.
Don't
- Don't hide the file list after upload — users need confirmation of what was uploaded.
- Don't auto-submit on file selection — let the user review before committing.
- Drop zone label: "Drag files here or click to browse."
- Helper text: list accepted types and size limit. "PNG, JPG up to 5 MB." or "PDF only, max 10 MB."
- File item: show filename + file size. Truncate long names with ellipsis in the middle: "very-long-fi…name.pdf".
- Error item: show the rejection reason next to the filename. "File too large", "Unsupported format."
- Remove button
aria-label: "Remove [filename]".
Keyboard
| Key | Behavior |
|---|---|
Tab |
Focus the drop zone |
Enter / Space |
Open the native file browser |
Tab (on file item remove button) |
Focus remove button |
Enter / Space (on remove button) |
Remove file from list |
Accessibility
- Drop zone must be a focusable
<button>or<div>withrole="button"andtabindex="0". - Screen reader instructions: include a visually hidden text explaining drag-and-drop and click-to-browse.
aria-live="polite"region announces file additions, removals, and upload progress updates.- Each file item's remove button:
aria-label="Remove [filename]". - Progress bar:
role="progressbar"witharia-valuenow,aria-valuemin="0",aria-valuemax="100". - Error state:
aria-describedbyon the drop zone pointing to the error message.
Spec
Values and token references resolved straight from the Zetta spec.
file-input
- backgroundColor
- {colors.surface-card}
- borderColor
- {colors.border-strong}
- borderWidth
- 1px
- borderStyle
- dashed
- borderRadius
- {rounded.base}
- textColor
- {colors.muted}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 400
- lineHeight
- 1.6
- padding
- {spacing.md}
- display
- flex
- flexDirection
- column
- alignItems
- center
- justifyContent
- center
- gap
- {spacing.sm}
- minHeight
- 96px
- cursor
- pointer
- iconSize
- 20px
- iconColor
- {colors.muted}
file-input-hover
- borderColor
- {colors.border-focus}
- backgroundColor
- {colors.primary-bg}
file-input-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
- borderColor
- {colors.border-focus}
file-input-drag-over
- borderColor
- {colors.primary}
- backgroundColor
- {colors.primary-bg}
- iconColor
- {colors.primary}
file-input-disabled
- backgroundColor
- {colors.disabled-bg}
- borderColor
- {colors.disabled-border}
- textColor
- {colors.disabled-text}
- cursor
- not-allowed
file-input-error
- borderColor
- {colors.danger}
file-input-file-list
- display
- flex
- flexDirection
- column
- gap
- {spacing.xs}
- marginTop
- {spacing.sm}
file-input-file-item
- display
- flex
- alignItems
- center
- gap
- {spacing.sm}
- backgroundColor
- {colors.surface-secondary}
- borderRadius
- {rounded.base}
- paddingX
- 12px
- paddingY
- {spacing.sm}
- fontFamily
- Geist
- fontSize
- 13px
- fontWeight
- 400
- textColor
- {colors.ink}
- iconSize
- 16px
- iconColor
- {colors.muted}
file-input-file-item-error
- backgroundColor
- {colors.danger-bg}
- textColor
- {colors.danger-text}
- iconColor
- {colors.danger-text}
file-input-progress
- height
- 4px
- borderRadius
- {rounded.full}
- trackColor
- {colors.surface-muted}
- fillColor
- {colors.primary}
- marginTop
- {spacing.xs}