Price range React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React price range filter: a histogram of the results over a two-thumb slider, synced min and max inputs, preset chips and a live count.
Drag either thumb along the histogram: the bars inside the range light up, the thumb shows its price in a bubble and the inputs follow, snapped to a round step. Drag a thumb all the way to an end to open that bound (the input empties to its Min or Max placeholder). On a phone the thumbs have 44px hit areas and a vertical swipe over the slider still scrolls the sheet it sits in. Type in Min or Max for an exact amount, even one beyond the slider's ends (the thumb stays pinned at the end); Enter or leaving both fields applies it, and a min above the max swaps. Tap a preset chip to set both ends at once, tap it again to clear the filter; on phones the chips scroll sideways. Keyboard: Tab to a thumb, Left / Right or Up / Down move by one step, Page Up / Page Down (or Shift with an arrow) by ten, Home and End jump the thumb to the start or the end of the track but never past the other thumb (Home on the minimum and End on the maximum open that bound); arrows move between preset chips and Space selects one. The count under the filter changes with a short fade and is read once by screen readers. Under reduced motion the bars appear without growing.
Price range is a Pro React component in the React commerce components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add price-range, together with Slider, Currency Input, Tag, Field, Commerce and 6 more and the npm package motion.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add price-rangePrice range is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import PriceRange from @/components/ui/price-range.
import { useState } from "react"
import { PriceRange, type PriceRangeValue } from "@/components/ui/price-range"
const PRICES = [320, 450, 890, 1150, 1200, 1490, 1650, 2100, 2450, 2900, 3400, 4300, 5200, 7800]
export function PriceFilter({ onApply }: { onApply: (value: PriceRangeValue) => void }) {
const [value, setValue] = useState<PriceRangeValue>([null, 3000])
const count = PRICES.filter((p) => (value[0] === null || p >= value[0]) && (value[1] === null || p <= value[1])).length
return (
<PriceRange
label="Price"
value={value}
onValueChange={setValue}
onValueCommit={onApply}
min={0}
max={8000}
prices={PRICES}
presets={[
{ min: null, max: 500 },
{ min: 500, max: 1500 },
{ min: 1500, max: null },
]}
count={count}
labels={{
results: (n, formatted) => (n === 1 ? "1 listing" : `${formatted} listings`),
}}
/>
)
}Props
valuePriceRangeValueThe controlled range, [min, max] in major units (dollars). null is an open end: [null, 3000] means under $3,000. A value outside the slider pins its thumb to the end while the inputs keep the real number. The type is exported as PriceRangeValue ([min: number | null, max: number | null]), shared with filter-panel.
defaultValuedefault"[null, null]"PriceRangeValueThe starting range while uncontrolled. The default leaves both ends open.
onValueChange(value: PriceRangeValue) => voidFires while dragging (at most once per frame), while typing and on presets. Use it for the live readout.
onValueCommit(value: PriceRangeValue) => voidFires when a thumb is released or moved by a key, when an input is left or confirmed with Enter, and when a preset is tapped. Fetch results here. A min above the max arrives swapped.
mindefault0numberThe slider's low end. A thumb dragged here opens the lower bound.
maxdefault10000numberThe slider's high end. A thumb dragged here opens the upper bound; prices above it count in the last bar. Equal to min it becomes a single price with locked thumbs.
stepdefault0numberThe price step thumbs, arrows and inputs snap to. 0 picks a round one for the range (1, 5, 10, 50, 100, 500...), about a hundredth of it; on a log scale it grows with the price. Currencies without decimals always step in whole units.
scaledefault"linear""linear" | "log"log spreads cheap prices out and squeezes the expensive tail, for wide ranges like real estate ($100 to $500,000). The histogram's buckets follow the same scale.
pricesreadonly number[]Raw prices of the results, counted into the histogram. To send histogram facets from the server instead, count them there with binPrices (see Parts).
histogramreadonly number[]Precomputed bucket counts (server facets), one per equal-width bucket of the track (log-spaced on a log scale). Wins over prices.
binsdefault24numberHow many bars prices are binned into. Empty buckets leave a gap.
showHistogramdefaulttruebooleanThe bars above the slider. Without prices or a histogram there is nothing to draw and it stays hidden.
showInputsdefaulttruebooleanThe Min and Max currency inputs under the slider.
presetsPriceRangePreset[]Chips that set both ends at once: PriceRangePreset = { label?: ReactNode; min: number | null; max: number | null }, e.g. { min: null, max: 500 }. A chip without a label reads labels.under / between / over ("Under $500", "$500 to $1,500"). It stays selected while the value matches it.
countdefault0numberResults in the current range, shown under the filter as labels.results ("832 results") in a polite live region. 0 hides it.
currencydefault"USD"stringISO 4217 code for the inputs, the bubbles and the chips: "$", "€", "lei".
localedefault""stringFormats every price ("$1,500" in en-US, "1.500 $" in de-DE). Empty uses UIProvider, then the page.
labeldefault""ReactNodeThe field label above the histogram; it names the whole group.
descriptiondefault""ReactNodeHelper text under the filter, like "per month".
errordefault""ReactNodeReplaces the description and marks both inputs invalid.
sizedefault"md""sm" | "md" | "lg"Scales the slider (4 / 6 / 8px track), the histogram (40 / 48 / 56px), the inputs (32 / 40 / 48px, 44px on touch for md) and the chips together.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"Colors the slider's range, the lit bars (at 55%) and the selected chip. neutral is black in light mode and white in dark mode.
colordefault""stringAny CSS color; replaces the tone everywhere. Leave empty to use the tone.
disableddefaultfalsebooleanDims the filter and blocks the thumbs, inputs and chips, e.g. while results load.
labelsPartial<PriceRangeLabels>Every string, English by default: min "Min", max "Max", any "Any", thumbMin "Minimum price", thumbMax "Maximum price", under (p) => `Under ${p}`, over (p) => `Over ${p}`, between (a, b) => `${a} to ${b}`, results (n, formatted) => `${formatted} results`, presets "Price presets". Pass only the keys you translate.
classNamesPartial<Record<"histogram" | "bar" | "slider" | "inputs" | "input" | "presets" | "count", string>>Extra classes for the inner parts. className stays on the root (the field wrapper when a label, description or error is set).
Parts
binPrices
functionbinPrices(prices, { min?, max?, bins?, scale? }) returns the histogram counts as number[]: equal-width or log-spaced buckets, prices outside min..max counted in the first or last one. The same counting the component does, for facets computed on the server.
DEFAULT_PRICE_RANGE_LABELS
constantThe English copy: min "Min", max "Max", any "Any", the thumb names, under / over / between preset text and the results count.
PriceRangePreset
typeOne preset chip: { label?, min, max }, with null for an open end. Without a label the chip reads Under, Over or between.
Examples
In a filter sheet
A bottom sheet keeps a draft; the footer shows the live count and applies it.
Real estate in EUR
A log scale from €20,000 to €500,000 with a step of €1,000.
Rent per month
$500 to $5,000 with a caption and no histogram.
Narrow sidebar
Slider and inputs only, size sm, in a 256px filter column.
Open ends
Under $3,000 and over $1,000: one bound left open.
Sizes in the brand tone
sm, md and lg with tone="brand" lighting the bars and the range.
Disabled while loading
Each commit locks the filter until the results come back.
Dependencies
Files
- components/ui/price-range.tsx
FAQ
Is the React Price range component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Price range?
Run npx wingo-ui@latest add price-range. It also adds the Wingo UI items it builds on (Slider, Currency Input, Tag, Field, Commerce and 6 more) and the npm package motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/price-range.
How does Price range behave on phones?
On a phone the thumbs have 44px hit areas and a vertical swipe over the slider still scrolls the sheet it sits in. Tap a preset chip to set both ends at once, tap it again to clear the filter; on phones the chips scroll sideways.
See also
Used in
More React commerce components
Articles using this component
- Nextjs Real Estate Template: Search, Map and Mortgage
Build it: screens and apps10 min read