Skip to main content

Matrix

A survey question type presenting multiple statements as rows with a shared set of radio button response options across columns. Use in engagement surveys where participants rate several related statements using the same scale.

OverviewStyleAccessibility

Description

Anatomy

PartRequired?Notes
Group label Required The overarching question or instruction that applies to all rows. Rendered as a legend or heading above the table. Example: 'Please rate your agreement with the following statements about this consultation process.'
Column headers Required The shared response scale options (e.g. Strongly disagree, Disagree, Neutral, Agree, Strongly agree). Displayed as column headers above the radio inputs.
Row label Required The individual statement or sub-question for each row. Left-aligned. Should be concise — one sentence maximum.
Radio input cell Required A single radio button within the grid cell. Each cell corresponds to the intersection of a row statement and a column response option. Only one cell per row may be selected.
Row divider Recommended A subtle horizontal rule between rows to aid scanability, especially in longer matrices.
Required indicator Optional An asterisk or 'Required' label appended to the group label when all rows must be answered before submission.
Error message Optional Inline validation message shown beneath an unanswered row when the field is required. Uses --op-color-status-error.
Helper text Optional Secondary instruction below the group label. Use to clarify the scale (e.g. '1 = Not at all satisfied, 5 = Extremely satisfied') or to set expectations about how many rows must be answered.

Variants

States

State Behaviour
Default All rows unselected. Column headers and row labels are visible. No validation state applied.
Selected The chosen radio cell shows a filled radio indicator using --op-color-interactive-default. The row may receive a subtle background highlight to confirm selection.
Focused The focused radio input receives a 3px focus ring in --op-color-interactive-focus. Focus moves cell by cell across columns with Arrow keys, and row by row with Tab.
Error (row-level) A required row that has not been answered after attempted submission shows an error border (--op-color-status-error) on the row label and an inline error message below. Other completed rows are unaffected.
Disabled (row) An individual row is grayed out and inputs are non-interactive. Used when a row is conditionally inapplicable. Row label uses --op-color-text-disabled.
Disabled (full matrix) All inputs are non-interactive. Used in read-only summary views or submitted survey confirmation pages.
Loading / skeleton A skeleton placeholder grid replaces the matrix while survey content loads. Row and column counts match the expected matrix dimensions to prevent layout shift.

Usage guidelines

Do / Don't

Do

Keep row labels to one concise sentence so they fit the constrained row label column without wrapping excessively.

Don't

Do not write paragraph-length row labels. Truncation or extreme wrapping destroys the grid alignment and makes the matrix harder to scan.

Do

Label column headers with the full text of each scale option, even if abbreviated on small screens.

Don't

Do not use numbers alone (1, 2, 3) as column headers without a visible scale legend — participants cannot infer meaning, which invalidates survey data.

Do

Provide a 'Not applicable' or 'No opinion' option in the column headers when participants may genuinely have no basis to respond.

Don't

Do not force a response on rows that may not apply to all participants — this introduces noise into survey results and frustrates respondents.

Do

Test the matrix at 320px viewport width and verify the horizontal scroll affordance is visible.

Don't

Do not assume the matrix will reflow gracefully on mobile without explicit responsive handling — default table layout clips silently.

Do

Group related matrices together under a section heading so participants understand the thematic context.

Don't

Do not mix matrices with very different scales on the same screen without clear visual separation — participants may carry over the scale from the previous matrix.

Layout & Spacing

Group label margin-bottom: --op-space-16 (16px) Helper text margin-bottom: --op-space-12 (12px) Table outer border-radius: --op-radius-md (8px) Column header cell padding: --op-space-8 (8px) vertical, --op-space-12 (12px) horizontal Row label cell padding: --op-space-12 (12px) vertical, --op-space-16 (16px) horizontal Radio cell padding: --op-space-12 (12px) all sides (centres the 44x44px touch target) Row divider: 1px solid --op-color-border-default Minimum row label column width: 200px Minimum radio cell width: 80px (to accommodate 44px touch target + padding) Error message margin-top: --op-space-4 (4px)

Tokens

PartTokenValue
Group label text --op-color-text-primary Full-weight heading or legend text
Helper text --op-color-text-secondary Subdued secondary instruction
Column header background --op-color-bg-subtle Light tint to distinguish headers from data rows
Column header text --op-color-text-primary Column labels must meet 4.5:1 contrast on --op-color-bg-subtle
Row label text --op-color-text-primary
Row divider --op-color-border-default
Radio indicator (selected) --op-color-interactive-default Green-400 fill for selected state
Radio indicator border (unselected) --op-color-border-default
Focus ring --op-color-interactive-focus 3px solid outline, 2px offset
Selected row highlight --op-color-interactive-default at 8% opacity Subtle row tint on selection to aid visual confirmation
Error border --op-color-status-error Applied to the row label cell left border (4px) on validation error
Error message text --op-color-status-error
Disabled row text --op-color-text-disabled
Disabled radio --op-color-border-default at reduced opacity Do not use interactive green for disabled state

Engineering notes

  • Render the matrix as a native HTML <table> element. Do not use CSS grid or flexbox to simulate a table — native table semantics are required for screen reader row/column association.
  • Wrap the entire matrix in a <fieldset> with a <legend> for the group label. This is required for ARIA semantics and is the correct HTML pattern for grouped form controls.
  • Each radio input must have a unique id. The name attribute must be shared across all inputs in the same row (not across the whole matrix) so only one selection per row is enforced by the browser.
  • Each radio input must have a visually hidden <label> that combines the row label and column header text, e.g. 'Information provided — Agree'. The visible row and column labels are not sufficient for assistive technology without this association.
  • Use <th scope='col'> for column headers and <th scope='row'> for row labels. This is mandatory for screen reader navigation of table relationships.
  • On mobile viewports (below 640px), apply overflow-x: auto to a wrapper div around the table and add role='region' with aria-label='[question text] — scroll to see all options' to announce the scroll region to screen readers.
  • For the error state, add aria-describedby on each unanswered row's radio inputs pointing to the error message id. Also set aria-invalid='true' on those inputs.
  • Respect prefers-reduced-motion — the selected row highlight transition should use a zero duration when this media query is active.
  • If implementing with Web Awesome, note that wa-radio-group does not natively render a matrix layout. The matrix must be implemented in plain HTML with custom CSS. wa-radio can be used as the individual input if its internal focus and checked states are compatible with the table cell layout — verify this in the target browser set.

Keyboard interaction

KeyAction
Tab Moves focus to the first unselected or selected radio input in the matrix. Within a row, Tab moves to the next row's first radio. Arrow keys handle movement within a row.
Arrow Right / Arrow Down Moves focus and selection to the next radio option within the current row.
Arrow Left / Arrow Up Moves focus and selection to the previous radio option within the current row.
Tab (from last column) Moves focus to the first radio in the next row, or out of the matrix if on the last row.
Shift + Tab Moves focus to the previous row's radio group or out of the matrix to the preceding focusable element.
Space Selects the currently focused radio input (standard radio behaviour).

Why it matters

Government consultation surveys are frequently accessed by participants using screen readers or keyboard-only navigation, including people with motor impairments or visual disabilities. A matrix rendered without proper table semantics will not communicate row and column relationships to assistive technology, making the survey effectively inaccessible and potentially unlawful under the Disability Discrimination Act and relevant state equivalents. The WCAG 2.1 AA baseline is a legal floor, not a target.

Focus

Focus is indicated by a 3px solid outline in --op-color-interactive-focus with a 2px offset, applied to the radio input element directly. The focus ring must be fully visible and must not be clipped by the table cell boundary — ensure overflow: visible on td elements. When an error state is active, focus is programmatically moved to the first row with an error after the form submission attempt, allowing screen reader users to immediately encounter the error message.

ARIA

Role or attributeWhen to useExample
scope="col" Applied to all <th> elements in the column header row <th scope="col">Agree</th>
scope="row" Applied to all <th> elements in the row label column <th scope="row">The consultation materials were easy to understand.</th>
aria-required="true" Applied to radio inputs in rows where a response is mandatory <input type="radio" aria-required="true" />
aria-invalid="true" Applied to radio inputs in a row that has failed validation <input type="radio" aria-invalid="true" aria-describedby="error-row-2" />
aria-describedby Points radio inputs to their row-level error message element id <input type="radio" aria-describedby="error-row-2" />
aria-label (visually hidden) Each radio input label must combine row and column text for screen reader users who do not receive table context from all readers <label class="visually-hidden" for="q1-r2-agree">Information provided — Agree</label>
role="region" + aria-label Applied to the scrollable wrapper on mobile to announce the horizontal scroll region <div role="region" aria-label="Consultation experience — scroll to see all options" style="overflow-x:auto;">

Contrast

Column header text (--op-color-text-primary) on column header background (--op-color-bg-subtle): must meet 4.5:1. Verify this token pairing in the current theme before shipping. Row label text (--op-color-text-primary) on row background (--op-color-bg-primary): must meet 4.5:1. Error message text (--op-color-status-error) on background: must meet 4.5:1. Do not rely on the red colour alone to indicate error — always include error text. Disabled row text (--op-color-text-disabled): intentionally below 4.5:1. Disabled controls are exempt from contrast requirements under WCAG 2.1, but ensure the reason for disabling is communicated through another mechanism (helper text or conditional logic explanation).

Touch targets

Each radio input cell must provide a minimum 44x44px touch target. Achieve this by applying min-width: 44px and min-height: 44px to the <td> element and centering the radio input within it. Do not rely on the radio input's intrinsic size alone — native radio inputs are typically 16–20px. On iOS and Android, the touch target must not be obscured by the table border or adjacent cell padding.

Things to avoid

  • Do not use a CSS grid or div-based layout to visually simulate a matrix — native table semantics are required.
  • Do not omit the <fieldset> and <legend> wrapper — without it, the group label is not programmatically associated with the radio inputs.
  • Do not place the error message outside the table in a way that breaks the aria-describedby association — keep error messages adjacent to the relevant row.
  • Do not animate row highlight transitions without respecting prefers-reduced-motion.
  • Do not use placeholder text or tooltip-only column header abbreviations as the sole label — they are not reliably accessible.
  • Do not disable the entire matrix when only some rows are inapplicable — disable individual rows and explain why.

Was this page helpful?

Updated 5 October 2026