Development of Countdown Timer (Promotion Timer) Functionality for 1C-Bitrix
Promotion without deadline — that's not a promotion, that's permanent discount. Countdown timer creates sense of limited offer and pushes toward decision. But Bitrix has no native countdown component. Standard discount mechanism can set action period, but visual timer on frontend — task for custom development. Let's link timer to real discount rules so end date comes from database, not hardcoded in template.
Data Source: Where to Get End Date
Timer must show real action deadline, not decorative numbers. In Bitrix, action dates stored in several places:
Catalog discounts — table b_catalog_discount, fields ACTIVE_FROM and ACTIVE_TO. Get via \Bitrix\Catalog\DiscountTable::getList() with filter ACTIVE = Y and ACTIVE_TO > NOW().
Cart rules — table b_sale_discount, similar fields. API: \Bitrix\Sale\Internals\DiscountTable::getList().
Product property — can create iblock property PROMO_END_DATE type "Date/Time" and fill manually or automatically when binding product to promotion.
Source choice depends on scenario. If timer shown on product card — property or catalog discount convenient. If global timer (main banner) — cart rule or separate iblock of promotions.
Component Architecture
Create custom component local:sale.countdown in /local/components/local/sale.countdown/. Standard structure:
-
class.php— data retrieval logic. -
templates/.default/template.php— HTML markup. -
templates/.default/script.js— JavaScript countdown logic. -
.parameters.php— component tunable parameters.
Component parameters:
| Parameter | Type | Description |
|---|---|---|
| SOURCE_TYPE | list | Source: catalog_discount, sale_discount, iblock_property |
| DISCOUNT_ID | int | Discount ID (for catalog/sale) |
| IBLOCK_ID | int | iblock ID (for product property) |
| ELEMENT_ID | int | Element ID (for product property) |
| PROPERTY_CODE | string | Code of property with end date |
| DISPLAY_FORMAT | list | Format: days+hours+minutes+seconds or hours+minutes+seconds |
| ACTION_ON_EXPIRE | list | Action on expiry: hide / show message |
| CACHE_TIME | int | Cache time |
In class.php component gets end date from selected source and passes timestamp to template:
$this->arResult['TIMESTAMP_END'] = (new \Bitrix\Main\Type\DateTime($endDate))->getTimestamp();
JavaScript: Client-Side Countdown
Timer works entirely on client. Server only provides end timestamp. This is principle: AJAX requests every second — pointless load.
Key moment — time synchronization. Client clocks can differ from server. Solution: server passes not just TIMESTAMP_END, but also TIMESTAMP_SERVER — current server time. JavaScript calculates delta and corrects countdown:
const serverNow = parseInt(container.dataset.serverTime);
const clientNow = Math.floor(Date.now() / 1000);
const drift = serverNow - clientNow;
const remaining = endTime - (Math.floor(Date.now() / 1000) + drift);
DOM update every second via setInterval — working variant. But for multiple timers on page (product listing with promotions) better one requestAnimationFrame-cycle updating all timers in single pass.
Action on Expiry. When remaining <= 0, timer shouldn't just stop. Options: hide promotion block, replace "Buy with discount" button with regular one, show "Promotion ended" message. For button change — AJAX request to server to verify discount relevance and rerender price block.
Integration with Composite Cache
Composite cache (auto-composite) Bitrix caches entire HTML page. Timer with server timestamp in HTML breaks cache: each hit unique.
Solution — put timer block in dynamic area. In template.php:
$frame = new \Bitrix\Main\Page\Frame('countdown_' . $this->arParams['DISCOUNT_ID']);
$frame->begin();
// Timer HTML
$frame->end();
Inside dynamic area, HTML updates on each request while rest of page served from cache.
Alternative — don't output timestamp in HTML at all. Instead store in separate endpoint (/ajax/countdown.php), which JavaScript requests once on load. Page fully cached, timer data loaded separately.
Binding to Discount Rules
Timer by itself — only visual. Must sync with real discount rule. If manager extended promotion in admin — timer should update automatically.
For this, component on each cache reset re-requests ACTIVE_TO from discount table. Component cache time set small — 300-600 seconds. Compromise between freshness and load.
Additionally: OnAfterCatalogDiscountUpdate / OnAfterSaleDiscountUpdate handler to clear component cache on discount change. Tagged cache (\Bitrix\Main\Data\TaggedCache) with tag catalog_discount_{ID} solves pointwise.
Timer on Product Listing
Separate task — show timers on catalog page with 20-50 products. Each may have its own promotion with separate deadline. Component called in loop catalog.section — multiple SQL requests.
Optimization: in class.php implement batch mode. Component accepts ELEMENT_IDS array, gets all dates in one query, returns array of timestamps. In catalog.section template — one call instead of N.
Implementation Timeline
| Variant | Contents | Timeline |
|---|---|---|
| Simple timer | One component, one discount, static endpoint | 3-4 days |
| Full solution | Batch mode, composite compatibility, sync with rules, auto cache clear | 7-10 days |







