Canvas Design System
Main Site Tokens

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.
Don't use when: The field already accepts a label — which Input and Select both do. Their labels are properly bound to the field; this component's is not. Prefer the field's own label whenever one exists.
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.

StateBehavior
DefaultLabel, field, and optional helper stacked with --space-1 / --space-4 rhythm.
RequiredAppends .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.
ErrorA 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.
HelperShown below the field, hidden while an error is present.
Inline.ws-form-row--inline switches to a horizontal label/field arrangement.
Columnscolumns 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

OptionTypeDefaultPurpose
$contentstringPositional. The field HTML, typically ws_input() or ws_select() output. Rendered raw.
labelstring''Row label. Not bound to the field — see Accessibility.
requiredboolfalseVisual asterisk only. Doesn't set the field's required attribute.
helperstring''Help text below the field. Note: helper, matching Select rather than Input's hint.
errorstring''Error message. Replaces the helper and adds the error class.
inlineboolfalseHorizontal label and field
columnsintnullGrid column span, as an inline style
idstring''ID on the row wrapper, not the field
classstring''Additional CSS classes

Accessibility

ConcernBehavior
Label bindingBroken 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 associationSame 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 indicatorThe asterisk is decoration. Without required on the field itself, the requirement isn't conveyed at all.
KeyboardThe 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.
ContrastLabel 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

TokenUsed for
--text-defaultLabel colour
--text-mutedHelper text
--color-errorError text and the required asterisk
--text-small / --text-captionLabel and helper type scale
--font-mediumLabel weight
--space-1 / --space-4Label-to-field and field-to-helper spacing

CSS Classes

ClassPurpose
.ws-form-rowRow wrapper
.ws-form-row--inlineHorizontal label and field
.ws-form-row--errorError state on the row
.ws-form-row__labelLabel element (unbound — see Accessibility)
.ws-form-row__requiredRequired asterisk
.ws-form-row__fieldField container
.ws-form-row__helperHelper text
.ws-form-row__errorError text

Files

FilePurpose
includes/components/helpers.phpws_form_row() helper function
includes/components/form-row.phpTemplate — label, field slot, helper/error branch
includes/components/components.cssStyles (.ws-form-row rules)