Form Row
Wraps a field in a consistent label, helper, and error layout — useful for grouping, but read the accessibility note first.
When to Use
Use when:
You need to label something that has no label option of its own — a group of related controls, a custom widget, a pair of fields under one heading. Also useful for the
columns span in multi-column form grids.
warning
Accessibility limitation. The label this component renders carries no
for attribute and doesn't wrap the field, so it is not programmatically associated with the control inside it. Sighted users see a label; screen-reader users get an unnamed field. Pass the label to ws_input() / ws_select() instead, and use Form Row only for layout — see Accessibility below for the details and workarounds.
Variants
Stacked (default)
Label above, field below, helper underneath. The standard vertical form rhythm.
We'll never share your email.
<?= ws_form_row(
ws_input('email', ['type' => 'email', 'placeholder' => 'you@example.com']),
[
'label' => 'Email address',
'required' => true,
'helper' => "We'll never share your email.",
]
) ?>Inline
Label beside the field. Use it in settings panels where a column of labels aids scanning; avoid it on narrow layouts where it forces the field too small.
<?= ws_form_row(ws_input('name'), ['label' => 'Full name', 'inline' => true]) ?>Wrapping a select
Any field HTML can be the content — the row doesn't care what it wraps.
expand_more
Select your role in the workshop.
<?= ws_form_row(
ws_select('role', $roles, ['placeholder' => 'Choose a role…']),
['label' => 'Role', 'helper' => 'Select your role in the workshop.']
) ?>States
Password must be at least 8 characters.
| State | Behavior |
|---|---|
| Default | Label, field, and optional helper stacked with --space-1 / --space-4 rhythm. |
| Required | Appends .ws-form-row__required — a red asterisk after the label. Purely visual; it doesn't set the field's required attribute, so set that on the field too. |
| Error | A non-empty error adds .ws-form-row--error and replaces the helper text with the message. The row's error is not wired to the field with aria-describedby — pass the error to the field itself as well. |
| Helper | Shown below the field, hidden while an error is present. |
| Inline | .ws-form-row--inline switches to a horizontal label/field arrangement. |
| Columns | columns emits an inline grid-column: span N, letting one row span multiple columns of a grid form. |
Real-World Usage
A two-column form where the email row spans both columns. Note the labels live on the fields, not on the rows — the rows are doing layout only, which is the pattern to copy.
<form style="display: grid; grid-template-columns: 1fr 1fr; gap: 20px;">
<!-- Label on the field, so it is properly bound -->
<?= ws_form_row(ws_input('first', ['label' => 'First name']), []) ?>
<?= ws_form_row(ws_input('last', ['label' => 'Last name']), []) ?>
<!-- Row spans both columns -->
<?= ws_form_row(
ws_input('email', ['type' => 'email', 'label' => 'Email']),
['columns' => 2]
) ?>
</form>Options
| Option | Type | Default | Purpose |
|---|---|---|---|
$content | string | — | Positional. The field HTML, typically ws_input() or ws_select() output. Rendered raw. |
label | string | '' | Row label. Not bound to the field — see Accessibility. |
required | bool | false | Visual asterisk only. Doesn't set the field's required attribute. |
helper | string | '' | Help text below the field. Note: helper, matching Select rather than Input's hint. |
error | string | '' | Error message. Replaces the helper and adds the error class. |
inline | bool | false | Horizontal label and field |
columns | int | null | Grid column span, as an inline style |
id | string | '' | ID on the row wrapper, not the field |
class | string | '' | Additional CSS classes |
Accessibility
| Concern | Behavior |
|---|---|
| Label binding | Broken as shipped. The template emits <label class="ws-form-row__label"> with no for, and the field sits in a sibling .ws-form-row__field rather than inside the label. Nothing connects the two, so the control has no accessible name — a WCAG 1.3.1 and 3.3.2 failure whenever this row supplies the only label. |
| Error association | Same problem. The error text is not referenced by aria-describedby and carries no role="alert", so it is neither announced when it appears nor tied to the field. |
| Required indicator | The asterisk is decoration. Without required on the field itself, the requirement isn't conveyed at all. |
| Keyboard | The row adds nothing focusable. Whatever it wraps behaves normally. Because the label isn't bound, clicking it does not focus the field — a usability loss for everyone, not just assistive-tech users. |
| Contrast | Label uses --text-default, helper --text-muted, and error --color-error, all tested. |
What to do instead
Do:
Put
label, error, and required on the field, where they're correctly bound, and use Form Row for spacing and columns only. If you must label at the row level, pass a matching attrs => ['aria-label' => …] to the field so it still has a name.
Don't:
Don't set the label on the row and leave the field's own label empty. That's the combination that produces an unnamed control — and it looks completely correct on screen, so it survives visual review.
info
The fix in the template is small — accept an
htmlFor option and emit for on the label — but it's a component change, not a documentation one, so it's recorded here rather than applied.
Tokens
| Token | Used for |
|---|---|
--text-default | Label colour |
--text-muted | Helper text |
--color-error | Error text and the required asterisk |
--text-small / --text-caption | Label and helper type scale |
--font-medium | Label weight |
--space-1 / --space-4 | Label-to-field and field-to-helper spacing |
CSS Classes
| Class | Purpose |
|---|---|
.ws-form-row | Row wrapper |
.ws-form-row--inline | Horizontal label and field |
.ws-form-row--error | Error state on the row |
.ws-form-row__label | Label element (unbound — see Accessibility) |
.ws-form-row__required | Required asterisk |
.ws-form-row__field | Field container |
.ws-form-row__helper | Helper text |
.ws-form-row__error | Error text |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_form_row() helper function |
includes/components/form-row.php | Template — label, field slot, helper/error branch |
includes/components/components.css | Styles (.ws-form-row rules) |