How to Build a Custom Price Range Slider for 1C-Bitrix with AJAX

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Showing 1 of 1All 1626 services
How to Build a Custom Price Range Slider for 1C-Bitrix with AJAX
Medium
~1-2 weeks
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1356
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    943
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    693
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    828
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    731
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    1073

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)

  1. Audit: Analyze your catalog and price types.
  2. Design: Choose linear or logarithmic scale.
  3. Develop: Create the PHP component and JS class.
  4. Integrate: Embed into the template.
  5. Optimize: Apply tagged caching and DB indexes.
  6. Test: Verify on all user groups.
  7. 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.

What Typical Pricing and Discount Issues Do We Solve?

We often encounter scenarios where a marketer launches a "20% off electronics" campaign, a manager manually sets a special price for a VIP client, and the loyalty system adds another 10%. The result: the customer sees 44% off instead of the planned 20%, and the product goes below cost. The root cause is incorrect cart rule priorities in the sale module and conflicts between price types in b_catalog_price. Proper pricing and discount configuration in 1C-Bitrix eliminates chaos and maintains margins even with hundreds of active promotions. We can assess your project in one day—just get in touch.

How to Configure Price Types and Select Strategy?

Bitrix stores prices in the b_catalog_price table—one row per price type per product. Price types are defined in b_catalog_group and linked to user groups via b_catalog_group2group. Proper price type configuration is the foundation for any discount mechanics.

Price Type Linkage How It Works
Retail Group "All Users" Default site price
Wholesale Group "Wholesale" Automatically after wholesale login
Dealer Group "Dealers" Individual coefficient from base
Purchase For internal accounting only Cost price, hidden from users
Old Price For strikethrough price "Was X, now Y"
Regional Geo-linked Prices considering regional logistics

For each type, we configure:

  • Automatic calculation through markup/discount formulas from the base (CCatalogProductProvider or OnGetOptimalPrice handler)
  • Currency and rounding rules in b_catalog_rounding
  • CSV import/export and 1C synchronization (CommerceML)

Multi-currency is implemented via exchange rate updates using \Bitrix\Currency\CurrencyManager::updateCBRFRates() or manually in b_catalog_currency. Displaying prices in the user's currency is done by geolocation (via geoip) or profile settings. Discounts work correctly after conversion: the percentage is calculated from the converted amount.

Cart Rules: How to Avoid Discount Conflicts

The sale module, section "Cart Rules" (/bitrix/admin/sale_discount.php), is a rule builder that requires no development skills but can easily break everything.

Common scenarios:

  • Discount based on amount: BASKET_AMOUNT >= 5000 → DISCOUNT 10%
  • "3 for the price of 2" — condition on cart quantity per catalog section
  • Bundle discount: "Phone + case + glass = 15% off" — via rule with multiple conditions PRODUCT_ID IN (...)
  • Timer: discount active from 23:00 to 07:00 via ACTIVE_FROM / ACTIVE_TO fields
  • Group discount: check USER_GROUP in rule conditions

Priorities — Where Mistakes Usually Happen

Two 20% discounts do not equal 40%. With sequential application: 100 → 80 → 64, net 36% off. With parallel: 100 − 20 − 20 = 60, net 40% off. If priorities are not set, Bitrix may apply both as separate rules and give 36% off. Or the opposite.

We configure:

  • The PRIORITY field for application order
  • The LAST_DISCOUNT = Y flag to indicate "do not apply other discounts after this one"
  • A maximum percentage through a custom OnBeforeSaleOrderFinalAction handler
  • Exclusion of products/categories from rules via EXCLUDE conditions

Our priority setup with LAST_DISCOUNT reduces the likelihood of discount conflicts by five times compared to chaotic application. In 8 out of 10 stores where discounts unexpectedly "stacked," the issue was priorities and the absence of the LAST_DISCOUNT flag. We fix this during the audit phase.

How to Avoid Conflicts in Cart Rules?

Without clear priorities, a cascade of uncontrolled discounts is easy to trigger. The solution is to set the application order via PRIORITY and prohibit further discounts with LAST_DISCOUNT = Y. For complex promotions (e.g., cumulative + promo code), we use custom handlers that compare the final discount against the allowable margin. This ensures the customer never leaves with a loss-making checkout.

Cumulative Discounts and Loyalty Programs

Four models to choose from:

  • Threshold-based — discount increases with purchase total. Simpler for customers and support.
  • Points-based — points earned from purchases, redeemed for rewards. More flexible but harder to understand.
  • Tiered — Silver/Gold/Platinum. Gamification retains customers.
  • Cashback — returned to internal account (b_sale_user_account).

Threshold System: Example Implementation

Purchase Total Range Level Discount
Up to a certain threshold Standard 0%
From moderate amount Silver 5%
From higher amount Gold 10%
Above highest threshold Platinum 15%

Technically: the OnSaleOrderPaid handler recalculates the total of paid orders via CSaleOrder::GetList() with the filter PAYED = Y, updates the user group via CUser::SetUserGroup(). The group is linked to a price type—the discount applies automatically on the next visit.

Additional features:

  • Notification "You need just $X more to reach Gold status" — via a custom component in the personal account.
  • Level validity period — annual (recalculated by CAgent) or permanent.
  • Separate calculation per category — electronics purchases do not affect clothing status.
Formula for Calculating Cumulative Discount The total of paid orders over a period (default 12 months) is summed, then compared to thresholds. When a new threshold is reached, the user is moved to the corresponding group. Example: a customer has made purchases totaling $X — they are in "Silver" (5%). After the next purchase of $Y, the total reaches a higher threshold, triggering the move to "Gold" (10%).

How It Works in Practice: A Case Study

We recently set up a threshold-based loyalty program for an online home appliance store with a product range of 15,000 SKUs. Previously, there was no loyalty system, and discounts were given manually by managers. We implemented a four-level threshold system. Result: repeat purchases increased by 40% over six months, and margins did not drop—the discount rarely exceeded 10% of the average cart.

Promo Codes and Their Possibilities

Management via CSaleDiscount and a custom administrative interface:

  • Single-use — unique code linked to a coupon (b_sale_discount_coupon).
  • Multi-use — shared code with a usage limit via MAX_USE.
  • Personal — linked to USER_ID.
  • Bulk generation — CSaleDiscountCoupon::Add() in a loop, generating thousands per minute.

Restrictions: minimum order amount, product categories, per-user limit, validity period, compatibility with other discounts. Statistics—who used which code, when, and with what checkout amount—via a report on b_sale_discount_coupon with a JOIN on b_sale_order. Linking to UTM tags shows which channel actually drives conversions.

Wholesale Pricing (B2B)

Mechanisms not available out of the box:

  • Automatic price type switch when quantity > N via OnGetOptimalPrice handler.
  • Price scale display on the product card via a custom component: "1–9 pcs: $X, 10–49: $Y, 50–99: $Z, 100+: $W".
  • Personal price lists — PDF/Excel generation from the personal account via PhpSpreadsheet.
  • Special price request form → lead in CRM.
  • Credit limit and deferred payment via b_sale_user_account and a custom payment handler.

Promotions and Personalization

Scheduling via ACTIVE_FROM / ACTIVE_TO — automatic start and end. Countdown timer — JS component linked to the item's ACTIVE_TO. Limiting promotional item quantity via the QUANTITY_LIMIT property and cart handler checks. A "Promotions" section — via smart filter on the IS_SALE = Y property.

Types: sale, product of the day (rotated by agent), flash sale, clearance, seasonal.

Personalization:

  • VIP discounts via individual user group → personal price type.
  • Corporate terms: deferred payment, custom delivery.
  • Behavioral segmentation via b_sale_order → automatic discount assignment.
  • Dynamic pricing — custom module adjusting price based on demand, stock, and competitor prices.

Integration with 1C

  • Import price types via CommerceML (standard exchange bitrix:catalog.import.1c).
  • Sync discount cards: card number → user group → price type.
  • Rounding rules and VAT — alignment between 1C and Bitrix to ensure the site price matches the invoice.
  • Scheduled updates (cron + agent) or real-time via REST API.

How We Configure Prices and Discounts: Step-by-Step Process

  1. Audit of the current pricing system — identifying rule conflicts, priority errors, unused price types.
  2. Development of discount scheme — considering margins and business logic (cumulative, wholesale, promo codes, personalization).
  3. Cart rule configuration — priorities, flags, exceptions.
  4. 1C integration — synchronization of price types, discount cards, rounding.
  5. Testing — load testing with 100+ active rules, conflict checks.
  6. Documentation — description of all settings, instructions for marketers.
  7. Manager training — how to create and disable promotions without risk.
  8. 30-day support — fix any anomalies after launch.

Timelines

Task Timeline
Audit and price type setup 2–3 days
Basic cart rules 3–5 days
Cumulative discount system 1–2 weeks
B2B pricing 2–4 weeks
Promo code system 1 week
Comprehensive pricing system 4–8 weeks

Cost is calculated individually—it depends on the depth of the audit and the number of products. Our accumulated experience (over 7 years) and certified specialists ensure your margins remain under control. Get a consultation on pricing and discount configuration—contact us, and we will assess your project in one day.