Input
Input is one styled native control. It forwards native attributes, supports framework-native value binding, exposes its element for explicit focus recovery, and merges caller Tailwind classes last.
Klean deliberately does not supply Field, Label, description, or error components. The browser's form model is the convention, so the application writes the real <label>, messages, IDs, and ARIA relationships where they remain visible.
Installation
One command installs one framework-native source file. There is no initializer, configuration file, alias prompt, context provider, or Klean runtime.
Run one command from a Boring Stack application. Klean detects the framework and conventional destination, then adds the framework-native source and its direct dependencies.
npx klean-ui add input- No initializer or configuration file
- No framework, alias, or theme questions
- No Klean runtime dependency
Native form recipe
<script setup>
import Input from '@/components/ui/input/Input.vue'
</script>
<template>
<div class="grid gap-2">
<label for="email">Email address</label>
<Input
id="email"
v-model="form.email"
name="email"
type="email"
autocomplete="email"
required
:aria-invalid="Boolean(form.errors.email)"
aria-describedby="email-help email-error"
/>
<p id="email-help">We only use this for account messages.</p>
<p id="email-error" class="empty:hidden text-sm text-red-700">
{{ form.errors.email }}
</p>
</div>
</template>
The application owns the visible label, deterministic IDs, help and error elements, validation timing, and submitted value. Help and error nodes keep stable IDs, so aria-describedby never needs conditional string building. aria-invalid="false" is valid, and empty:hidden collapses an empty error. When an error appears, the existing relationship becomes useful automatically.
This explicit repetition is smaller and clearer than a Field configuration language or accessibility helper. Extract an application-owned form composition only when your product repeats the same complete markup and policy.
API
Input accepts native input attributes, framework-native value binding, and caller classes. It has no variant, size, tone, label, description, error, or validation props.
The default type is text. Native name, required, disabled, autocomplete, aria-invalid, and aria-describedby pass through unchanged.
Styling
The neutral defaults are monochrome, touch-safe, dark-mode aware, and visibly focusable. The 16px text default avoids mobile browser zoom. Caller Tailwind wins:
<Input class="min-h-9 rounded-none border-2 py-1 text-sm shadow-none" />If that dense treatment is a recurring product concept, create an application-owned DenseInput.vue; do not turn it into a Klean size prop.
Accessibility contract
- Every input needs a visible associated label unless the application has a justified accessible-name alternative.
- Help and error text connect through
aria-describedby. - Invalid state uses
aria-invalid; color is never the only signal. - Stable empty errors remain unannounced until they contain useful text.
- Native required and disabled behavior stays native.
- Focus remains visible in light, dark, and high-contrast contexts.
- Validation waits for blur or submission instead of punishing untouched input.
- A failed submission that needs announcement uses one application-owned error summary and focus recovery, not
role="alert"on every inline error.
Related components
- Textarea — growing multi-line input.
- Select — one persistent value from a known fixed list.
- Date Picker — one date-only
YYYY-MM-DDvalue with Calendar. - Schedule Picker — date, time, and IANA timezone stored as an exact ISO instant.
- Button — native form submission and actions.