Price Range Filtering for 1C-Bitrix
The standard 1C-Bitrix smart filter includes a price range as two text fields — "from" and "to". In most cases this is sufficient — until requirements emerge: a dual-handle slider, instant catalog updates without page reload, correct handling of multiple price types, and boundary values drawn from real data. The standard filter component does not cover these scenarios without custom development.
How Price Filtering Works in Bitrix
The smart filter builds a query parameter in the form PRICE_1[MIN]=100&PRICE_1[MAX]=5000, where 1 is the price type ID. CIBlockElement::GetList accepts these parameters via arFilter with the keys >=PRICE and <=PRICE. With multiple price types, filtering is applied against the base price or the price for the current user's group.
Retrieving the minimum and maximum price from the catalog to initialize the slider:
// Retrieving price boundary values from a 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.
Implementing a Dual-Range Slider
Native HTML does not support a slider with two handles. Implementation using two input[type=range] elements 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);
});
}
Server-Side Price Parameter Handling
In the catalog component or filter template — receiving parameters and passing them to CIBlockElement::GetList:
// catalog component template.php
$filterPrice = [];
if (!empty($_REQUEST['PRICE_1_MIN'])) {
$filterPrice['>=MIN_PRICE'] = floatval($_REQUEST['PRICE_1_MIN']);
}
if (!empty($_REQUEST['PRICE_1_MAX'])) {
$filterPrice['<=MAX_PRICE'] = floatval($_REQUEST['PRICE_1_MAX']);
}
// Merge with the main filter
$arFilter = array_merge($arFilter, $filterPrice);
When using the smart filter component (bitrix:catalog.smart.filter), the parameters are handled automatically if the slider submits values via the standard filter form fields.
Filtering by Multiple Price Types
B2B catalogs often have multiple price types for different buyer groups. The slider must work with the current group's price:
// Determining the price type ID for the current user
$userGroupIds = CUser::GetUserGroup($USER->GetID());
$priceTypeId = 1; // base price 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
An appliance online store had a catalog with prices ranging from 300 to 450,000. The standard "from/to" input fields worked poorly: users typed values manually and made mistakes with zeros. A slider with a logarithmic scale solved the problem — the lower range (300–5,000) occupied as much screen space as the upper range (100,000–450,000).
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
}
After implementation, the share of users who applied the price filter rose from 12% to 29%, and conversion from filtered catalog pages was 18% higher than from unfiltered pages.
Displaying Product Count
Dynamic counter update without catalog reload:
// Ajax method for retrieving count
if ($_REQUEST['action'] === 'price_count') {
$min = floatval($_REQUEST['min']);
$max = floatval($_REQUEST['max']);
$res = CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => $iblockId,
'ACTIVE' => 'Y',
'>=PRICE' => $min,
'<=PRICE' => $max,
],
[]
);
$count = $res->SelectedRowsCount();
header('Content-Type: application/json');
echo json_encode(['count' => $count]);
die();
}
JavaScript requests the counter with a 600 ms debounce while the slider handle moves and updates the "Show N products" button.
Timeline
Slider with AJAX updates and standard parameters for a single price type — 2–3 business days. Full implementation with logarithmic scale, multiple price types, product counter, and synchronization with the smart filter — 4–6 business days.







