Input
A labeled text field with its hint above and its error below, both wired with aria-describedby.
Code
The three render the same class names, the same ARIA and the same data attributes. That is checked when this site builds: a class in any of these samples that neither the markup contract nor the shipped stylesheet defines fails the build.
<div class="wsu-field">
<label class="wsu-field__label" for="course">Course number
<span class="wsu-field__required">(required)</span></label>
<p class="wsu-field__hint" id="course-hint">Subject and catalog number, for example CSC 2110.</p>
<input class="wsu-input" id="course" name="course" required
autocomplete="off" aria-describedby="course-hint">
</div>
<!-- With an error -->
<div class="wsu-field">
<label class="wsu-field__label" for="reason">Reason</label>
<textarea class="wsu-textarea" id="reason" name="reason"
aria-invalid="true" aria-describedby="reason-error"></textarea>
<p class="wsu-field__error" id="reason-error">
<svg class="wsu-icon" aria-hidden="true"><use href="/assets/wayne-ui/icons.svg#wsu-circle-alert"/></svg>
Tell us why you need this override.
</p>
</div>{{-- The error is read from the validation error bag automatically --}}
<x-wsu::form.input
name="course"
label="Course number"
hint="Subject and catalog number, for example CSC 2110."
autocomplete="off"
required />
<x-wsu::form.textarea name="reason" label="Reason" required /><Input
v-model="form.course"
name="course"
label="Course number"
hint="Subject and catalog number, for example CSC 2110."
autocomplete="off"
required
/>
<!-- Inertia errors are picked up from provideFormErrors, or passed directly -->
<Textarea v-model="form.reason" name="reason" label="Reason" :error="form.errors.reason" required />Why it works this way
- The hint sits above the control on every path, so it is read before the user starts typing rather than discovered afterwards. Blade and raw PHP rendered it after the control until 2.0.6, because they emitted the hint and the error together from one partial; the partial was split and the order now matches Vue exactly, with a test on each path pinning it. Checkbox is the one deliberate exception everywhere: its label already wraps the toggle, so its hint follows the control.
- The error is text, has an icon, and is referenced by aria-describedby. Never the red border alone.
- autocomplete is left to the caller and not defaulted. WCAG 1.3.5 wants the right token for fields that collect information about the user, and there is no token the component could guess that would be right more often than it was wrong.
- A placeholder is not a label. It disappears on focus, it has lower contrast, and it is gone by the time someone checks what they typed.
Accessibility
- A real label with for pointing at the control (1.3.1, 3.3.2).
- aria-invalid and aria-describedby on error (3.3.1).
- (required) in the label text, not an asterisk that only sighted users can decode.
Success criteria in brackets refer to WCAG 2.2. Every one of them is checked by axe-core in both themes at three viewport widths before this page can be published. See Accessibility for what the gates are.
Props
Blade
| Attribute | Default | What it does |
|---|---|---|
name | required | Also the default id, and the key looked up in the error bag. |
label | null | Rendered as a real label. |
type | 'text' | Any input type. |
value | old input | Falls back to old() on a failed submit, so a redisplay does not lose what was typed. |
hint | null | Above the control, wired with aria-describedby. |
error | the error bag | Pass a string to override. |
required | false | Adds the required attribute and the "(required)" text. |
id | a slug of name | Set it to change the id without changing name. |
bag | 'default' | Named error bag. |
Vue
| Prop | Type and default | What it does |
|---|---|---|
modelValue | String or Number, '' | v-model. |
label / hint / error | String, '' | error sets aria-invalid and renders the message. |
type | String, 'text' | Any input type. |
placeholder | String, '' | Shown in the field. Not a substitute for label (1.3.1, 3.3.2). |
autocomplete | String, undefined | Deliberately not defaulted (1.3.5). |
required / disabled | Boolean, false | Reflected on the control. |
In the markup contract
contract/markup-contract.json is extracted from the two static templates and every package tests against it. This is what it records for .wsu-field.
| Template | Elements | Accessibility attributes | Uses |
|---|---|---|---|
| internal apps/examples/static-html/index.html | divfieldset | tabindex | 5 |
| public apps/examples/static-html/public-app.html | div | none recorded | 2 |