PATTERN NAME: Choice Matrix
COMPOSES: .table | .form-check / .form-check-input / .form-check-label | .form-label (on the legend) | .form-field
Several questions that share one answer scale — a school's yes/no enquiry questions. A matrix on
a laptop, one labelled question per row on a phone, from one markup: the table is
reflowed below sm, never duplicated. Narrow this window past 640px and watch a row
you have already answered keep its answer.
Default: the whole set is a <fieldset> with a
<legend class="form-label"> — never a bare <div> with a
heading. Each row is its own radio group: one name per row, never
one name across the table, or answering the second question would unanswer the first.
<fieldset class="form-field"> <legend class="form-label form-label-optional">Please answer each one</legend> <table class="table choice-matrix"> <thead><tr><th><span class="tw:sr-only">Question</span></th><th class="choice-matrix-col" id="cm-yes">Yes</th>…</tr></thead> <tbody><tr> <td class="choice-matrix-question" id="cm-q1">Would you like a tour of the school</td> <td class="choice-matrix-col"><div class="form-check"> <input class="form-check-input" type="radio" name="cm-tour" id="cm-tour-yes" aria-labelledby="cm-q1 cm-yes"> <label class="form-check-label" for="cm-tour-yes">Yes</label> </div></td>…
Two ids, one name: every radio carries
aria-labelledby="<question cell id> <column header id>", so it announces as
"Would you like a tour of the school, Yes" — identical at both widths, because the name
never comes from the in-cell label. That label exists so the answer is tappable on a phone; above
sm the column header is what the eye reads and the label is out of the document, so a
screen reader in table mode does not hear "Yes" twice.
Not only yes/no: any shared scale works — the columns are whatever the school
asked. Column width is --choice-matrix-col (5.5rem); override it on the table when a
scale needs wider headings.
<table class="table choice-matrix"> — one <th class="choice-matrix-col"> per answer, one <td class="choice-matrix-col"> per answer per row
State lives once, on the wrapper: .form-field.is-invalid tints every
control in the set and reveals the .invalid-feedback. The message names what to do,
not what went wrong with the machine.
<fieldset class="form-field is-invalid"> … <p class="invalid-feedback" id="cmi-error">