Range

Adding fa-component="range" to a .group-range wrapper turns one or two input[type="range"] elements inside it into a fully functional Range component — complete with a positioned tooltip, an animated fill bar, and optional tick marks.

Adding a second input[type="range"] inside the wrapper automatically switches the component into dual (min–max) mode in JavaScript — no extra option is required, the presence of the second input is enough for Range to detect it. On the markup side, also add the .form-range-dual class to the wrapper: it is what tells the stylesheet to lay the two thumbs and the fill bar on top of a shared track instead of stacking two independent tracks.

The component reads min, max, and step directly from each input, so these attributes should always be set explicitly (don't rely on browser defaults).


Getting Started

Range is lazy-loaded. Calling new FrontAlign() doesn't instantiate every fa-component="range" element on the page immediately — instead, each one is handed to a shared IntersectionObserver, and the real new Range(el) call only fires once that element enters (or nears, thanks to a 100px root margin) the viewport. On pages with many sliders, this keeps the initial render light: off-screen ranges never build their tooltip, fill bar, or tick DOM until they're actually needed.

The runtime also keeps a MutationObserver watching the document. Any fa-component="range" element added later — from an AJAX response, inside a modal, after a client-side route change — is picked up automatically and fed into the same lazy-load pipeline. No manual re-scan or init call is required.

import 'frontalign';
 
// FrontAlign initializes itself on import — new FrontAlign() runs once
// and registers every [fa-component] element on the page (and any added
// later) with the lazy-load observer. A range only becomes a live Range
// instance once it enters, or approaches, the viewport:
// <div class="group-range" fa-component="range">...</div>
 
// Need custom options for one specific instance? Import the class and
// construct it yourself — this bypasses the observer and initializes
// immediately, regardless of scroll position.
import { Range } from 'frontalign';
const el = document.querySelector('#price-range');
new Range(el, { valueLabel: (v) => `$${v}` });

React: see React Integration for the complete useRange reference.

Basic

A single thumb with the default tooltip.

<div class="group-range" fa-component="range">
  <input class="form-range" type="range" min="0" max="100" step="1" value="40">
</div>

Custom Value Label

Pass valueLabel as an option to format the tooltip and the aria-valuetext attribute — either a '{value}' string template or a function.

<div class="group-range" fa-component="range" id="price-range">
  <input class="form-range" type="range" min="0" max="2000" step="50" value="650">
</div>

Note: valueLabel is set only through the constructor — not through markup or a data-* attribute. Instantiate the component with new Range(el, options) for any range that needs custom formatting, rather than relying on Range.init().


Ticks

Bind an input to a <datalist> via the list attribute to render tick marks along the track. Ticks are only supported for single-thumb ranges — a list attribute on a dual-range input is simply ignored.

<div class="group-range" fa-component="range">
  <input class="form-range" type="range" min="0" max="100" step="25" value="50" list="range-steps">
</div>

<datalist id="range-steps">
  <option value="0" label="0"></option>
  <option value="25" label="25"></option>
  <option value="50" label="50"></option>
  <option value="75" label="75"></option>
  <option value="100" label="100"></option>
</datalist>

Dual Range (Min–Max)

Two input[type="range"] elements inside the same wrapper, plus the .form-range-dual class, produce a min–max slider. The thumbs are prevented from crossing while dragging — one stops as it approaches the other by step.

<div class="group-range form-range-dual" fa-component="range">
  <input class="form-range" type="range" min="0" max="1000" step="10" value="200">
  <input class="form-range" type="range" min="0" max="1000" step="10" value="800">
</div>

Note: Dual ranges use the first input's step value to determine the minimum gap between thumbs — give both inputs the same step value, otherwise behavior may be inconsistent.


Disabled

The standard disabled attribute on the input; the thumb and track are dimmed and the cursor changes to not-allowed.

<div class="group-range" fa-component="range">
  <input class="form-range" type="range" min="0" max="100" value="60" disabled>
</div>

Customizing Colors (CSS Variables)

The entire visual appearance of the Range component is built on CSS custom properties, all scoped to .group-range. To change color, size, or other visual parameters, you don't need to edit the CSS file — just override these variables on .group-range (the wrapper) or any ancestor element.

VariableControlsDefault
--range-fillColor of the filled/progress portion, the thumb's border, and the tick marksvar(--primary)
--range-trackBackground color of the empty trackoklch(0.94 0.005 260)
--range-focus-ringColor of the focus ring around the thumboklch(from var(--primary) l c h / 0.35)
--range-heightHeight of the track6px
--range-thumbDiameter of the thumb (circle)18px
--range-hitHeight of the touch/click hit area (larger than the visible track for mobile comfort)32px

Example — changing the color of a single range

<div class="group-range" fa-component="range" style="--range-fill: #22c55e; --range-track: #e5f9ee;">
  <input class="form-range" type="range" min="0" max="100" value="40">
</div>

Example — changing colors globally across the site

:root {
  --primary: #7c3aed;
  --range-track: #ede9fe;
  --range-focus-ring: rgba(124, 58, 237, 0.35);
  --range-thumb: 20px;
  --range-height: 8px;
}

Note: --range-fill defaults to var(--primary), and the tick marks (.form-range-tick) read var(--primary) directly. If your design system already defines a global --primary variable, changing it will automatically update the fill color and the tick color of every Range component — there's no need to override --range-fill separately.

Tick label color (.form-range-tick-label) follows var(--body-text) rather than a dedicated Range variable — override that class directly if you need a different label color independent of body text.


Options

OptionTypeDefaultDescription
tooltipbooleantrueShow a floating value tooltip while interacting with a thumb.
ticksbooleantrueRender tick marks from a bound <datalist>. Ignored on dual ranges.
valueLabelstring | function | nullnullFormats the displayed value. String templates use ''; functions receive the raw value and return a string.

Events

Both events bubble and expose the current value on event.detail.value:

  • On dual ranges: an array of two numbers — [min, max]
  • On single ranges: a single number

event.detail.index identifies which thumb triggered the event (0 or 1 on dual ranges).

EventFires
fa.range.inputContinuously while dragging a thumb (coalesced to one per animation frame via requestAnimationFrame).
fa.range.changeOnce, when a thumb is released or its value is committed.
const el = document.getElementById('price-range');

// Continuous tracking — e.g. for a live preview
el.addEventListener('fa.range.input', (event) => {
  console.log('Dragging:', event.detail.value, 'thumb:', event.detail.index);
});

// Only when the value settles — e.g. to trigger an API request
el.addEventListener('fa.range.change', (event) => {
  console.log(event.detail.value); // 65  —  or [200, 800] for a dual range
});

Performance tip: Bind "heavy" operations like server requests or filtering to fa.range.change. fa.range.input is meant only for live UI feedback (e.g. updating a price label in real time).


Methods

MethodDescription
Range.getInstance(el)Returns the existing instance for el, or null.
Range.getOrCreateInstance(el, options)Returns the existing instance, or creates one.
Range.init(root)Initializes every [fa-component="range"] under root (defaults to document).
instance.getValue()Returns the current value — a number, or [min, max] for dual ranges.
instance.setValue(value)Sets the value programmatically and re-renders. Accepts a number, or [min, max] for dual ranges.
instance.refresh()Re-reads min/max/step/list from the DOM and re-renders. Call this after changing those attributes programmatically.
instance.dispose()Removes all listeners and injected DOM (tooltip, fill, ticks), and detaches the instance.
const el = document.querySelector('#price-range');
const instance = Range.getInstance(el);

// Read the current value
console.log(instance.getValue());

// Set the value programmatically
instance.setValue(850);

// After dynamically changing the min/max attribute
el.querySelector('input').max = 3000;
instance.refresh();

// Fully tear down the component
instance.dispose();

Notes

  • The wrapper element must carry both fa-component="range" (so Range.init() finds it, matching Range.SELECTOR) and the .group-range class (so the CSS custom properties and layout rules apply).
  • Dual mode is detected by JavaScript purely from the number of input[type="range"] elements inside the wrapper (exactly two). The .form-range-dual class is a separate, required piece on the markup side — it's what the stylesheet uses to lay the thumbs, track, and fill on top of one another instead of stacking two full-width tracks.
  • Ticks are only supported on single-thumb ranges — a list attribute on a dual-range input is simply ignored, no error is thrown.
  • Dual ranges use the first input's step value to determine the minimum gap between thumbs; give both inputs the same step value.
  • The valueLabel option is set only through the constructor (new Range(el, options)) — not through markup or a data attribute.
  • If you change min, max, step, or the bound list on an input programmatically after init, call instance.refresh() — the component has no MutationObserver watching those attributes, so the change won't apply automatically.
  • The component does not run crossing-correction during the initial render (inside the constructor) — this only becomes active once the user starts dragging. This ensures your initial markup values are never silently rewritten.
  • In environments that support ResizeObserver, the component automatically recalculates the tooltip position whenever the wrapper's size changes.

FrontAlign