Getting StartedComponentsDesign TokensThemingTheme SynthesizerFrameworksAccessibilityUtilitiesServer RenderingBrowser SupportContributingChangelog App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip
ARC UI ARC Radiant Components
v3.2 Docs Components Tokens Synthesizer
Getting StartedFrameworksServer Rendering Design TokensThemingTheme SynthesizerTypographyUtilities All ComponentsAccessibilityBrowser SupportChangelogContributingStats App ShellAspect GridAuth ShellCenterClusterContainerDashboard GridDockFloat BarInsetMasonryPage HeaderPage LayoutResizableResponsive SwitcherSectionSettings LayoutSplit PaneStatus BarStickyToolbar Anchor NavBottom NavBreadcrumbBreadcrumb MenuCommand BarDrawerFooterLinkMenubarNavigation MenuPage IndicatorPaginationRailScroll IndicatorScroll SpyScroll To TopSidebarSkip LinkSpeed DialStepper NavTabsTop BarTree View AccordionAspect RatioAvatarAvatar GroupCalloutCardCarouselCollapsibleColor SwatchCTA BannerDividerEmpty StateFeature CardIconImageImage CompareImage HotspotsInfinite ScrollLightboxMarqueeQR CodeScroll AreaSeparatorSkeletonSpinnerStackVideoVirtual List Activity HeatmapAnimated NumberBadgeChartClockComparisonCountdown TimerData GridData TableDescription ListDiffEvent CalendarGaugeJSON TreeKanbanKey ValueLevel MeterListMeterSparklineStatStepperTableTagTimelineUptimeValue CardWaveform BlockquoteCode BlockGradient TextHighlightKbdKeyboard MapMarkdownNumber FormatProseTerminalTextTime AgoTruncateTypewriter ButtonButton GroupCalendarCheckboxChipColor PickerComboboxCopy ButtonDate PickerDate Range PickerFieldsetFile UploadFormHotkeyIcon ButtonImage CropperInline EditInputInput GroupKnobLabelMasked InputMulti SelectNumber InputOTP InputPassword InputPin InputRadio GroupRange SliderRatingSearchSegmented ControlSelectSignature PadSliderSortable ListSwitch GroupTag InputTextareaTheme ToggleTime PickerToggleTransfer ListTree Select AlertAnnouncementBannerCommand PaletteConfirmConnection StatusContext MenuConversationDialogDropdown MenuGuided TourHover CardInline MessageLoading OverlayModalNotification PanelPopoverProgressProgress ToastSheetSnackbarSpotlightToastTooltip

Range Slider

Dual-thumb range slider for selecting a numeric interval within a defined range, with accent-primary fill between the thumbs and live value display.

Components Range Slider
input interactive
<arc-range-slider>

Overview

RangeSlider lets users select a contiguous sub-range between two bounds by dragging two thumbs along a shared track. The filled region between the thumbs is rendered with accent-primary, giving an immediate visual cue of the selected interval. When a `label` is provided the component renders a header row with the label on the left and the current range values ("low – high") on the right in monospace font, matching the single Slider's visual pattern. The `show-values` attribute (on by default) controls whether the numeric readout appears. Rather than layering two native range inputs, this component implements a custom track with pointer capture-based dragging for reliable cross-browser behavior. Both thumbs are keyboard accessible with arrow keys, Home, and End, and each carries proper ARIA `role="slider"` attributes (`aria-valuenow`, `aria-valuemin`, `aria-valuemax`) so screen readers announce the current value and range constraints. RangeSlider fires `arc-input` continuously during drag for real-time previews and `arc-change` on thumb release for committing the final selection, with `{ low, high }` in the event detail. The low thumb is clamped to never exceed the high thumb and vice versa, preventing invalid crossover states.

Guidelines

When to use

  • Provide a `label` so users understand what the range represents at a glance
  • Use for selecting a sub-range within a larger set, such as a price filter, date range, or frequency band
  • Use `arc-input` for real-time filtering or preview and `arc-change` for committing the selection to a server
  • Choose `step` values that match your data granularity — use 1 for integers, 0.01 for fine decimal values
  • Place the RangeSlider in a container at least 250px wide for comfortable dual-thumb dragging
  • Set `low` and `high` to sensible defaults that represent the most common range for your use case

When not to use

  • Do not use RangeSlider when the user only needs to pick a single value — use Slider instead
  • Do not set a `step` so small that the two thumbs become difficult to separate with a mouse
  • Do not omit `label` when the slider is standalone — without context the numeric readout is meaningless
  • Do not use for non-numeric selections — use a multi-select or checkbox group instead
  • Avoid placing multiple range sliders in a narrow column without sufficient vertical spacing

Features

  • Custom dual-thumb track built with pointer capture for reliable cross-browser dragging
  • Accent-primary filled region between the two thumbs visually indicates the selected range
  • Header row displaying the label and "low – high" values in monospace font when `label` and `show-values` are set
  • Configurable `min`, `max`, and `step` props for precise range and increment control
  • Thumb hover and focus effects with scale-up and accent-primary glow shadow matching the single Slider
  • Keyboard support: ArrowLeft/Right/Up/Down step by `step`, Home and End jump to limits
  • Dual events: `arc-input` fires continuously during drag, `arc-change` fires on release, both with `{ low, high }` detail
  • Full ARIA slider roles on each thumb with `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, and `aria-label`
  • Clamping logic prevents thumbs from crossing each other, maintaining low ≤ high invariant
  • Click-on-track moves the nearest thumb to the clicked position
  • Disabled state at 50% opacity with pointer events blocked
  • Reduced-motion media query disables thumb transitions

Preview

Usage

This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.

<script type="module" src="@arclux/arc-ui"></script>

<!-- Basic price range filter -->
<arc-range-slider label="Price" low="20" high="80" min="0" max="200" step="5"></arc-range-slider>

<!-- Temperature comfort zone -->
<arc-range-slider label="Comfort Zone" low="18" high="24" min="10" max="40" step="0.5"></arc-range-slider>

<!-- Frequency band selector -->
<arc-range-slider label="Frequency (Hz)" low="200" high="4000" min="20" max="20000" step="10"></arc-range-slider>

<!-- Disabled state -->
<arc-range-slider label="Locked Range" low="30" high="70" disabled></arc-range-slider>

<!-- Without value display -->
<arc-range-slider label="Silent" low="10" high="90" show-values="false"></arc-range-slider>

<script>
  const rs = document.querySelector('arc-range-slider');

  // Real-time filtering while dragging
  rs.addEventListener('arc-input', (e) => {
    console.log('Filtering:', e.detail.low, '–', e.detail.high);
  });

  // Commit final range on release
  rs.addEventListener('arc-change', (e) => {
    console.log('Committed:', e.detail.low, '–', e.detail.high);
  });
</script>
import { RangeSlider } from '@arclux/arc-ui-react';
import { useState } from 'react';

function PriceFilter() {
  const [low, setLow] = useState(20);
  const [high, setHigh] = useState(80);

  return (
    <div style={{ maxWidth: 400 }}>
      <RangeSlider
        label="Price"
        low={low}
        high={high}
        min={0}
        max={200}
        step={5}
        onArcInput={(e) => { setLow(e.detail.low); setHigh(e.detail.high); }}
        onArcChange={(e) => fetchProducts(e.detail.low, e.detail.high)}
      />
    </div>
  );
}

function AudioEqualizer() {
  const [band, setBand] = useState({ low: 200, high: 4000 });

  return (
    <div style={{ maxWidth: 400 }}>
      <RangeSlider
        label="Frequency Band (Hz)"
        low={band.low}
        high={band.high}
        min={20}
        max={20000}
        step={10}
        onArcInput={(e) => setBand({ low: e.detail.low, high: e.detail.high })}
      />
      <p style={{ marginTop: 12, fontSize: 14, color: 'var(--text-muted)' }}>
        Bandwidth: {band.high - band.low} Hz
      </p>
    </div>
  );
}
<script setup>
import { RangeSlider } from '@arclux/arc-ui-vue';
import { ref, computed } from 'vue';

const minAge = ref(18);
const maxAge = ref(65);
const range = computed(() => maxAge.value - minAge.value);
</script>

<template>
  <div style="max-width:400px;">
    <RangeSlider
      label="Age Range"
      :low="minAge"
      :high="maxAge"
      :min="0"
      :max="100"
      @arc-input="minAge = $event.detail.low; maxAge = $event.detail.high"
    />
    <p style="margin-top:12px; font-size:14px; color:var(--text-muted);">
      Span: {{ range }} years
    </p>
  </div>
</template>
<script>
  import { RangeSlider } from '@arclux/arc-ui-svelte';

  let low = 25;
  let high = 75;

  function onInput(e) {
    low = e.detail.low;
    high = e.detail.high;
  }
</script>

<div style="max-width:400px;">
  <RangeSlider
    label="Score Range"
    {low}
    {high}
    min={0}
    max={100}
    on:arc-input={onInput}
    on:arc-change={() => console.log('Committed:', low, '–', high)}
  />
  <p style="margin-top:12px; font-size:14px; color:var(--text-muted);">
    Selected: {low} – {high} ({high - low} points)
  </p>
</div>
import { Component } from '@angular/core';
import { RangeSlider } from '@arclux/arc-ui-angular';

@Component({
  imports: [RangeSlider],
  template: `
    <div style="max-width:400px;">
      <arc-range-slider
        label="Budget"
        [low]="low"
        [high]="high"
        [min]="0"
        [max]="10000"
        [step]="100"
        (arc-input)="onInput($event)"
        (arc-change)="onCommit($event)"
      ></arc-range-slider>

      <p style="margin-top:12px; font-size:14px; color:var(--text-muted);">
        ${{ low | number }}${{ high | number }}
      </p>
    </div>
  `,
})
export class BudgetFilterComponent {
  low = 1000;
  high = 5000;

  onInput(e: CustomEvent) {
    this.low = e.detail.low;
    this.high = e.detail.high;
  }

  onCommit(e: CustomEvent) {
    this.fetchResults(e.detail.low, e.detail.high);
  }

  fetchResults(low: number, high: number) { /* ... */ }
}
import { RangeSlider } from '@arclux/arc-ui-solid';
import { createSignal } from 'solid-js';

function WeightFilter() {
  const [low, setLow] = createSignal(50);
  const [high, setHigh] = createSignal(150);

  return (
    <div style={{ 'max-width': '400px' }}>
      <RangeSlider
        label="Weight (kg)"
        low={low()}
        high={high()}
        min={0}
        max={300}
        step={5}
        onArcInput={(e) => { setLow(e.detail.low); setHigh(e.detail.high); }}
        onArcChange={(e) => console.log('Final:', e.detail.low, '–', e.detail.high)}
      />
      <p style={{ 'margin-top': '12px', 'font-size': '14px', color: 'var(--text-muted)' }}>
        Range: {high() - low()} kg
      </p>
    </div>
  );
}
import { RangeSlider } from '@arclux/arc-ui-preact';
import { useState } from 'preact/hooks';

function DateRangeFilter() {
  const [low, setLow] = useState(2010);
  const [high, setHigh] = useState(2025);

  return (
    <div style={{ maxWidth: 400 }}>
      <RangeSlider
        label="Year Range"
        low={low}
        high={high}
        min={1990}
        max={2030}
        step={1}
        onArcInput={(e) => { setLow(e.detail.low); setHigh(e.detail.high); }}
        onArcChange={(e) => fetchByYear(e.detail.low, e.detail.high)}
      />
      <p style={{ marginTop: 12, fontSize: 14, color: 'var(--text-muted)' }}>
        {high - low} years selected
      </p>
    </div>
  );
}
<!-- arc-range-slider is interactive — requires JS -->
<arc-range-slider></arc-range-slider>
<!-- arc-range-slider is interactive — requires JS -->
<arc-range-slider></arc-range-slider>

API

size 'sm' | 'md' | 'lg' 'md'
Control size. `md` is the default; `sm` and `lg` scale the track and thumbs.
min number 0
Minimum allowed value at the left edge of the track.
max number 100
Maximum allowed value at the right edge of the track.
step number 1
Increment granularity. Values snap to multiples of this number.
low number 0
Lower bound value of the selected range. Reflected as an attribute.
high number 100
Upper bound value of the selected range. Reflected as an attribute.
name string ''
disabled boolean false
Disables interaction, reducing opacity and blocking pointer events.
label string ''
Label text displayed above the slider with the range values shown on the right.
show-values boolean true
Whether to display the numeric "low – high" readout in the header.
formAssociated boolean true
properties object { required: { type: Boolean, reflect: true }, readonly: { type: Boolean, reflect: true }, }
Lit merges static properties up the prototype chain, so every consumer gets these without declaring them. `required` participates in constraint validation below; `readonly` reflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state).
autoValidates boolean true
Components that run their own constraint-validation logic (pattern checks, range checks) opt out of the automatic required sync by overriding this to false, and own the whole validity flag set instead.
form
validity
validationMessage
required boolean false
readonly boolean false

Events

arc-input detail: { low: number, high: number }
Fired continuously as the user drags either thumb. Detail contains `{ low, high }`. Use for real-time filtering or preview.
arc-change detail: { low: number, high: number }
Fired once when the user releases a thumb, indicating the final committed range. Detail contains `{ low, high }`. Use for persisting to a database or triggering an expensive operation.

See Also