Primitives Forms
File field
The native file input with a styled selector button. The browser owns the file chooser, multiple, accept, capture, the file names and the form value; the package adds a stylesheet and a contract. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
<input type="file">,multiple,accept,capture,::file-selector-button- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<input type="file">
On this page
Example
Images or PDF files.
<label for="file-field-attachments">Attachments</label>
<input
id="file-field-attachments"
type="file"
name="attachments"
data-slean="file-field"
accept="image/*,.pdf"
multiple
aria-describedby="file-field-attachments-hint"
/>
<p id="file-field-attachments-hint">Images or PDF files.</p><script lang="ts">
// Nothing to import: the browser owns the field. The stylesheet is optional.
import '@svelte-lean/styles/file-field.css';
</script>
<label for="file-field-attachments">Attachments</label>
<input
id="file-field-attachments"
type="file"
name="attachments"
data-slean="file-field"
accept="image/*,.pdf"
multiple
aria-describedby="file-field-attachments-hint"
/>
<p id="file-field-attachments-hint">Images or PDF files.</p>Why this implementation exists
A common file control hides the input and styles a label as its button. It then has to rebuild what the input showed: the chosen file names, the focus ring and the keyboard path, usually with a script that reads files on every change.
::file-selector-button styles the input’s own button instead. The input stays visible, focusable and labelled, and the browser keeps showing what was chosen.
The browser owns
- the file chooser, filtered by accept, with several files under multiple
- the camera on mobile devices for capture
- the button label and the chosen file names, localized
- form participation (multipart/form-data) and the label association
Svelte Lean owns
- file-field.css: the box and the selector button, styled through ::file-selector-button
- the contract
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/file-field.css';Give the input a name and submit it in a form with method="post" and enctype="multipart/form-data", or read its files in the application.
accept filters the chooser by MIME type or extension. It is not validation, since the chooser can show all files: check the type on the server. multiple allows several files; capture asks a mobile browser for the camera.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <input type="file" data-slean="file-field"> | – | yes | A label and a name; accept, multiple and capture as needed. |
| selector button | ::file-selector-button | – | styles only | The browser’s button inside the input; its label cannot be changed from CSS. |
Runtime profile
Tier 0: the field has no behavior module, and the Vite plugin maps file-field to no module. Choosing, filtering and submitting are the browser’s.
Accessibility contract
- A label through
<label for>; accepted types and limits througharia-describedby. - The input is one tab stop; Enter or Space opens the chooser, which belongs to the operating system.
- The focus ring is the shared one, drawn around the whole field.
:user-invalidmarks arequiredfield left empty only after the user interacted with it.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab/Shift+Tab | anywhere | Moves focus to and from the field (native) |
| Enter/Space | focus on the field | Opens the file chooser, which has its own keyboard (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
<input type="file">, multiple, accept | Widely available | Not applicable |
::file-selector-button | Baseline 2021 | The browser’s default button inside the styled box |
capture | Honored by mobile browsers | Desktop browsers open the file chooser |
Without JavaScript
Fully functional: choosing files and submitting them with a form need no script.
Server rendering
Static HTML. A file input cannot have an initial value; the server renders it empty.
Before hydration
Nothing is attached; the field works before and after hydration alike.
Styling
file-field.css draws the box at the control height of the other fields, styles ::file-selector-button as a quiet button inside it and grows both on coarse pointers. The button label and the file name text cannot be replaced from CSS. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-control-height-md | 2.25rem | min-block-size |
--slean-space-1 | 0.25rem | padding, min-block-size, border-radius |
--slean-space-3 | 0.75rem | padding-inline-end, margin-inline, padding-inline |
--slean-control-border | var(--slean-border-strong) | border |
--slean-radius-md | 0.625rem | border-radius |
--slean-control-bg | var(--slean-surface) | background |
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-muted | var(--slean-neutral-3) | background |
--slean-fg | var(--slean-neutral-12) | color |
--slean-font-weight-medium | 500 | font-weight |
--slean-control-border-hover | var(--slean-accent) | border-color |
--slean-control-border-focus | var(--slean-accent) | border-color |
--slean-control-ring | 0 0 0 3px color-mix(in oklch, var(--slean-accent) 22%, transparent) | box-shadow |
--slean-warning | oklch(76% 0.16 80) | border-color |
--slean-control-ring-warning | 0 0 0 3px color-mix(in oklch, var(--slean-warning) 28%, transparent) | box-shadow |
--slean-danger | oklch(55% 0.2 25) | border-color |
--slean-control-ring-danger | 0 0 0 3px color-mix(in oklch, var(--slean-danger) 22%, transparent) | box-shadow |
--slean-control-border-disabled | var(--slean-border) | border-color |
--slean-control-bg-disabled | var(--slean-muted) | background-color |
--slean-muted-hover | var(--slean-neutral-4) | background |
--slean-text-md | 1rem | font-size |
State selectors the stylesheet targets, all from the platform or ARIA: ::file-selector-button, :disabled, :focus-visible, :hover, :user-invalid, [aria-invalid="true"].
Variant attributes: data-status (warning, error).
Controlled integration
The chosen files are the input’s files. bind:files gives the application the FileList to list, preview or upload; the field renders nothing beyond the browser’s summary.
<script lang="ts">
let files = $state<FileList>();
</script>
<input id="file-field-attachments" type="file" name="attachments" data-slean="file-field"
multiple bind:files />
{#if files?.length}
<ul>
{#each files as file (file.name)}
<li>{file.name}</li>
{/each}
</ul>
{/if}Compatibility notes
::file-selector-button is Baseline 2021; where it is missing, the browser’s own button appears inside the styled box. The package adds no drop zone: dropping a file onto the input itself is the browser’s behavior where it exists, and a larger drop area needs dragover and drop listeners, which this Tier 0 primitive does not ship.
Examples
Camera
capture asks a mobile browser to open the camera instead of the chooser; environment is the rear camera, user the front one. Desktop browsers ignore
it and open the file chooser.
<!-- On a phone, capture offers the camera directly; desktop browsers open the chooser. -->
<label for="file-field-receipt">Receipt photo</label>
<input
id="file-field-receipt"
type="file"
name="receipt"
data-slean="file-field"
accept="image/*"
capture="environment"
/>Testing
apps/playground/tests/primitives/inputs-overlays.spec.tsPlaywright, with page JavaScript enabled and disabled, and axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/file-field/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/file-field/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/file-field.cssthe optional stylesheet