Column Priority and Hiding on Small Screens
Permalink to "Column Priority and Hiding on Small Screens"Wide tables do not fit phones or zoomed desktop windows. One common responsive strategy is column priority: essential columns always show, less important ones hide as space shrinks. Done carelessly, it hides information with no way to get it back, or hides columns visually but leaves them in the accessibility tree, so screen reader users hear headers that sighted users cannot see. Done well, it keeps essential data visible and makes hidden values reachable in one step.
This guide belongs to responsive data tables.
Spec reference
Permalink to "Spec reference"- SC 1.4.10 Reflow (AA) — content usable at 320 CSS pixels wide without two-dimensional scrolling, except for content that requires it (data tables may scroll horizontally, but hiding with access is often better).
- SC 1.3.1 (A) — hidden columns removed consistently (
display: noneremoves from both views). - SC 4.1.3 (AA) — announce when the column set changes in response to user action (not on resize alone).
- SC 1.3.2 (A) — revealed details follow their row in reading order.
When to use — and when not to
Permalink to "When to use — and when not to"Use column priority when some columns are clearly secondary for most tasks on small screens. Combine it with a column chooser so users can override defaults.
Do not hide columns users need to compare across rows; for comparison tasks, horizontal scrolling with a sticky first column is better. The misapplication: hiding columns with visibility: hidden or opacity, leaving them announced by screen readers but invisible to sighted users.
Annotated code example
Permalink to "Annotated code example"/* Priorities via classes on th and td */
@media (max-width: 900px) { .p3 { display: none; } } /* removed from both visual and AT views (SC 1.3.1) */
@media (max-width: 600px) { .p2 { display: none; } }
@media (min-width: 601px) { .row-details-toggle { display: none; } }
<p class="hidden-cols" id="hidden-cols">3 columns hidden. <button type="button" aria-haspopup="dialog">Choose columns</button></p>
<table aria-describedby="hidden-cols">
<thead><tr><th scope="col" class="p1">Invoice</th><th scope="col" class="p2">Customer</th>
<th scope="col" class="p1">Amount</th><th scope="col" class="p3">Owner</th><th scope="col" class="p1">Details</th></tr></thead>
<tbody>
<tr>
<th scope="row" class="p1">INV-1042</th><td class="p2">Lund & Co</td><td class="p1">1,240.00</td><td class="p3">M. Ortiz</td>
<td class="p1"><button type="button" class="row-details-toggle" aria-expanded="false" aria-controls="d-1042">Details for INV-1042</button></td>
</tr>
<tr id="d-1042" hidden><td colspan="5"><dl><dt>Customer</dt><dd>Lund & Co</dd><dt>Owner</dt><dd>M. Ortiz</dd></dl></td></tr>
</tbody>
</table>
// Update the hidden-column summary on resize (no announcement), announce on user choice
const update = () => { hiddenCount.textContent = `${countHidden()} columns hidden.`; };
new ResizeObserver(update).observe(table);
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Event | Expected | Announcement |
|---|---|---|
| Table at narrow width | Fewer columns | “Invoices, table, 3 columns” |
| Tab to Details button | Focus | “Details for INV-1042, collapsed” |
| Enter | Details row shown | “expanded”; details read next |
| Choose columns dialog | Checkboxes per column | Column names and states |
| Resize window | Columns hide | No announcement |
Integration context
Permalink to "Integration context"Horizontal scrolling alternatives are covered in keyboard scrollable table containers and sticky headers and first columns. Card layouts are in stacked card layouts that keep table semantics. The column chooser follows column visibility menus and column choosers.
Gotchas
Permalink to "Gotchas"Zoom triggers breakpoints. At 400% zoom, desktop users hit the narrow breakpoint; make sure the experience works with a keyboard and large text, not just touch.
Details rows and table structure. A details row with colspan is a valid table row; keep it immediately after its parent row.
Persisting overrides. If users choose to show a column on small screens, remember it.
Worked example
Permalink to "Worked example"An order management table had nine columns that overflowed on phones and at 400% zoom. The team ranked columns by task (order, status and total as priority 1), hid lower priorities with display: none at breakpoints, added “Details for {order}” disclosures showing hidden values as a description list, and a “4 columns hidden — Choose columns” summary. Zoom users no longer scrolled in two dimensions, and every value remained one button press away.
Common mistakes
Permalink to "Common mistakes"- visibility: hidden or opacity for hiding.
- No way to reach hidden values.
- Announcing column changes on every resize.
- Hiding the identifying column.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Add priority to column definitions and let the table component generate breakpoints, the hidden-column summary and row details automatically.
Step-by-step implementation
Permalink to "Step-by-step implementation"- Rank columns with real tasks. Ask what someone on a phone needs to decide what to do with a row. Usually it is the identifier, the status and one key value. Give those priority 1 and resist promoting everything.
- Hide with classes generated from the definition. Each column gets a priority class on both its header and its cells, so header and data hide together and the table stays rectangular.
- Add the details control last. The details button and the details row exist in the DOM at all widths but only display at narrow widths, so the reading order is predictable.
- Summarise what is hidden. Update the “3 columns hidden” text whenever the breakpoint changes, and tie it to the table with
aria-describedby. - Let users override. The column chooser stores per-user preferences; a user who always needs the due date on a phone should not have to open details for every row.
Edge cases
Permalink to "Edge cases"- Printing. Print styles should show all columns or print in landscape; hidden columns on paper cannot be revealed.
- Copy and paste. Copying a table with hidden columns skips them; offer an export for complete data.
- Assistive tech at zoom. Screen magnifier users at narrow effective widths benefit most from the details pattern; test with a magnifier as well as a phone.
Testing it with a screen reader
Permalink to "Testing it with a screen reader"On an iPhone with VoiceOver, open the table and use the rotor to move by rows. Each row should contain only the visible columns plus a “Details” button, with no announcements of hidden headers. Double-tap Details and confirm that the hidden values are read next as a labelled list. On Android with TalkBack, repeat and check that the “3 columns hidden” summary is read when entering the table. On desktop, zoom to 400% and use NVDA table navigation to confirm the same structure. If a screen reader still announces a hidden column header, the column was hidden with something other than display: none.
FAQ
Permalink to "FAQ"Does hiding columns violate reflow?
No; reflow encourages it. Hidden information must remain reachable.
Should hidden columns be announced?
State the count in a visible summary tied to the table; do not announce on resize.
Can details be in a dialog instead?
They can, but inline disclosures keep context and are simpler.
How do I choose priorities?
From user tasks on small screens: what must be seen to decide what to do with a row?
What about sorting by a hidden column?
Allow it from the sort control and mention the hidden column in the sort announcement.
Is horizontal scrolling acceptable instead?
For data tables, horizontal scrolling is an accepted exception under reflow, especially with a sticky first column. Choose per task.
Related
Permalink to "Related"- Sticky headers and first columns — the scrolling alternative
- Stacked card layouts — the card alternative
- Meeting reflow 1.4.10 with wide tables — the criterion