Mobile product filters that work with a keyboard

Design mobile filters as a complete interaction, including focus, draft selections, result updates, and a reliable path back to the product list.

Znode 10GuideIntermediateGCG engineering guide
In this guide
Open with context Edit draft choices Apply or cancel Return focus Understand results
A filter panel is a stateful interaction with a beginning, a decision, and a predictable return path.

A drawer that looks finished but traps the buyer

On a phone, the filter panel slides over the product list. Touch users can tap a checkbox and apply it. A keyboard user opens the same panel, tabs behind it into hidden product links, and cannot tell where the close control went. The layout is responsive, but the interaction is incomplete.

Start with the journey: open filters, understand the available choices, change them, apply or cancel, and return to the results. A mobile drawer that blocks the rest of the page should behave as a modal dialog. A nonmodal panel needs a different interaction model; do not mix their semantics accidentally.

Make focus part of the component contract

For a modal filter panel, move focus to a useful control when it opens, keep Tab navigation within it, support Escape, and return focus to the trigger when it closes. Provide a visible close button and an accessible name. The W3C modal dialog pattern describes these core expectations.

Use a proven dialog primitive where possible instead of rebuilding focus containment from scattered event handlers. The surrounding page should not remain interactable while presented as modal. Initial focus needs judgment: on a long filter panel, focusing a heading can provide context without scrolling directly to a distant checkbox.

Build a modal filter component

MobileFilters.tsx includes the trigger, native modal dialog, labeled checkboxes, draft state, Apply action, Escape handling, and focus return. The browser supplies modal focus containment through showModal. Give the dialog a viewport-bounded height and internal scrolling in the theme stylesheet.

The parent component owns the applied query, URL state, and result-count announcement. Wire onApply to the existing storefront filter action and pass unique option values. Reopening copies the current applied filters, so canceling never commits draft selections. Verify the native dialog behavior in your supported browsers and assistive technologies.

filters/MobileFilters.tsx tsx

"use client";
import { useId, useRef, useState } from "react";
type FilterOption = { value: string; label: string };
type Props = {
options: readonly FilterOption[];
applied: readonly string[];
onApply(values: string[]): void;
};
export function MobileFilters({ options, applied, onApply }: Props) {
const dialog = useRef<HTMLDialogElement>(null);
const trigger = useRef<HTMLButtonElement>(null);
const [draft, setDraft] = useState<string[]>([]);
const titleId = useId();
function open() {
setDraft([...applied]);
dialog.current?.showModal();
}
function close() { dialog.current?.close(); }
return <>
<button ref={trigger} type="button" onClick={open}>
Filters ({applied.length})
</button>
<dialog ref={dialog} aria-labelledby={titleId}
onClose={() => trigger.current?.focus()}
onCancel={() => setDraft([...applied])}>
<h2 id={titleId}>Filter products</h2>
<button type="button" autoFocus onClick={close}>Cancel</button>
<fieldset>
<legend>Product options</legend>
{options.map(option => <label key={option.value}>
<input type="checkbox" checked={draft.includes(option.value)}
onChange={event => {
const checked = event.currentTarget.checked;
setDraft(current => checked ? [...current, option.value]
: current.filter(value => value !== option.value));
}} />
{option.label}
</label>)}
</fieldset>
<button type="button" onClick={() => {
const allowed = new Set(options.map(option => option.value));
onApply([...new Set(draft)].filter(value => allowed.has(value)));
close();
}}>Apply filters</button>
</dialog>
</>;
}
// Native dialog supplies modal focus containment and Escape behavior.
// Give dialog a viewport-bounded height and overflow:auto in theme CSS.
// Parent owns URL/query updates and announces the resulting product count.

Use ordinary controls for ordinary choices

A filter value is usually a checkbox or radio button with a real label. A collapsible category group needs an operable button that communicates expanded state. Avoid making a styled text label the only click target or using color alone to indicate selection.

Keep counts and disabled states understandable. If a value would yield no results, decide whether it remains available and explain any disabled choice. Long technical attribute values should wrap without pushing the checkbox off-screen. The code block, drawer, and product list must each stay within their intended scrolling boundaries at narrow widths.

Test with keys and real layout constraints

At a narrow viewport, open the panel using Enter, traverse it with Tab and Shift+Tab, expand groups, change selections, apply, and reopen. Test Escape and the visible close button. Verify focus returns predictably and does not disappear when result content changes.

Exercise long labels, many groups, zero results, browser back navigation, and a zoomed layout. Check that result updates are announced without excessive repetition. Use automated checks to catch structural issues, then manually inspect interaction and screen-reader behavior. Combine rendered checks with keyboard and screen-reader interaction checks.

  • Keep focus visible on every control.
  • Give the trigger an understandable selected-filter count.
  • Do not unexpectedly scroll the page when results update.
  • Honor reduced motion for optional transitions.

Treat the empty result as part of the design

When filters produce no products, retain the active selections and provide an obvious way to revise them. A blank list should not force the buyer to remember which hidden filters caused it. Useful recovery matters as much as opening the panel smoothly.

An accessible filter experience also helps mouse and touch users because its states and choices are explicit. The design is complete when a buyer can enter, change, understand, and exit the interaction confidently, regardless of whether they operate the storefront with a finger, keyboard, or assistive technology.

References and further reading

Bring your next engineering question.

Improve the B2B buying experience

Explore how GCG can help

Independent guidance from GCG. Znode is a trademark of its owner. Examples use fictional data and are not official platform documentation. Suggest a correction.