Input
Collects a value from the user, with the label, hint, and error wiring already correct.
When to Use
icon, iconPosition, state, and helper options. The template reads none of them — code copied from those examples silently did nothing. The real option for help text is hint, and the error state is inferred from error being non-empty. See Options below.
Variants
Basic
The label is bound to the field by id, which defaults to the field name. Always pass a label.
<?= ws_input('workshop_name', [
'label' => 'Workshop name',
'placeholder' => 'Enter workshop name…',
'required' => true,
]) ?>Types
One helper covers nine types. textarea and select render different elements but keep the same label, hint, and error treatment.
<?= ws_input('email', ['type' => 'email', 'label' => 'Email address']) ?>
<?= ws_input('description', ['type' => 'textarea', 'label' => 'Description', 'rows' => 4]) ?>
<!-- NOTE the options shape — list of arrays, not value => label pairs -->
<?= ws_input('category', [
'type' => 'select',
'label' => 'Category',
'placeholder' => 'Select category…',
'options' => [
['value' => 'ideation', 'label' => 'Ideation'],
['value' => 'strategy', 'label' => 'Strategy'],
],
]) ?>ws_input(type: 'select') wants a list of ['value' => …, 'label' => …, 'disabled' => …] arrays. ws_select() wants flat value => label pairs. Passing the flat shape here throws Cannot access offset of type string on string — a fatal, not a silent miss. This page previously shipped that exact bug. When you want a dropdown, prefer ws_select().
Sizes
<?= ws_input('small', ['label' => 'Small', 'size' => 'sm']) ?>States
There is no state option. The error state is derived: pass a non-empty error and the field styles itself, sets aria-invalid, and points aria-describedby at the message.
We only use this for session reminders.
Please enter a valid email address
| State | Behavior |
|---|---|
| Default | Bordered field using the --input-* token family. |
| Hover | Border shifts to --input-border-hover. |
| Focus | Border becomes --input-border-focus with a --input-ring halo. This is the sanctioned non-red focus accent — don't remove it. |
| Error | Triggered by a non-empty error. Adds .ws-input--error and .ws-field--error, sets aria-invalid="true", and renders the message with role="alert". |
| Hint | Rendered only when there's no error — the error replaces the hint rather than stacking below it. |
| Disabled | Native disabled: greyed, unfocusable, and not submitted with the form. |
| Read-only | Native readonly: still focusable and still submitted, unlike disabled. Use it for values the user should see and copy but not change. |
| Success | .ws-input--success exists in CSS but nothing in the template emits it. Apply it yourself through class if you want a verified-field treatment. |
<?= ws_input('email', [
'label' => 'Email address',
'error' => $errors['email'] ?? null, // non-empty ⇒ error state
'hint' => 'We only use this for session reminders.',
]) ?>Real-World Usage
A save-workshop form on redisplay after failed validation: values echoed back, per-field errors from the validation array, and autocomplete hints set so browsers fill sensibly.
Duration must be at least 15 minutes
One sentence on what the group should leave with.
<?= ws_input('title', [
'label' => 'Workshop title',
'value' => $plan['title'] ?? '',
'error' => $errors['title'] ?? null,
'required' => true,
'maxlength' => 120,
]) ?>
<?= ws_input('goal', [
'type' => 'textarea',
'label' => 'Goal',
'value' => $plan['goal'] ?? '',
'error' => $errors['goal'] ?? null,
'hint' => 'One sentence on what the group should leave with.',
'rows' => 3,
]) ?>Options
| Option | Type | Default | Purpose |
|---|---|---|---|
$name | string | '' | Positional. The field name, and the default id. |
type | string | 'text' | text | email | password | number | tel | url | search | textarea | select |
id | string | $name | Element ID, used to bind the label |
label | string | null | Visible label. Effectively required — see Accessibility. |
placeholder | string | '' | Placeholder text. Never a substitute for a label. |
value | string | '' | Current value |
error | string | null | Error message. A non-empty value is the error state. |
hint | string | null | Help text below the field. Hidden while an error is showing. |
required | bool | false | Native required attribute and the label indicator |
disabled | bool | false | Disabled and excluded from submission |
readonly | bool | false | Not editable, but focusable and submitted |
autocomplete | string | null | Autocomplete token — set it on name, email, and address fields |
minlength / maxlength | int | null | Native length constraints |
pattern | string | null | Native validation pattern |
options | array | [] | select type only. A list of arrays: [['value' => …, 'label' => …, 'disabled' => false]]. Not the same shape as ws_select(). |
rows | int | 4 | textarea type only |
showPasswordToggle | bool | true | password type only — the visibility toggle button |
size | string | 'md' | sm | md | lg |
class | string | '' | Additional CSS classes |
attrs | array | [] | Extra HTML attributes as key/value pairs |
icon, iconPosition, state, helper. The CSS carries .ws-input-wrap__icon--left/right rules, but no template emits them — leading icons would need markup you write yourself, or a template change.
Accessibility
| Concern | Behavior |
|---|---|
| Labelling | A real <label for> bound to the field's id. Omitting label leaves the field unnamed — a placeholder does not substitute, since it vanishes on typing. |
| Errors | The message carries role="alert", so it's announced when it appears, and the field gets aria-invalid="true" plus aria-describedby pointing at it. |
| Hints | Wired through aria-describedby to the hint's ID — but only when there's no error. Error takes the slot; the two never both describe the field. |
| Password toggle | A real button labelled aria-label="Toggle password visibility", with both eye icons marked aria-hidden. It swaps the input's type via an inline handler. |
| Keyboard | All native. Disabled fields leave the tab order; read-only fields stay in it. |
| Required | Sets the native required attribute, so the state is announced rather than relying on a visual asterisk. |
| Autofill | Pass autocomplete on personal-data fields. It's an accessibility feature (WCAG 1.3.5), not just convenience. |
Tokens
| Token | Used for |
|---|---|
--input-bg / --input-text / --input-radius | Field surface, text, and corners |
--input-border / --input-border-hover / --input-border-focus | Border across resting, hover, and focus |
--input-ring / --input-ring-width | Focus halo |
--focus-ring-width / --focus-ring-offset | Shared focus geometry |
--input-placeholder / --gray-400 | Placeholder text |
--gray-50 | Disabled fill |
--color-error / --color-error-light | Error border and message |
--color-success / --color-success-light | The unused success treatment |
--color-primary / --wpr-phase | Accent, themeable per app |
--text-body / --text-ui / --text-meta | Field, label, and hint type scale |
--font-body | Body face |
--transition-fast | Border and ring transitions |
CSS Classes
| Class | Purpose |
|---|---|
.ws-input | The field element |
.ws-input--sm / --lg | Size modifiers (md emits nothing) |
.ws-input--error | Error styling, emitted when error is set |
.ws-input--success | Success styling — defined in CSS, never emitted by the template |
.ws-field--error | Error class on the wrapper, for styling the label and message together |
.ws-input-wrap | Wrapper for a field with adornments |
.ws-input-wrap--has-icon-left / --has-icon-right | Padding adjustments for icons — orphaned, since no template emits them |
.ws-input-wrap__icon / --left / --right | Icon positioning — also orphaned |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_input() helper function |
includes/components/input.php | Template — type branching, ARIA wiring, password toggle |
includes/components/components.css | Styles (.ws-input rules) |