Brush Selection Alternatives for Range Filtering

Permalink to "Brush Selection Alternatives for Range Filtering"

Brushing is a powerful technique in data exploration: drag across a timeline to select a period, and every linked chart and table filters to it. It is also a pure dragging interaction with no keyboard equivalent in most libraries, and its effect — other views changing — is silent. WCAG 2.2’s Dragging Movements criterion requires a single-pointer alternative, and keyboard users need one too. The best alternatives are boring and effective: two labelled inputs and, optionally, a range slider.

This guide belongs to interactive chart controls.

Spec reference

Permalink to "Spec reference"
  • SC 2.5.7 Dragging Movements (AA) — brushing needs a non-drag, single-pointer alternative.
  • SC 2.1.1 Keyboard (A) — range selection must be keyboard operable.
  • ARIA slider pattern (multi-thumb) — each thumb is a role="slider" (or input type="range") with its own name, aria-valuenow, aria-valuemin, aria-valuemax and aria-valuetext.
  • SC 4.1.3 Status Messages (AA) — the selection’s effect on linked views (“Showing 240 orders from 1–14 Sep”) is a status message.
  • SC 3.3.2 Labels or Instructions (A) — inputs need labels and formats.
Selecting a range without dragging Sequence among start input, end input, chart and linked table: the user types a start date and an end date; the chart draws the brush; the table filters; the status reports the range and count. Selecting a range without draggingStart inputEnd inputChartLinked table2026-09-01: brush start2026-09-14: brush endfilter to range"Showing 240orders, 1 to 14Sep"
Typed values drive the same state the brush would.

When to use — and when not to

Permalink to "When to use — and when not to"

Provide inputs whenever a chart supports brushing. Add a range slider when the range is continuous and approximate selection is acceptable (a price range, a time window); prefer typed inputs when precision matters (exact dates).

Do not provide only a slider for long ranges with fine granularity — moving a thumb across 365 days one key press at a time is slow. Support Page Up and Page Down, Home and End, and typed input.

The misapplication: a “brush” implemented with D3’s brushX and no other control, and linked views that change silently.

Brush only versus brush with alternatives Comparison: a brush alone needs dragging and gives no feedback; brush plus inputs and slider supports keyboard and single-pointer use and announces effects. Brush only versus brush with alternatives✗ Brush onlyDrag requiredSelection invisible to ATLinked views change silentlyClear by clicking outside✓ Brush + inputs + sliderType dates or move thumbsInputs and sliders announce values"Showing 240 orders, 1 to 14 Sep"Clear selection button

Annotated code example

Permalink to "Annotated code example"
<fieldset class="range-filter">
  <legend>Order date range</legend>
  <label for="r-start">From</label>
  <input id="r-start" type="date" min="2026-07-01" max="2026-09-30" value="2026-09-01">
  <label for="r-end">To</label>
  <input id="r-end" type="date" min="2026-07-01" max="2026-09-30" value="2026-09-14">
  <button type="button" id="r-clear">Clear range</button>
</fieldset>

<!-- Optional two-thumb slider: two native ranges styled together -->
<div class="dual-range">
  <input type="range" id="t-start" aria-label="Range start" min="0" max="91" value="62" aria-valuetext="1 September">
  <input type="range" id="t-end" aria-label="Range end" min="0" max="91" value="75" aria-valuetext="14 September">
</div>
<p id="r-status" role="status" class="visually-hidden"></p>
function setSelection(start, end, source) {
  if (end < start) [start, end] = [end, start];
  state.range = { start, end };
  if (source !== 'brush') brush.move(brushG, [x(start), x(end)]);         // keep the drawing in sync
  if (source !== 'inputs') { startInput.value = iso(start); endInput.value = iso(end); }
  if (source !== 'slider') { tStart.value = idx(start); tEnd.value = idx(end); }
  tStart.setAttribute('aria-valuetext', fmt(start)); tEnd.setAttribute('aria-valuetext', fmt(end));
  const n = linked.filter(start, end);
  clearTimeout(t);
  t = setTimeout(() => { status.textContent = `Showing ${n} orders from ${fmt(start)} to ${fmt(end)}.`; }, 400);
}
brush.on('end', ({ selection }) => selection && setSelection(x.invert(selection[0]), x.invert(selection[1]), 'brush'));

Keyboard & AT behaviour

Permalink to "Keyboard & AT behaviour"
Control Key Expected announcement AT deviations
Date inputs Type or use picker “From, 2026-09-01” Native date input behaviour varies by browser
Range thumbs Arrows “Range start, 2 September” —
Range thumbs Page Up / Down Moves by a week Implement if using custom sliders
After change — “Showing 240 orders from 1 Sep to 14 Sep.” —
Clear range Enter “Range cleared. Showing all 1,980 orders.” —
Alternatives to brushing and their strengths Matrix of alternatives — typed inputs, two-thumb slider, preset buttons — against precision, speed for large changes, keyboard support and screen reader clarity. Alternatives to brushing and their strengthsPrecisionLarge changesKeyboardClarityTyped inputsExactFastYesValues spokenTwo-thumb sliderStep-sizedPage keysYesvaluetextPreset buttonsFixed rangesOne pressYesNamed
Typed inputs for precision; presets for speed; sliders for approximate ranges.

Integration context

Permalink to "Integration context"

Linked views that react to the selection should follow linked chart and table highlighting. For time ranges specifically, presets plus custom inputs are covered in time range selectors for time series. Range sliders for numeric filters are detailed in range slider filters for numeric columns.

Gotchas

Permalink to "Gotchas"

Thumb crossing. When the start thumb passes the end thumb, swap values or block the move; never allow start after end silently.

Brushes on canvas. Canvas brushes have no DOM at all; the inputs are the only accessible state, so keep them visible.

Announcing during drags. Do not announce on every brush move; announce on brush end.

Common mistakes

Permalink to "Common mistakes"
  • No alternative to the brush.
  • Sliders without value text, announced as raw indices.
  • Linked views that change without a message.
  • No way to clear the selection without clicking empty space.

Framework notes

Permalink to "Framework notes"

D3 brushX supports programmatic moves via brush.move, which keeps the drawing in sync with inputs. ECharts brush and dataZoom components can be driven with dispatchAction. Vega-Lite interval selections can be bound to input widgets (bind), which gives an accessible alternative almost for free.

{ "params": [{ "name": "range", "select": { "type": "interval", "encodings": ["x"] }, "bind": "scales" }] }

Worked example

Permalink to "Worked example"

A logistics dashboard let analysts brush a timeline to filter shipments in four linked charts and a table. A keyboard-only analyst could not filter at all. The team added From and To date inputs above the timeline, a “Last 7 days / 30 days / Quarter” preset group, and a Clear button, all synchronised with the brush, plus a status message describing the effect: “Showing 412 shipments from 1 to 14 September in all views.” Several mouse users switched to the inputs because they wanted exact dates.

Writing the effect message

Permalink to "Writing the effect message"

The effect message after a selection should answer “what am I looking at now?” across every linked view: the range in words, the resulting count, and which views changed. “Showing 412 shipments from 1 to 14 September in all views” is better than “Selection updated”. If only some views respond to the selection, name them: “Map and table filtered; trend chart unchanged.”

Testing checklist

Permalink to "Testing checklist"

Design system notes

Permalink to "Design system notes"

Model selections as shared state with sources (brush, inputs, slider, presets) and provide a RangeFilter component that renders inputs and presets and announces effects. Charts subscribe to the state rather than owning it.

FAQ

Permalink to "FAQ"
Is a brush ever acceptable without alternatives?

Not under WCAG 2.2 AA when it is the only way to select a range.

Slider or inputs?

Inputs for precision; sliders for approximate ranges; both if you can.

Should the brush be focusable?

It can be, with arrow keys to move its edges, but inputs are simpler and more reliable.

How should multiple brushes (on several charts) be handled?

Show each selection as a named filter chip with its inputs, and announce the combined effect.

What about two-dimensional brushes on scatter plots?

Provide X and Y range inputs, or lasso alternatives such as “select points where X between … and Y between …”.

Should the selection persist in the URL?

For shareable analyses, yes.

How should presets and custom inputs interact?

Choosing a preset fills the inputs with its dates; editing an input switches the preset to “Custom”. Both changes update the brush, so the three controls never disagree.

Permalink to "Related"

← Back to Interactive Chart Controls