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.
FigmaDescription
Presents a grid of radio buttons where each row is a distinct statement and each column is a shared response option, allowing participants to rate multiple related items using one consistent scale.
Survey builder and published survey pages in both Open Point and Social Point. Appears within consultation questionnaires, community feedback forms, and stakeholder satisfaction surveys.
Government engagement surveys frequently ask participants to rate several related statements on the same scale (e.g. Likert). Repeating individual radio groups for each statement wastes space and creates visual noise. The matrix consolidates these into a scannable, compact grid that reduces cognitive load and survey fatigue.
Anatomy
| Part | Required? | 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
Likert scale (5-point)
Standard five-option agreement scale (Strongly disagree to Strongly agree)
Measuring participant agreement or satisfaction across a set of related statements. Most common in consultation and stakeholder surveys.
Frequency scale
Options describe how often something occurs (Never, Rarely, Sometimes, Often, Always)
Assessing how frequently stakeholders or community members experience something, such as barriers to participation or service quality.
Quality scale
Options describe perceived quality (Very poor, Poor, Fair, Good, Excellent)
Rating the quality of consultation elements, such as information provided, accessibility of materials, or responsiveness of the agency.
Binary (Yes / No)
Two-option scale for straightforward factual or opinion checks
Use sparingly. Appropriate when nuance is genuinely unnecessary and the survey is short. Avoid when participant views are likely to be mixed.
Scrollable (mobile)
Horizontal scroll applied to the column header row and radio cells on narrow viewports
Automatically applied when the matrix exceeds the available viewport width. Always pair with visible scroll affordance (shadow or fade on the right edge).
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
When to use
- You have three or more related statements that all use the same response scale.
- Survey length and participant time are a concern and consolidation aids completion rates.
- The response scale has four to seven options — enough to warrant a shared header row.
- The statements are genuinely parallel in structure so the same scale applies meaningfully to each.
When not to use
- You have only one or two statements — use individual radio groups instead to avoid the overhead of a full table.
- Each statement requires a different scale — the matrix assumes a single shared scale across all rows.
- The survey is delivered on a platform where table rendering is unreliable or screen reader support is poor — use stacked radio groups as a fallback.
- Statements are long, multi-sentence items — the row label column becomes too wide and the layout breaks at smaller viewports.
- You need to allow multiple selections per row — use a checkbox matrix or a separate component.
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
| Part | Token | Value |
|---|---|---|
| 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
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
- 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
- Use
for column headers and 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
Key Action 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 attribute When to use Example scope="col"Applied to all elements in the column header row <th scope="col">Agree</th>scope="row"Applied to all 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-describedbyPoints 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-labelApplied 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
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
- 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.