Skip to content

Documentation

On this page

Range group

Two <input type="range"> stacked on one track inside <fieldset class="form-range-group">. Load src/behaviors/range-group.js for clamp and fill. It is not a pe-* element. Markup follows the Open UI <rangegroup> proposal, so a future native wrapper is a rename.

Sizes are minified and gzip. A plugin figure includes the Bootstrap runtime that plugin imports. Shipping every plugin is bootstrap.bundle.min.js at 23.3 KB gzip, not the sum of these rows.
What runs it JavaScript
Bootstrap 5.3 No range-group plugin. Dual sliders are a third-party widget or custom ARIA sliders. Not in bootstrap.min.js
This repo Two `.form-range` in `.form-range-group`. Optional `src/behaviors/range-group.js` for clamp and fill. 0 KB. Optional src/behaviors/range-group.js
Markup this component accepts. The full inventory is /components.json.
Avoid
  • Do not point aria-labelledby at an <output>. That replaces the label and the slider is named by its value.
  • Do not put role="tooltip" on the <output>. It is a readout, not a tooltip.
  • Do not add a pe-range-group element. The fieldset is already the group.
  • data-range-group="off" skips clamp and fill. Omit it when you load the module and want that behavior.
  • Give both inputs the same min, max, and step. Different bounds break the shared geometry.
  • Do not add role="slider" or aria-valuemin / aria-valuemax / aria-valuenow on the inputs. type="range" already has that role.

How it works

  • The <legend> names the group. Each thumb has its own <label for>. Both inputs share min, max, and step so they sit on the same scale.
  • CSS stacks the inputs in .form-range-track. The track is not a click target. Only the thumbs receive pointer events.
  • The module listens to input and clamps a thumb against its neighbours in DOM order. Overlap is allowed. It does not rewrite min or max on the inputs.
  • It writes --bs-range-lower and --bs-range-upper on the fieldset so CSS can paint the fill. There is no CSS path from a live range value.
  • An optional <output for aria-live="off"> is a visible readout. The slider already announces the value, so the output is not a live region.
  • Without the script the inputs still work. Thumbs can cross, and there is no fill. data-range-group="off" on the fieldset skips the module when it is on the page. The CSS stack stays.

Price

Drag either thumb. They stop at each other. The fill and the numbers follow.

Price

200 – 800

HTML
<script type="module" src="./node_modules/bootstrap-html/src/behaviors/range-group.js"></script>

<fieldset class="form-range-group">
  <legend class="form-label">Price</legend>
  <label class="visually-hidden" for="price-min">Minimum price</label>
  <label class="visually-hidden" for="price-max">Maximum price</label>
  <div class="form-range-track">
    <input type="range" class="form-range" id="price-min" name="price-min" min="0" max="1000" step="10" value="200">
    <input type="range" class="form-range" id="price-max" name="price-max" min="0" max="1000" step="10" value="800">
  </div>
  <p class="form-text mb-0">
    <output for="price-min" aria-live="off">200</output> – <output for="price-max" aria-live="off">800</output>
  </p>
</fieldset>

No script

Same stacking. data-range-group="off" skips clamp and fill when the module is on the page. Thumbs can pass, and the track has no fill.

Year
HTML
<fieldset class="form-range-group" data-range-group="off">
  <legend class="form-label">Year</legend>
  <label class="visually-hidden" for="year-min">Earliest year</label>
  <label class="visually-hidden" for="year-max">Latest year</label>
  <div class="form-range-track">
    <input type="range" class="form-range" id="year-min" name="year-min" min="1900" max="2026" step="1" value="1980">
    <input type="range" class="form-range" id="year-max" name="year-max" min="1900" max="2026" step="1" value="2010">
  </div>
</fieldset>

Accessibility

Each thumb is a native slider. Arrow keys, Home, End, and Page Up / Page Down come from the browser. Tab order is DOM order and does not change when thumbs overlap.

Bootstrap’s focus ring is on the thumb, so each stacked input still shows which handle is focused.

Name the group with <legend>. Name each thumb with <label for>. Do not use aria-labelledby on a value readout.

The readout is <output for aria-live="off">. Focus is on the slider, which already announces the value; a live region would speak it twice. That is the opposite of Spin button, where focus stays on the button.

Thumbs are 1.5rem (24px) because the track is not a pointer target. That meets WCAG 2.5.8 Target Size (Minimum).

Browser support

Three columns, not one baseline. The same row is on the overview.
No script Polyfill No fallback
Two native type="range" on one track. Keyboard, submit, and focus work. Without range-group.js thumbs can cross and there is no fill. data-range-group="off" skips the module when it is loaded. None The track is not a click target: pointer-events: none on the inputs, so only the thumbs receive pointer events. The later thumb in the DOM wins when they overlap. Fill needs range-group.js.