Size Filter Development for 1C-Bitrix
A size filter in a clothing, footwear, or furniture online store is critically important for UX and directly impacts conversion. Technically it is more complex than a color filter: sizes can be numeric (36, 37, 38), alphabetic (S, M, L, XL), or combined (42/86/176), and size availability is determined at the trade offer (SKU) level, not at the parent product level.
Data Model for Sizes
In most Bitrix stores, size is an attribute of a trade offer (the SKU infoblock), not a property of the main product. Structure:
Product (CATALOG_IBLOCK_ID)
└── Trade Offers (OFFERS_IBLOCK_ID)
├── SIZE = 36, COLOR = Black, QUANTITY = 5
├── SIZE = 37, COLOR = Black, QUANTITY = 0
├── SIZE = 36, COLOR = White, QUANTITY = 3
└── SIZE = 37, COLOR = White, QUANTITY = 12
The filter "show products that have size 37" is a search over offers with PROPERTY_SIZE = 37 AND QUANTITY > 0.
Fetching Available Sizes
function getAvailableSizes(int $catalogIblockId, int $offersIblockId, int $sectionId): array
{
// Fetch all SKUs in this section with size and stock quantity
$rs = \CIBlockElement::GetList(
['PROPERTY_SIZE_VALUE' => 'ASC'],
[
'IBLOCK_ID' => $offersIblockId,
'ACTIVE' => 'Y',
// Filter by parent products from the required section
'PROPERTY_CML2_LINK.SECTION_ID' => $sectionId,
],
['PROPERTY_SIZE'],
false,
['PROPERTY_SIZE_VALUE', 'PROPERTY_SIZE_ENUM_ID', 'CATALOG_QUANTITY']
);
$sizes = [];
while ($offer = $rs->Fetch()) {
$sizeEnumId = $offer['PROPERTY_SIZE_ENUM_ID'];
$sizeValue = $offer['PROPERTY_SIZE_VALUE'];
$qty = (int)$offer['CATALOG_QUANTITY'];
if (!$sizeEnumId || !$sizeValue) continue;
if (!isset($sizes[$sizeEnumId])) {
$sizes[$sizeEnumId] = [
'id' => $sizeEnumId,
'value' => $sizeValue,
'available' => false,
'count' => 0,
];
}
if ($qty > 0) $sizes[$sizeEnumId]['available'] = true;
$sizes[$sizeEnumId]['count']++;
}
return array_values($sizes);
}
Sorting Sizes
Standard alphabetic sorting produces incorrect order: 10, 6, 7, 8, 9 or L, M, S, XL, XS. Custom sorting is required:
// Sorting numeric sizes (footwear, clothing)
function sortSizes(sizes) {
const ORDER_LETTER = ['XXS', 'XS', 'S', 'S/M', 'M', 'M/L', 'L', 'XL', 'XXL', 'XXXL', '3XL', '4XL']
return [...sizes].sort((a, b) => {
const aVal = a.value
const bVal = b.value
// Both numeric — numeric sort
const aNum = parseFloat(aVal)
const bNum = parseFloat(bVal)
if (!isNaN(aNum) && !isNaN(bNum)) return aNum - bNum
// Both alphabetic — sort by predefined order
const aIdx = ORDER_LETTER.indexOf(aVal.toUpperCase())
const bIdx = ORDER_LETTER.indexOf(bVal.toUpperCase())
if (aIdx !== -1 && bIdx !== -1) return aIdx - bIdx
// Otherwise — alphabetical
return aVal.localeCompare(bVal, 'en')
})
}
Vue Component for the Size Filter
<!-- SizeFilter.vue -->
<template>
<div class="size-filter">
<div class="size-filter__label">Size</div>
<!-- Size system switcher (footwear) -->
<div class="size-filter__system" v-if="hasSizeSystem">
<button
v-for="sys in sizeSystems"
:key="sys.id"
:class="{ active: activeSizeSystem === sys.id }"
@click="activeSizeSystem = sys.id"
>{{ sys.label }}</button>
</div>
<div class="size-filter__grid">
<button
v-for="size in sortedSizes"
:key="size.id"
class="size-btn"
:class="{
'size-btn--selected': isSelected(size.id),
'size-btn--unavailable': !size.available,
}"
:disabled="!size.available && !isSelected(size.id)"
@click="toggleSize(size.id)"
:title="!size.available ? 'Out of stock' : `${size.count} products`"
>
{{ size.value }}
</button>
</div>
<div class="size-filter__guide" v-if="showSizeGuide">
<a href="/size-guide/" target="_blank">Size chart →</a>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps(['sizes', 'initialSelected', 'showSizeGuide'])
const emit = defineEmits(['change'])
const selected = ref(props.initialSelected || [])
const activeSizeSystem = ref('eu')
const sortedSizes = computed(() => sortSizes(props.sizes))
function isSelected(id) { return selected.value.includes(id) }
function toggleSize(id) {
if (isSelected(id)) {
selected.value = selected.value.filter(s => s !== id)
} else {
selected.value = [...selected.value, id]
}
emit('change', selected.value)
}
</script>
Filtering with Availability Consideration
Whether to show unavailable sizes (struck through) or hide them depends on UX requirements. Showing unavailable sizes serves as a range guide; hiding them is simpler for the buyer:
// Server-side filter split: in stock / total
public function getSizesAction(int $sectionId): array
{
$allSizes = getAvailableSizes(CATALOG_IBLOCK_ID, OFFERS_IBLOCK_ID, $sectionId);
return [
'all' => $allSizes,
'available' => array_filter($allSizes, fn($s) => $s['available']),
];
}
Applying the Size Filter to the Catalog
public function filterBySizeAction(int $sectionId, array $sizeIds): array
{
if (empty($sizeIds)) {
return $this->getAllProductsAction($sectionId);
}
// Find offers with the required sizes
$parentProductIds = [];
$rs = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => OFFERS_IBLOCK_ID,
'ACTIVE' => 'Y',
'PROPERTY_SIZE' => $sizeIds,
'>CATALOG_QUANTITY' => 0,
],
false, false,
['PROPERTY_CML2_LINK']
);
while ($offer = $rs->Fetch()) {
$parentId = $offer['PROPERTY_CML2_LINK_VALUE'];
if ($parentId) $parentProductIds[] = (int)$parentId;
}
$parentProductIds = array_unique($parentProductIds);
if (empty($parentProductIds)) return ['items' => [], 'total' => 0];
// Fetch parent products
return $this->getProductsByIds($parentProductIds);
}
Size Filter in the Bitrix Smart Filter
The standard smart filter supports filtering by offer properties via the OFFERS_PROPERTY_CODE parameter. This works, but a custom template is still needed to correctly render the size grid.
Delivery Timeline
Size grid with AJAX filtering by SKU and stock availability consideration — 2–3 business days. Full implementation with multiple size systems, a size chart, and synchronization with the color filter — 4–5 business days.







