Canvas Design System
Main Site Tokens

Input

Collects a value from the user, with the label, hint, and error wiring already correct.

When to Use

Use when: You need any free-text or numeric field in a form. It covers text, email, password, number, tel, url, search, textarea, and select through one call, so the label and validation markup stay consistent.
Don't use when: The choice is boolean — use Toggle. For a richer dropdown use Select, and for multi-value entry use Chip Input.
warning Corrected July 2026. This page previously demonstrated 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'],
    ],
]) ?>
warning Options shape differs from Select. 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.

StateBehavior
DefaultBordered field using the --input-* token family.
HoverBorder shifts to --input-border-hover.
FocusBorder becomes --input-border-focus with a --input-ring halo. This is the sanctioned non-red focus accent — don't remove it.
ErrorTriggered by a non-empty error. Adds .ws-input--error and .ws-field--error, sets aria-invalid="true", and renders the message with role="alert".
HintRendered only when there's no error — the error replaces the hint rather than stacking below it.
DisabledNative disabled: greyed, unfocusable, and not submitted with the form.
Read-onlyNative 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.

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

OptionTypeDefaultPurpose
$namestring''Positional. The field name, and the default id.
typestring'text'text | email | password | number | tel | url | search | textarea | select
idstring$nameElement ID, used to bind the label
labelstringnullVisible label. Effectively required — see Accessibility.
placeholderstring''Placeholder text. Never a substitute for a label.
valuestring''Current value
errorstringnullError message. A non-empty value is the error state.
hintstringnullHelp text below the field. Hidden while an error is showing.
requiredboolfalseNative required attribute and the label indicator
disabledboolfalseDisabled and excluded from submission
readonlyboolfalseNot editable, but focusable and submitted
autocompletestringnullAutocomplete token — set it on name, email, and address fields
minlength / maxlengthintnullNative length constraints
patternstringnullNative validation pattern
optionsarray[]select type only. A list of arrays: [['value' => …, 'label' => …, 'disabled' => false]]. Not the same shape as ws_select().
rowsint4textarea type only
showPasswordTogglebooltruepassword type only — the visibility toggle button
sizestring'md'sm | md | lg
classstring''Additional CSS classes
attrsarray[]Extra HTML attributes as key/value pairs
info Not supported: 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

ConcernBehavior
LabellingA 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.
ErrorsThe message carries role="alert", so it's announced when it appears, and the field gets aria-invalid="true" plus aria-describedby pointing at it.
HintsWired 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 toggleA 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.
KeyboardAll native. Disabled fields leave the tab order; read-only fields stay in it.
RequiredSets the native required attribute, so the state is announced rather than relying on a visual asterisk.
AutofillPass autocomplete on personal-data fields. It's an accessibility feature (WCAG 1.3.5), not just convenience.

Tokens

TokenUsed for
--input-bg / --input-text / --input-radiusField surface, text, and corners
--input-border / --input-border-hover / --input-border-focusBorder across resting, hover, and focus
--input-ring / --input-ring-widthFocus halo
--focus-ring-width / --focus-ring-offsetShared focus geometry
--input-placeholder / --gray-400Placeholder text
--gray-50Disabled fill
--color-error / --color-error-lightError border and message
--color-success / --color-success-lightThe unused success treatment
--color-primary / --wpr-phaseAccent, themeable per app
--text-body / --text-ui / --text-metaField, label, and hint type scale
--font-bodyBody face
--transition-fastBorder and ring transitions

CSS Classes

ClassPurpose
.ws-inputThe field element
.ws-input--sm / --lgSize modifiers (md emits nothing)
.ws-input--errorError styling, emitted when error is set
.ws-input--successSuccess styling — defined in CSS, never emitted by the template
.ws-field--errorError class on the wrapper, for styling the label and message together
.ws-input-wrapWrapper for a field with adornments
.ws-input-wrap--has-icon-left / --has-icon-rightPadding adjustments for icons — orphaned, since no template emits them
.ws-input-wrap__icon / --left / --rightIcon positioning — also orphaned

Files

FilePurpose
includes/components/helpers.phpws_input() helper function
includes/components/input.phpTemplate — type branching, ARIA wiring, password toggle
includes/components/components.cssStyles (.ws-input rules)