Components · Forms

Range slider

A slider lets the user pick a value — or a span of values — by dragging a thumb along a track. Use it when the choice is one of many on a continuous scale, and seeing the position matters as much as picking it.

Documentedby Derek Fidler

Cash advance

Pick the amount you want advanced against next month's revenue.

0 €

10.000 €

Overview

A slider is the right control when the user is picking a point on a continuous scale and the position itself carries meaning — a cash advance against revenue, a daily auto-debit cap, a risk tier. It makes sense the moment a precise number is overkill (the user wants “around 4 grand”, not “exactly 4.137 €”) but the magnitude still matters. For exact values, reach for a number input.

Two thumbs only when the answer is genuinely a span

A range slider with two thumbs picks bothends of a window — a price filter, a date range, a transaction-size band. Don't use two thumbs for a single value with a default; that's a single slider with a starting position.

Anatomy

Five parts. The label area sits above the track; the range labels sit below. Both are optional in pairs — but the track itself, the fill, and the thumb are always present.

Cash advance

Pick the amount you want advanced.

0 €

10.000 €

  1. Label area

    Inter Tight Medium 16/20 label + optional regular description (16/20 in text.tertiary). Names the question the slider is answering.

  2. Track (unfilled)

    4 px-tall pill at 14% foreground. Spans the full row; the thumb glides along the centre line.

  3. Track (filled)

    Same height, foreground colour. Stretches from 0 to the thumb on a single slider; from thumb-1 to thumb-2 on a range.

  4. Thumb

    16 px circle in foreground. Centred on the value position, sits on top of the track. Hover halo at 8% foreground; focus ring with 2 px offset.

  5. Range labels (optional)

    Min on the left, max on the right. Inter Tight Regular 16/20 in text.tertiary, tabular numerals so units stack cleanly.

Types

Two type variants. The single-thumb slider picks one value on a scale; the two-thumb range picks both ends of a window. Same track, same dimensions — only the thumb count changes.

Single value

One thumb. The user picks a point on a continuous scale.

Daily cap

The most we'll auto-debit toward repayment in any one day.

0 €

2.500 €

Range · two thumbs

Two thumbs. The user picks a span — a min and a max on the same scale.

Transaction size

Filter to transfers that fall inside this range.

0 €

50.000 €

Three pieces of supporting copy can sit around the track: a label (always recommended unless the question is obvious), an inline value display (when the user needs to read the live number as they drag), and the min/max range labels (when the absolute scale matters).

Label + description + range

Cash advance

Pick the amount you want advanced.

0 €

10.000 €

Label only

Cash advance

Inline value display

Cash advance

4.200 €

Adjust the live amount.

0 €

10.000 €

The current value sits to the right of the label, in foreground weight. Use when the user needs to read the value as they drag.

No header

Bare track. Use only inside dense composed surfaces (a filter row, a settings table) where the question is already obvious from surrounding structure.

Continuous and discrete

A continuous slider lets the thumb glide; the value can be any number in the scale. A discrete slider snaps to evenly spaced notches — use it when only specific values are meaningful.

Continuous

Volume

0%

100%

The default. Any value on the scale is valid; the thumb glides.

Discrete · 5 steps

Risk tolerance

Low

High

The thumb snaps to evenly spaced notches. Use when only specific values are meaningful — risk tiers, payment plans, batch sizes.

States

Five visible states. Held still here for review — hover, focus, and active come for free on the real component via CSS pseudo-classes.

  • Default

  • Hover

  • Focus

  • Disabled

  • Error

Hover and focus are halo-only

Pointer hover renders a soft halo at 8% foreground around the thumb; keyboard focus renders a 2 px ring with 2 px offset. The track and thumb fill colours don't change. Don't add a scale transform on the thumb — the halo is enough.

Error

Apply the error tone when the picked value violates a constraint the user can resolve — exceeding a cap, dropping below a minimum, or conflicting with another field. The track and thumb both flip; a single-line helper below the range names the constraint.

Error

Cash advance

Pick the amount you want advanced.

0 €

10.000 €

Maximum advance is 8.000 € against your current revenue.

Apply the error tone when the picked value violates a constraint the user can resolve. The track + thumb both flip; a single-line helper names the limit.

Behavior

  • Click anywhere on the track to jump to that value. The thumb animates to the click position in 150 ms. The whole track is the click target — not just the 16 px thumb.
  • Drag with the cursor or finger. The thumb follows the pointer; the value updates as it moves (controlled state) so any inline value display reflects the live number.
  • Keyboard moves in steps. / move by 1 step; Home and End jump to min and max. Page Up / Page Dn move by 10× step.
  • Range thumbs can't cross. On a two-thumb slider, the lower thumb stops at the upper thumb's position — and vice versa. The two never pass each other; the constraint is enforced before the value updates.
  • Round to a sensible step. Even on a continuous slider, the user almost never wants fractional values. Round to the nearest 1, 5, 10, or 100 depending on the scale; the input feels smooth without committing to noise.

Slider vs. input vs. select

The slider, the number input, and the select all answer “pick a value” questions, but they earn their place in different contexts.

  • Range slider

    The position on the scale matters; the user's aiming for an approximate value (“around half”) and seeing the magnitude is part of the answer.

  • Number input

    The exact value matters and the user knows it — invoice amount, product weight, retry count. Pair with stepper buttons if the user often nudges the value.

  • Select

    The values are discrete and named — country, currency, plan tier with its own marketing label. Don't use a discrete slider when the steps deserve names.

  • Money field

    Currency-aware input with the system's formatting ( European separators, prefix/suffix per locale). Use this for exact money values; pair with a slider only when both are useful.

Accessibility

  • Native input where possible. <input type="range"> inherits keyboard, focus, value reporting, and aria-valuenow announcements for free. Wrap it for visual styling; don't replace it.
  • Label association. Every slider needs an accessible name — either a wrapping <label> element or aria-labelledby pointing at the visible label area.
  • aria-valuetext when units matter. The default aria-valuenow announces a bare number. When the value is currency, percent, or anything formatted, set aria-valuetextto the formatted string — “4.200 €”, not just “4200”.
  • Range:use two inputs, each with its own accessible name (“Minimum”, “Maximum”). A single role="group" wrapper with a shared legend describes the field as a whole.
  • Focus order:on a range, the lower thumb tabs first, then the upper. Don't reverse the order or skip the lower thumb when it's at the start.
  • Touch target:the visible thumb is 16 px; the actual hit area extends to the full track height (44 px+) so touch users don't have to thread the needle.
  • Reduced motion: the click-to-jump animation is a 150 ms transform. Honour prefers-reduced-motion — snap the thumb instead of animating.

Code

One RangeSlider component, one props shape. Single-thumb is the default; pass value as a tuple to render the two-thumb range.

tsx

import { RangeSlider } from "@flatpay-dk/ui";

// Single value
const [advance, setAdvance] = useState(4200);
<RangeSlider
  label="Cash advance"
  description="Pick the amount you want advanced."
  value={advance}
  onValueChange={setAdvance}
  min={0}
  max={10000}
  step={100}
  format={(v) => new Intl.NumberFormat("da-DK", {
    style: "currency",
    currency: "EUR",
    maximumFractionDigits: 0,
  }).format(v)}
/>

// Discrete steps
<RangeSlider
  label="Risk tolerance"
  value={tier}
  onValueChange={setTier}
  min={0}
  max={4}
  step={1}
  showRangeLabels
  rangeLabels={["Low", "High"]}
/>

// Two-thumb range
const [bounds, setBounds] = useState([10, 70]);
<RangeSlider
  label="Transaction size"
  description="Filter to transfers inside this range."
  value={bounds}
  onValueChange={setBounds}
  min={0}
  max={50000}
  step={500}
  format={(v) => `${v.toLocaleString("da-DK")} €`}
/>

// Inline value display + error
<RangeSlider
  label="Cash advance"
  value={advance}
  onValueChange={setAdvance}
  min={0}
  max={10000}
  showValue
  error={advance > maxAllowed
    ? "Maximum advance is 8.000 € against your current revenue."
    : undefined}
/>

Best practices

Sliders are unfamiliar in dense product UI. The defaults below keep them legible without surprising the user.

Cash advance

0 €

10.000 €

Do

Show the live value somewhere — inline next to the label, or as a min/max range below the track. The slider is for approximation; the number anchors it.

Don't

Don't ship a bare track. Without a label, range labels, or live value, the user has no idea what they're moving.

Cash advance

0 €

10.000 €

Step: 100 € · current: 4.200 €

Do

Round to a sensible step. The user picks 4.200 €, not 4.137,82 €. Steps of 100 / 500 / 1.000 fit currency; 1 / 5 / 10 fit counts.

Cash advance

0 €

10.000 €

Step: 0,01 € · current: 4.137,82 €

Don't

Don't snap to fractional pennies. A continuous slider that reports 4.137,82 € reads as broken; the user wanted approximate.

Transaction size

0 €

50.000 €

Do

Use two thumbs for genuine spans — a min and a max on the same scale, like a price filter.

Invoice amount

0 €

10.000 €

Pick exactly 1.247,50 € from a slider? Hard.

Don't

Don't use a slider for an exact value the user already knows. Invoice amounts, retry counts, fixed payment values — those want a number input.

Props

PropTypeDefaultDescription
valuenumber | [number, number]Current value (single thumb) or [min, max] tuple (range). Make it controlled; the slider is never internally uncommitted.
onValueChange(value: number | [number, number]) => voidFires on every move (drag, click-to-jump, keyboard step). Receives the value(s), not the event.
labelstringQuestion the slider is answering. Inter Tight Medium 16/20. Required unless aria-labelledby is set externally.
descriptionstringOptional supporting copy below the label. Inter Tight Regular 16/20 in text.tertiary.
minnumber0Lowest value. Surfaces as the left range label when showRangeLabels is on.
maxnumber100Highest value. Surfaces as the right range label.
stepnumber1Snap increment. The thumb rounds to the nearest step; keyboard ←/→ moves by step, Page Up/Dn by 10× step.
showRangeLabelsbooleantrueRenders min and max labels below the track in tabular numerals. Disable when surrounding context already implies the scale.
rangeLabels[string, string]Override the default min/max labels with named tier strings — e.g. ['Low', 'High'] on a discrete risk slider.
showValuebooleanfalseRenders the current value to the right of the label, in foreground weight. Use when the user reads the live value as they drag.
format(value: number) => stringFormatter used for the inline value display, range labels (when overridden), and aria-valuetext announcements. Pair with Intl for currency / percent.
errorstringError message rendered below the range labels. Track + thumb flip to the danger tone.
disabledbooleanfalseRemoves from the tab order and dims the track + thumb to 40% foreground.
namestringForm field name. Required when the slider submits as part of a form.