A recruiting app filters candidates by years of experience. Most sit between 0 and 20, a handful run up to 45, and the slider spans 0 to 45. A standard range slider sets the bounds, and shows nothing about where the candidates actually are. Is the 20 to 45 range busy or nearly empty? The user finds out by filtering and counting rows.
We built a range slider with the distribution drawn on it, so the filter shows its own data. It is a Retool custom component written in TypeScript, published on GitHub under the MIT license.
What the histogram changes
With the distribution drawn above the handles, the experience filter answers the question at a glance: 20 to 45 years is close to empty. Uneven data is common, so the component offers three scales. On a logarithmic scale, a bar of 12,500 and a bar of 1 both stay visible.
Features
Histogram. The data's distribution drawn directly on the slider.
Scales. Linear, logarithmic, or square root, for skewed data.
Range selection. Drag the handles, with live feedback.
Click to select. Click a histogram bar to select its range.
Flexible input. Accepts buckets as an array of objects or an object of arrays, straight from a query.
Custom formatting. A JavaScript function controls how values are displayed.
Negative values. Optionally drawn below the axis for data that crosses zero.
Tooltips. Reposition themselves so they never get cut off.
Styling. Colors, label, and step size are all properties.
Install and deploy
You need Node.js 20 or later and an API access token with read and write scopes for Custom Component Libraries.
git clone https://github.com/StackdropCO/custom-range-slider-retool-component.git
cd custom-range-slider-retool-component
npm install
npx retool-ccl loginDevelop with live sync into your Retool organization, then deploy an immutable version when it is ready:
npm run dev
npm run deployIn the app's Custom Component settings, switch from dev to the version you just published. For self-hosted or multi-instance setups, npx retool-ccl sync distributes the library across instances.
Set up the slider
Drag the Custom Range Slider onto the canvas and set its range properties. Each accepts a static value or a binding.
minandmax, the ends of the slider (defaults 0 and 100).defaultStartanddefaultEnd, the range selected on load.step, the increment.label, shown above the slider.
For the experience filter, that means min 0, max 45, step 1, and a label of "Years of experience".
Feed it the distribution
distributionData takes buckets with a minimum, a maximum, and a count. A query can build them. This Postgres query groups candidates into 5-year buckets:
select
floor(years_experience / 5) * 5 as "min",
floor(years_experience / 5) * 5 + 5 as "max",
count(*) as "count"
from candidates
group by 1, 2
order by 1;The component accepts buckets in either of two shapes. These are the examples from the repository:
[
{ "min": 0, "max": 10, "count": 5 },
{ "min": 10, "max": 20, "count": 8 },
{ "min": 20, "max": 30, "count": 3 }
]{
"bucket_index": [0, 1, 2],
"bucket_min": [0, 10, 20],
"bucket_max": [10, 20, 30],
"count": [5, 8, 3]
}Bind it with {{ getExperienceBuckets.dataArray }}.
Choose a bucket width that matches how people think about the value. Five-year bands suit experience. Price bands might be 10 or 100, depending on the catalog. Buckets much narrower than the slider's step add noise without helping anyone choose. Set histogramScale to logarithmic when one bucket dwarfs the rest, and turn on showNegativeValues if the data crosses zero.
Format the values
formatterFunction takes a JavaScript function as a string. This one labels years:
(v) => {
if (v === 0) return `${v}`;
if (v === 1) return '1 year';
return `${v} years`;
}Currency and dates follow the same pattern. The repository's examples:
// Currency
v => `$${v.toFixed(2)}`
// Unix timestamps as dates, with moment
v => moment.unix(v).format('MMM DD')Use the selected range
The component exposes selectedRange as { start, end } and fires rangeChanged when it moves. Point the filtered query at it:
select *
from candidates
where years_experience between {{ customRangeSlider1.selectedRange.start }}
and {{ customRangeSlider1.selectedRange.end }}
order by years_experience;Then add a rangeChanged handler that runs that query. In JavaScript, the same values destructure directly:
const { start: startValue, end: endValue } = customRangeSlider1.selectedRange;Before production
Pin the app to a published version of the library, never the dev version.
Test with your real distribution, including the empty and skewed ranges.
Check the formatter on the smallest and largest values the data can hold.
Make sure the bucket query and the filtered query read the same table with the same conditions, so the histogram matches the rows.
What is next
On the roadmap, not committed: histograms generated from a plain number[], finer control over bar spacing and styling, optional statistics such as the mean and median, and more scale types.
Our view
We recommend a histogram slider wherever a numeric filter covers skewed data, because users then pick ranges that contain rows. For evenly spread data, Retool's built-in Range Slider is the simpler choice, because it already matches the app's theme.
Issues and pull requests are welcome. More components are in awesome-retool-components, and what is a custom Retool component covers when building one is worth it.