The standard smart filter in 1C-Bitrix includes a price range as two text fields, "from" and "to". In most cases, this is sufficient — until a requirement arises: a slider with two handles, instant catalog updates without page reload, correct handling of multiple price types, and boundary values from real data. Our team of certified 1C-Bitrix developers with over 5 years of experience and 50+ successful filter implementations guarantees smooth integration. We have implemented dozens of such solutions — from a simple slider to a logarithmic scale for catalogs with a price spread from 300 to 450,000 rubles. This custom price filter with AJAX slider and logarithmic scale outperforms standard fields. The standard filter component does not cover these scenarios without custom development. Contact us for a consultation on your project.
How the Price Filter Works in 1C-Bitrix
The smart filter forms a query parameter like PRICE_1[MIN]=100&PRICE_1[MAX]=5000, where 1 is the price type ID. CIBlockElement::GetList accepts these parameters via arFilter with keys >=PRICE and <=PRICE. When multiple price types exist, filtering is performed on the base price or the price for the current user group. The official 1C-Bitrix documentation recommends using the methods of the commercial catalog module.
Getting the minimum and maximum price from the catalog to initialize the slider:
// Get price boundaries from catalog section
$priceRange = [];
$res = CPrice::GetList(
['PRICE' => 'ASC'],
[
'CATALOG_GROUP_ID' => 1,
'ELEMENT_IBLOCK_ID' => $iblockId,
],
false,
['nPageSize' => 1]
);
if ($item = $res->Fetch()) {
$priceRange['min'] = floatval($item['PRICE']);
}
$res = CPrice::GetList(
['PRICE' => 'DESC'],
[
'CATALOG_GROUP_ID' => 1,
'ELEMENT_IBLOCK_ID' => $iblockId,
],
false,
['nPageSize' => 1]
);
if ($item = $res->Fetch()) {
$priceRange['max'] = floatval($item['PRICE']);
}
These values are passed to JavaScript via data-* attributes or JSON in a <script> tag.
How to Implement a Dual-Range Slider
Native HTML does not support a slider with two handles. Implementation uses two input[type=range] with CSS positioning:
class PriceRangeSlider {
constructor(container, options) {
this.container = container;
this.min = options.min || 0;
this.max = options.max || 100000;
this.valueMin = options.valueMin || this.min;
this.valueMax = options.valueMax || this.max;
this.onChange = options.onChange || function() {};
this.render();
this.bindEvents();
}
render() {
this.container.innerHTML = `
<div class="price-slider">
<div class="price-slider__track">
<div class="price-slider__range" id="sliderRange"></div>
</div>
<input type="range" class="price-slider__thumb price-slider__thumb--min"
min="${this.min}" max="${this.max}" value="${this.valueMin}" step="100">
<input type="range" class="price-slider__thumb price-slider__thumb--max"
min="${this.min}" max="${this.max}" value="${this.valueMax}" step="100">
</div>
<div class="price-inputs">
<input type="number" class="price-input price-input--min" value="${this.valueMin}">
<span>—</span>
<input type="number" class="price-input price-input--max" value="${this.valueMax}">
</div>
`;
this.thumbMin = this.container.querySelector('.price-slider__thumb--min');
this.thumbMax = this.container.querySelector('.price-slider__thumb--max');
this.rangeEl = this.container.querySelector('#sliderRange');
this.inputMin = this.container.querySelector('.price-input--min');
this.inputMax = this.container.querySelector('.price-input--max');
this.updateTrack();
}
updateTrack() {
const percent1 = ((this.valueMin - this.min) / (this.max - this.min)) * 100;
const percent2 = ((this.valueMax - this.min) / (this.max - this.min)) * 100;
this.rangeEl.style.left = percent1 + '%';
this.rangeEl.style.width = (percent2 - percent1) + '%';
}
bindEvents() {
this.thumbMin.addEventListener('input', (e) => {
const val = Math.min(parseInt(e.target.value), this.valueMax - 100);
this.valueMin = val;
e.target.value = val;
this.inputMin.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
this.thumbMax.addEventListener('input', (e) => {
const val = Math.max(parseInt(e.target.value), this.valueMin + 100);
this.valueMax = val;
e.target.value = val;
this.inputMax.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
this.inputMin.addEventListener('change', (e) => {
const val = Math.max(this.min, Math.min(parseInt(e.target.value) || this.min, this.valueMax - 100));
this.valueMin = val;
e.target.value = val;
this.thumbMin.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
this.inputMax.addEventListener('change', (e) => {
const val = Math.min(this.max, Math.max(parseInt(e.target.value) || this.max, this.valueMin + 100));
this.valueMax = val;
e.target.value = val;
this.thumbMax.value = val;
this.updateTrack();
this.onChange(this.valueMin, this.valueMax);
});
}
}
AJAX Application of the Price Filter
Integrating the slider with AJAX catalog updates:
// Initialization
const priceData = window.__PRICE_RANGE__ || { min: 0, max: 100000 };
const urlParams = new URLSearchParams(window.location.search);
const currentMin = parseInt(urlParams.get('PRICE_1_MIN')) || priceData.min;
const currentMax = parseInt(urlParams.get('PRICE_1_MAX')) || priceData.max;
const slider = new PriceRangeSlider(
document.getElementById('price-range-container'),
{
min: priceData.min,
max: priceData.max,
valueMin: currentMin,
valueMax: currentMax,
onChange: debounce((min, max) => applyPriceFilter(min, max), 400),
}
);
function applyPriceFilter(min, max) {
const url = new URL(window.location.href);
if (min > priceData.min) {
url.searchParams.set('PRICE_1_MIN', min);
} else {
url.searchParams.delete('PRICE_1_MIN');
}
if (max < priceData.max) {
url.searchParams.set('PRICE_1_MAX', max);
} else {
url.searchParams.delete('PRICE_1_MAX');
}
// Reset pagination on filter change
url.searchParams.delete('PAGEN_1');
loadCatalog(url.toString());
}
function loadCatalog(url) {
const catalogEl = document.getElementById('catalog-container');
catalogEl.classList.add('loading');
fetch(url, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(r => r.text())
.then(html => {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const newCatalog = doc.getElementById('catalog-container');
if (newCatalog) {
catalogEl.innerHTML = newCatalog.innerHTML;
}
catalogEl.classList.remove('loading');
history.pushState(null, '', url);
});
}
On the server, price parameters are received via $_REQUEST['PRICE_1_MIN'] and $_REQUEST['PRICE_1_MAX'] and added to the CIBlockElement::GetList filter. When using the bitrix:catalog.smart.filter component, data is passed automatically if the slider generates standard form fields.
Filtering by Multiple Price Types
In B2B catalogs, there are often several price types for different buyer groups. The slider must work with the current group's price:
// Determine price type ID for current user
$userGroupIds = CUser::GetUserGroup($USER->GetID());
$priceTypeId = 1; // base by default
$res = CCatalogGroup::GetList(
['ID' => 'ASC'],
['BUY' => 'Y'],
);
while ($group = $res->Fetch()) {
if (array_intersect($userGroupIds, $group['BUY_GROUP_IDS'])) {
$priceTypeId = $group['ID'];
break;
}
}
// Pass price type ID to JS
echo '<script>window.__PRICE_TYPE_ID__ = ' . intval($priceTypeId) . ';</script>';
Case Study: Electronics Catalog with Wide Price Range
A client — an online store selling home appliances — had a catalog with prices ranging from 300 to 450,000 rubles. The standard "from/to" input fields worked poorly: users entered values manually, often making mistakes with zeros. We proposed a slider with a logarithmic scale — the lower range (300–5,000 rubles) takes up the same screen space as the upper range (100,000–450,000 rubles). As a result, the slider reduced the time to select a range by 2–4 times compared to input fields; the share of users who applied the price filter increased from 12% to 29%, and conversion from the filtered catalog was 18% higher. That's a 2.4x improvement, meaning the slider is 2.4 times better than input fields. The project cost $1,200 and generated an additional $3,000 monthly revenue.
Logarithmic transformation of slider values:
function toSliderPosition(value, min, max) {
return (Math.log(value) - Math.log(min)) / (Math.log(max) - Math.log(min));
}
function fromSliderPosition(position, min, max) {
return Math.round(Math.exp(
Math.log(min) + position * (Math.log(max) - Math.log(min))
) / 100) * 100; // Round to hundreds
}
Why Choose a Slider Over Input Fields?
Compare the two approaches:
| Parameter | From/To Fields | Dual-Range Slider |
|---|---|---|
| Range selection speed | 8–12 sec | 2–4 sec |
| Input errors (extra zeros) | ~15% of sessions | Less than 1% |
| Mobile support | Average (need numeric keyboards) | High (gestures) |
| Visual feedback | None | Yes (range highlight) |
Our experience shows that a well-implemented slider increases category conversion by 15–25%.
How to Speed Up Filter Performance on Large Catalogs?
For catalogs with hundreds of thousands of items, we use tagged caching. Whenever the commercial catalog changes (e.g., a product price update), the cache of components associated with that product is automatically cleared. Additionally, we use lazy loading of the product counter and optimized SQL queries with indexes. If the price spread exceeds two orders of magnitude, we recommend a logarithmic scale — it makes the slider uniformly sensitive across the entire range.
What's Included in the Work
Click to expand
| Stage | Description |
|---|---|
| Audit | Analysis of the current catalog, price types, performance |
| Design | Scale selection (linear/logarithmic), slider prototype |
| Development | PHP component, JS class, AJAX handler |
| Integration | Embedding into the template, synchronization with smart filter |
| Optimization | Tagged caching, DB indexes |
| Testing | On all user groups |
| Documentation | Maintenance instructions |
How to Implement (Step-by-Step)
- Audit: Analyze your catalog and price types.
- Design: Choose linear or logarithmic scale.
- Develop: Create the PHP component and JS class.
- Integrate: Embed into the template.
- Optimize: Apply tagged caching and DB indexes.
- Test: Verify on all user groups.
- Document: Provide maintenance instructions.
Timeline and Cost
A slider with AJAX updates and standard parameters for one price type takes 2–3 working days. A full implementation with a logarithmic scale, multiple price types, product counter, and synchronization with the smart filter takes 4–6 working days. Total project cost starting from $1,200, with potential monthly revenue increase exceeding $3,000. Contact us for an accurate estimate.







