Primitives Forms
Input
The native text inputs and the textarea: text, email, password, search, url and tel. The browser provides editing, autofill, validation 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="text|email|password|search|url|tel">,<textarea>,:user-invalid,:read-only,::placeholder,field-sizing: content- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<input type="text|email|password|search|url|tel">, <textarea>
On this page
Example
<label for="input-email">Email</label>
<input id="input-email" type="email" name="email" autocomplete="email"
placeholder="name@example.com" required data-slean="input" />
<label for="input-password">Password</label>
<input id="input-password" type="password" name="password"
autocomplete="current-password" data-slean="input" />
<label for="input-notes">Notes</label>
<textarea id="input-notes" name="notes" data-slean="input"></textarea><script lang="ts">
// Nothing to import: the browser owns the field. The stylesheet is optional.
import '@svelte-lean/styles/input.css';
let email = $state('');
let notes = $state('');
</script>
<label for="input-email">Email</label>
<input id="input-email" type="email" name="email" autocomplete="email"
placeholder="name@example.com" required bind:value={email} data-slean="input" />
<label for="input-password">Password</label>
<input id="input-password" type="password" name="password"
autocomplete="current-password" data-slean="input" />
<label for="input-notes">Notes</label>
<textarea id="input-notes" name="notes" bind:value={notes} data-slean="input"></textarea>Why this implementation exists
A text field is the control every form has, and the platform implements all of it: editing and selection in every script, autofill and password managers keyed by autocomplete, the on-screen keyboard chosen by type, constraint validation on submit and :user-invalid after an edit. A component that wraps <input> to provide these adds code the element does not need.
Svelte Lean ships the element as it is, in three sizes, with the states the platform reports (:read-only, :disabled, :user-invalid, ::placeholder) and aria-invalid for an error the application found. The textarea grows with its content through field-sizing: content where the browser supports it.
The browser owns
- editing, selection, autofill and spell checking
- constraint validation (required, pattern, minlength, the email and url formats) and :user-invalid after an edit
- the platform keyboard chosen by type and autocomplete
- implicit form submission with Enter, and the form value
- a textarea that grows with its content (field-sizing)
Svelte Lean owns
- input.css: three sizes, border, colors, read-only, invalid and disabled states, a three-line textarea minimum
- 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/input.css';Put data-slean="input" on an <input> of type text, email, password, search, url or tel, or on a <textarea>. Give it a label and the autocomplete token that matches the value.
data-size="sm" or data-size="lg" changes the height and padding. Bind the value with bind:value; it is always a string.
For a description and an error message under the input, put it in a Field. Other input types have their own primitives: Number field, Date field, Color field, File field, Slider.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <input type="text|email|password|search|url|tel" data-slean="input"> | – | yes | A label through <label for>; autocomplete with the right token; data-size="sm" or "lg". |
| root (multi-line) | <textarea data-slean="input"> | – | no | Full width of its container, three lines at least, growing with its content where field-sizing is supported. |
Runtime profile
Tier 0: the input has no behavior module, the Vite plugin maps input to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.
Accessibility contract
- A visible label through
<label for>. Aplaceholderis a hint about the format, not a label: it disappears when the user types. autocompletetokens let the browser and password managers fill the field;typechooses the on-screen keyboard.- The browser exposes
requiredand constraint failures itself. An error the application found isaria-invalid="true"on the input and a message listed inaria-describedby. :user-invalidcolors a field only after the user edited it, so an untouched required field is not red.- Under a coarse pointer the text takes the base size, which keeps iOS Safari from zooming the page when a field takes focus.
Keyboard
| Key | When | Result |
|---|---|---|
| characters | focus in the field | Types (native) |
| ArrowLeft/ArrowRight/Home/End | focus in the field | Moves the caret; Shift extends the selection (native) |
| Enter | focus in an <input> | Submits the form (implicit submission, native) |
| Enter | focus in a <textarea> | Inserts a line break (native) |
| Escape | focus in type="search" | Clears the value in Chrome and Safari (native) |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
Text-like inputs, <textarea>, ::placeholder, :read-only | Widely available | Not applicable |
:user-invalid | Baseline 2023 (Chrome 119, Firefox 88, Safari 16.5) | No invalid border after an edit |
field-sizing: content | Newly available since June 2026 (Chrome 123, Safari 26.2, Firefox 152) | The textarea keeps the height of its rows (three lines at least) and scrolls |
lh unit | Baseline 2023 (Chrome 109, Safari 16.4, Firefox 120) | No three-line minimum; rows sets the height |
Without JavaScript
Fully functional, including validation on submit and the invalid border after an edit.
Server rendering
The value attribute, or the textarea’s text, renders the initial value; required, pattern and aria-invalid render as attributes.
Before hydration
Nothing is attached; the field works before and after hydration alike.
Styling
input.css sets the height from the control-height tokens, the border, background and text colors, the muted background of :read-only, the danger border of :user-invalid and aria-invalid, half opacity for :disabled, and a three-line minimum for the textarea (in lh, so it follows the line height). The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-control-height-md | 2.25rem | min-block-size |
--slean-space-1 | 0.25rem | padding-block |
--slean-space-3 | 0.75rem | 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 | var(--slean-neutral-12) | color |
--slean-accent | oklch(54% 0.19 258) | caret-color |
--slean-text-sm | 0.875rem | font-size |
--slean-leading | 1.5 | line-height |
--slean-duration-fast | 100ms | transition |
--slean-ease | cubic-bezier(0.2, 0, 0, 1) | transition |
--slean-fg-muted | var(--slean-neutral-11) | color |
--slean-control-height-sm | 2rem | min-block-size |
--slean-space-2 | 0.5rem | padding-inline, min-block-size, padding-block |
--slean-radius-sm | 0.375rem | border-radius |
--slean-control-height-lg | 2.75rem | min-block-size |
--slean-space-4 | 1rem | padding-inline |
--slean-text-md | 1rem | font-size |
--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 |
State selectors the stylesheet targets, all from the platform or ARIA: ::placeholder, :disabled, :focus-visible, :hover, :user-invalid, [aria-invalid="true"].
Variant attributes: data-size (sm, lg); data-status (warning, error).
Compatibility notes
Text-like inputs and the textarea are widely available; :user-invalid is Baseline 2023. field-sizing is newly available since June 2026 (Chrome 123, Safari 26.2, Firefox 152): where it is missing, the textarea keeps the height of its rows, three lines at least, and scrolls.
Examples
Sizes
data-size="sm" and data-size="lg" change the height, the inline padding
and, for the large size, the text; without the attribute the size is medium. Under a coarse pointer
the medium size takes the large height.
<input data-slean="input" data-size="sm" value="Small" aria-label="Small" />
<input data-slean="input" value="Medium" aria-label="Medium" />
<input data-slean="input" data-size="lg" value="Large" aria-label="Large" />States
Every state is the platform’s or an ARIA attribute: readonly keeps the value
focusable and submitted, disabled removes it from the tab order and the form data,
and aria-invalid="true" marks an error the application found. The error text is the application’s;
the Field page shows where it goes.
Use letters, digits and hyphens only.
<div data-slean="field">
<label for="input-plan" data-slean-part="label">Plan</label>
<input id="input-plan" value="Team" readonly data-slean="input" />
</div>
<div data-slean="field">
<label for="input-seats" data-slean-part="label">Seats</label>
<input id="input-seats" value="12" disabled data-slean="input" />
</div>
<div data-slean="field">
<label for="input-handle" data-slean-part="label">Handle</label>
<input id="input-handle" value="ada lovelace" aria-invalid="true"
aria-describedby="input-handle-error" data-slean="input" />
<p id="input-handle-error" data-slean-part="error">Use letters, digits and hyphens only.</p>
</div>Testing
apps/playground/tests/primitives/forms.spec.tsPlaywright: keyboard, form values, :user-invalid, names and descriptions, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)fixtures/native-onlyconsumer build asserting that no behavior runtime ships (invariant A)
Source
packages/primitives/src/input/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/input/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/input.cssthe optional stylesheet