Saved Views Menus for Data Grids
Permalink to "Saved Views Menus for Data Grids"Saved views let users name a combination of filters, sort, visible columns and density — “My overdue invoices”, “Q3 by region” — and return to it in one step. For keyboard and screen reader users they are especially valuable, because rebuilding a complex filter by hand is slow. But the typical implementation — a dropdown of names, a tiny “modified” dot, and a save icon — hides most of what users need to know: which view is active, whether it has unsaved changes, and what switching views actually did to the grid.
This guide belongs to tabs & view switchers for data views.
Spec reference
Permalink to "Spec reference"- Menu button pattern — a button with
aria-haspopupopening amenu(for actions) or alistbox(for choosing a value). Saved views are usually a choice, so a listbox (or a menu withmenuitemradio) fits. menuitemradiowitharia-checked— marks the active view inside a menu.- SC 4.1.2 Name, Role, Value (A) — the button’s name includes the current view; the active item is marked checked.
- SC 4.1.3 Status Messages (AA) — switching views changes the grid substantially; announce the outcome.
- SC 1.4.1 Use of Color (A) — an unsaved-changes dot must have a text equivalent.
- SC 3.3.2 Labels or Instructions (A) — the save dialog’s fields are labelled.
When to use — and when not to
Permalink to "When to use — and when not to"Offer saved views when users repeatedly configure the same grid state — triage queues, reports, admin lists. Combine them with URL state so a view can also be bookmarked or shared.
Do not auto-save every change into the current view; users lose the original. Show “modified” and let them choose Save or Save as. Do not hide the current view’s name behind an icon.
The misapplication: switching views silently. The grid’s rows, columns and sort all change, and a screen reader user hears only the menu closing.
Annotated code example
Permalink to "Annotated code example"<!-- Current view and modified state in the name (SC 4.1.2, 1.4.1) -->
<button type="button" id="views-btn" aria-haspopup="menu" aria-expanded="false" aria-controls="views-menu">
View: My overdue invoices <span class="modified">(modified)</span>
</button>
<div role="menu" id="views-menu" aria-labelledby="views-btn" hidden>
<div role="group" aria-label="My views">
<div role="menuitemradio" aria-checked="true" tabindex="-1">My overdue invoices</div>
<div role="menuitemradio" aria-checked="false" tabindex="-1">Large accounts</div>
</div>
<div role="group" aria-label="Shared views">
<div role="menuitemradio" aria-checked="false" tabindex="-1">Finance — month end</div>
</div>
<div role="separator"></div>
<div role="menuitem" tabindex="-1">Save changes to My overdue invoices</div>
<div role="menuitem" tabindex="-1" aria-haspopup="dialog">Save as new view…</div>
<div role="menuitem" tabindex="-1" aria-haspopup="dialog">Manage views…</div>
</div>
<p id="grid-status" role="status" class="visually-hidden"></p>
async function switchView(view) {
const before = gridState();
await grid.applyState(view.state); // filters, sort, columns, density
const after = gridState();
closeMenu(); viewsBtn.focus(); // stay on the button (SC 2.4.3)
status.textContent = [ // what changed (SC 4.1.3)
`View: ${view.name}.`, `${after.rowCount} rows.`,
after.sort !== before.sort ? `Sorted by ${describeSort(after.sort)}.` : '',
columnDelta(before, after), // e.g. "3 columns hidden."
].filter(Boolean).join(' ');
}
Keyboard & AT behaviour
Permalink to "Keyboard & AT behaviour"| Key | Action | Expected announcement | AT deviations |
|---|---|---|---|
| Enter on view button | Open menu | “View: My overdue invoices (modified), menu, My overdue invoices, checked” | — |
| Arrows | Move through views and actions | “Large accounts, radio menu item, not checked” | Group labels read on entering a group in NVDA |
| Enter on a view | Switch; menu closes | “View: Large accounts. 42 rows. Sorted by Amount, descending.” | — |
| Enter on Save as | Dialog with “View name” field | Dialog name and field | — |
| Escape | Close menu | Button name | — |
Integration context
Permalink to "Integration context"The menu follows row action menus with the menu button pattern. Filter state inside a view uses the same descriptions as applied filter chips and removal. Column visibility changes are those from column visibility menus and column choosers. View announcements in general are covered in announcing view changes in data apps.
Gotchas
Permalink to "Gotchas"Long view names. Truncated names in the button lose the distinguishing part (“Finance — month end — EU…”). Keep the full name in the accessible name and show a tooltip on focus for the visual.
Shared views that change. If a colleague edits a shared view, the user’s current grid may no longer match. Indicate “updated by Maria at 10:42” and offer reload rather than switching silently.
Default view. Mark which view loads by default (“default”) in text, and let users change it from Manage views.
Framework notes
Permalink to "Framework notes"AG Grid exposes getState() and setState() (v31+) covering filters, sort, columns and more — a natural base for saved views. MUI X provides apiRef.current.exportState() and restoreState(). TanStack Table state is plain objects you can persist. In all cases, compute the announcement from before and after state rather than trusting the view definition, because some state (like columns that no longer exist) may not apply.
// AG Grid
const before = api.getState();
api.setState(view.state);
announce(diffStates(before, api.getState()));
Worked example
Permalink to "Worked example"A support team’s queue offered twelve shared views in an icon-only dropdown. A screen reader user could not tell which view was active, switching produced no feedback, and “modified” was a blue dot. The rebuild put “View: Urgent — EMEA (modified)” in the button text, split personal and shared views into labelled groups with menuitemradio, announced each switch with the row count and key changes, and moved Save, Save as and Manage into the same menu after a separator. The user now switches between four views a dozen times a day and always knows where she is.
Testing checklist
Permalink to "Testing checklist"Design system notes
Permalink to "Design system notes"Make saved views a grid capability with a serialisable state contract, a standard menu, a save dialog and a diff-based announcer. Teams supply storage; the component guarantees names, states and messages.
FAQ
Permalink to "FAQ"Menu or listbox for saved views?
A menu with menuitemradio items works well when actions (Save, Manage) live in the same popup. A listbox suits a pure choice. Either is fine if roles and checked state are correct.
Should switching views move focus to the grid?
Usually not; users often switch several times to compare. Keep focus on the button and announce the change.
How should unsaved changes be shown?
In text in the button’s name — “(modified)” — and, optionally, a visual dot. Never the dot alone.
Should views be in the URL?
Yes, at least the view ID and any unsaved modifications, so Back and shared links work.
What if a view references a column that was removed?
Apply what you can and say what you could not: “View: Q3 by region. 1 column in this view is no longer available.”
Related
Permalink to "Related"- Announcing view changes — the general rules
- Applied filter chips — describing filters
- Persisting search state — state in the URL