EmTextField
A single-line text input. Label, help text, and error messages are props, and the field wires them to the control.
Usage
Everything around the input is a prop. label renders the visible label and wires it to the control, description is the help text below it, and errorMessages is the error region that appears alongside it — the two coexist (aria-describedby + aria-errormessage), they do not replace one another. There are no named slots — this is a shell component with fixed anatomy, which is the convention across every Emerald control whose shape does not vary.
v-model is a string and defaults to ''. It stays a string for every type, including number. The DOM returns a string. Coercing that string is how a form submits NaN.
Anatomy
<script setup lang="ts">
import { EmTextField } from '@paper/emerald'
</script>
<template>
<EmTextField />
</template>Composed on Vuetify0
EmTextField renders v0’s Input compound: Input.Root owns the field, Input.Control is the element you type into, and Input.Description and Input.Error are the two message regions.
Most of what is hard about a text field lives in that compound rather than in Emerald. Input.Root wires each message region to the control by its own attribute and runs the validation pipeline — Vuetify0’s createValidation under the hood, so the rule contract and the validateOn timing are the same ones documented on the Input page.
Emerald generates the field id with useId() when you do not pass one, renders its own <label for="…">, and passes the id to Input.Root. The root uses that id for the control and derives the description and error ids from it. Emerald owns the for/id pair. v0 owns the attribute wiring under it.
Emerald supplies the CSS and one structural decision: Input.Error is always rendered rather than conditionally mounted. That matters for announcements rather than for layout — the region carries aria-live="polite", and a live region has to already be in the document when its content arrives, or the insertion is not announced at all.
The state attributes the stylesheet keys on — data-focused, data-disabled, data-readonly, data-state="invalid" — are all emitted by v0. Emerald writes no state class of its own.
Examples
Props
| Prop | Type | Default | Description |
|---|---|---|---|
v-model | string | '' | The field value. Always a string, whatever the type |
label | string | — | Visible label, associated by for/id |
description | string | — | Help text below the control |
placeholder | string | — | Native placeholder. Not a substitute for label |
type | string | 'text' | Native input type |
autocomplete | string | — | Native autocomplete token |
name | string | — | Form field name |
id | ID | generated | Field id. Falls back to useId() |
required | boolean | false | Sets the required attribute and adds a required rule that fails an empty value when validation runs (default: blur) |
disabled | boolean | false | Field unavailable and not submitted |
readonly | boolean | false | Value shown but not editable; still focusable and submitted |
rules | FormValidationRule[] | — | Validators — (value) => true | string, optionally async |
validateOn | ValidateOn | 'blur' | When rules run: blur, input or submit, each combinable with lazy or eager |
error | boolean | false | Force the invalid state from outside the rule pipeline |
errorMessages | string | string[] | — | Manual messages, merged ahead of rule errors. A non-empty list marks the field invalid even when error is false |
namespace | string | — | Which v0 Input instance to bind to. Only needed when nesting |
FormValidationRule and ValidateOn are v0 types, re-exported from @vuetify/v0. There are no slots.
Accessibility
Input.Root renders a native <input> and owns the relationships between it and everything around it, which is the part hand-rolled fields most often get wrong.
Label association
label renders a real <label for="…"> pointing at the field id — Emerald’s own element, over an id it generates with useId() when you do not supply one. That gives you the two things a wrapping label alone does not: the label text becomes the control’s accessible name, and clicking it moves focus into the field.
A placeholder is not a label. It disappears the moment someone types, it is not reliably announced, and it fails contrast requirements in most designs. Use it for a format hint — you@example.com — and always pass label as well.
Descriptions and errors
The two regions are wired to the control by different attributes, and they coexist rather than replacing one another.
| Region | Attribute on the control | Present when |
|---|---|---|
Input.Description | aria-describedby | A description is set — regardless of validity |
Input.Error | aria-errormessage | The field is invalid and has at least one message |
So a field that is both described and invalid exposes both: the help text stays associated the whole time, and the error arrives on its own channel alongside it. The control also gets aria-invalid while it is failing, which is what flips a screen reader into reporting the error at all — aria-errormessage is ignored without it.
Input.Error additionally carries aria-live="polite", so a message that appears while the reader is already past the field is announced without stealing focus. This is the reason the region is always mounted: a live region has to be in the document before its content changes, or the browser has nothing to observe and the first message is silently missed.
Help text stays visible when validation fails. Put the format requirement in description. The error message does not replace it.
Required
required sets the native attribute, the aria state, and a required rule that fails an empty value when validation runs (default: blur). The field is announced as required on focus. The failure appears when validation runs.
States
| State | Focusable | Submitted | Announced as |
|---|---|---|---|
readonly | Yes | Yes | Read-only |
disabled | No | No | Disabled |
error | Yes | Yes | Invalid, with the message on aria-errormessage |