Form Row
Wraps a field, or a group of fields, in a consistent label, helper, and error layout. The label names what it wraps.
When to Use
columns span in multi-column form grids.
for that control, adding an id if the control has none, and points the control's aria-describedby at the helper or error. With several controls, the row is a role="group" named by its label. Before this, the label carried no for and a field labelled only here had no accessible name.
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.
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.']
) ?>Around content that helpers print
ws_form_row_start and ws_form_row_end take content written with helpers that print rather than return, such as a stack of switches, with no output buffering of your own. The end prints the row, so the label binding is the same as ws_form_row's: here two switches make the row a group named by its label.
Each one changes as soon as you switch it.
<?php ws_form_row_start(['label' => 'Email me about', 'helper' => 'Each one changes as soon as you switch it.']) ?>
<?php ws_stack_start(['gap' => '3']) ?>
<?= ws_toggle('reminders', ['label' => 'Session reminders']) ?>
<?= ws_toggle('digest', ['label' => 'Weekly digest']) ?>
<?php ws_stack_end() ?>
<?php ws_form_row_end() ?>States
Password must be at least 8 characters.
| State | Behavior |
|---|---|
| Default | Label, field, and optional helper stacked with --space-1 / --space-4 rhythm. |
| Required and optional | required sets aria-required="true" on a single control that has neither required nor aria-required, and draws no asterisk: Workshopr defaults to required (DESIGN.md §11.3). optional appends "(optional)" to the label. |
| Error | A non-empty error adds .ws-form-row--error and replaces the helper text with the message, which carries role="alert". A single control gets aria-invalid="true" and the error in its aria-describedby. |
| 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. Bound to a single control with for, or names the row as a group. |
required | bool | false | Asterisk, plus aria-required on a single control. Set required on the field itself when the browser should enforce it. |
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 | One control inside (hidden inputs aside): the label gets for its id, and the row adds an id when the control has none. Several controls, or none: the row is role="group" with aria-labelledby on its label, so a screen reader announces the group's name as focus enters it. |
| Error association | The helper or error gets an id, and a single control lists it in aria-describedby, after any description the field already had (an Input hint stays). A group points its own aria-describedby at it. The error carries role="alert", so it is announced when it appears. |
| Required indicator | The asterisk is aria-hidden; the requirement reaches assistive technology as aria-required on a single control. |
| Keyboard | The row adds nothing focusable. Clicking the label focuses a single control. |
| Contrast | Label uses --text-default, helper --text-muted, and error --color-error, all tested. |
What to do instead
label, hint or helper, and error. Use the row's label for what has no label option: a group of related controls, a custom widget, a pair of fields under one heading.
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, bound to a single control or naming the group |
.ws-form-row__optional | The "(optional)" marker in the label |
.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) |