Color Filter with Visual Display for 1C-Bitrix
A color filter is a case where a standard checkbox is categorically inappropriate. The user selects a color visually, not from a text list like "Red, Blue, Green." Swatches (color circles) with proper multi-select handling are custom development with non-trivial details: multi-select, highlighting unavailable colors, and correct integration with trade offers (SKUs).
Storing Color Data
Color in Bitrix is stored in two ways:
List property with an attached image. In b_iblock_property_enum, each value (Red, Blue) can have an attached image file via b_iblock_property_enum.FILE_ID. This is the native Bitrix mechanism for visual swatches.
UF property with a HEX code. A string-type property with the code COLOR_HEX, where the value is #FF0000. Easier to fill in manually, requires no image uploads. Display is handled via CSS background.
Populating Color Data in the Enumeration
// Adding a list property value with color (HEX via XML_ID or description)
\CIBlockPropertyEnum::Add([
'PROPERTY_ID' => $colorPropertyId,
'VALUE' => 'Red',
'XML_ID' => '#E53935', // HEX code in XML_ID — non-standard but handy trick
'SORT' => 10,
'DEF' => 'N',
]);
Color Filter Template
<!-- ColorFilter.vue -->
<template>
<div class="color-filter">
<div class="color-filter__label">Color</div>
<div class="color-filter__swatches">
<button
v-for="color in colors"
:key="color.id"
class="color-swatch"
:class="{
'color-swatch--selected': isSelected(color.id),
'color-swatch--unavailable': !color.available,
'color-swatch--light': isLightColor(color.hex),
}"
:style="getSwatchStyle(color)"
:title="color.name + (color.count ? ` (${color.count})` : '')"
:disabled="!color.available"
@click="toggleColor(color.id)"
:aria-label="color.name"
:aria-pressed="isSelected(color.id)"
>
<span class="color-swatch__check" v-if="isSelected(color.id)">✓</span>
<span class="color-swatch__cross" v-if="!color.available">×</span>
</button>
</div>
<div class="color-filter__selected" v-if="selectedColors.length">
<span v-for="id in selectedColors" :key="id" class="selected-tag">
{{ getColorName(id) }}
<button @click="removeColor(id)">×</button>
</span>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const props = defineProps(['colors', 'initialSelected'])
const emit = defineEmits(['change'])
const selectedColors = ref(props.initialSelected || [])
function isSelected(id) {
return selectedColors.value.includes(id)
}
function toggleColor(id) {
if (isSelected(id)) {
selectedColors.value = selectedColors.value.filter(c => c !== id)
} else {
selectedColors.value = [...selectedColors.value, id]
}
emit('change', selectedColors.value)
}
function removeColor(id) {
selectedColors.value = selectedColors.value.filter(c => c !== id)
emit('change', selectedColors.value)
}
function getSwatchStyle(color) {
if (color.image) {
return { backgroundImage: `url(${color.image})`, backgroundSize: 'cover' }
}
return { backgroundColor: color.hex || '#cccccc' }
}
function isLightColor(hex) {
if (!hex) return false
const r = parseInt(hex.slice(1, 3), 16)
const g = parseInt(hex.slice(3, 5), 16)
const b = parseInt(hex.slice(5, 7), 16)
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255
return luminance > 0.7
}
function getColorName(id) {
return props.colors.find(c => c.id === id)?.name || ''
}
</script>
Fetching Colors with Images from an Infoblock
function getColorsForFilter(int $iblockId, int $sectionId): array
{
$propertyCode = 'COLOR';
// Fetch enumeration values with files
$enumValues = [];
$rsEnum = \CIBlockPropertyEnum::GetList(
['SORT' => 'ASC'],
['PROPERTY_ID' => getPropertyIdByCode($iblockId, $propertyCode)]
);
while ($enum = $rsEnum->Fetch()) {
$imageUrl = null;
if ($enum['FILE_ID']) {
$imageUrl = \CFile::GetPath($enum['FILE_ID']);
}
$enumValues[$enum['ID']] = [
'id' => $enum['ID'],
'name' => $enum['VALUE'],
'hex' => $enum['XML_ID'], // using XML_ID as HEX
'image' => $imageUrl,
'count' => 0,
];
}
// Count products per color in the section
$rs = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => $iblockId,
'ACTIVE' => 'Y',
'SECTION_ID' => $sectionId,
'INCLUDE_SUBSECTIONS' => 'Y',
],
["PROPERTY_{$propertyCode}"],
false,
["PROPERTY_{$propertyCode}_ENUM_ID"]
);
while ($el = $rs->Fetch()) {
$enumId = $el["PROPERTY_{$propertyCode}_ENUM_ID"];
if ($enumId && isset($enumValues[$enumId])) {
$enumValues[$enumId]['count']++;
}
}
// Remove colors with no products
return array_values(array_filter($enumValues, fn($c) => $c['count'] > 0));
}
CSS for Swatches
.color-swatch {
width: 32px;
height: 32px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
position: relative;
transition: transform 0.1s, border-color 0.1s;
}
.color-swatch:hover {
transform: scale(1.15);
}
.color-swatch--selected {
border-color: #333;
transform: scale(1.1);
}
.color-swatch--light.color-swatch--selected {
border-color: #666;
}
.color-swatch--unavailable {
opacity: 0.4;
cursor: not-allowed;
}
.color-swatch--unavailable::after {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: rgba(0,0,0,0.5);
transform: rotate(-45deg);
}
.color-swatch__check {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
color: white;
text-shadow: 0 0 2px rgba(0,0,0,0.8);
}
Color Filter for Trade Offers (SKUs)
If color is an attribute of a SKU rather than the parent product, an intermediate step is required: first find offers of the desired color, then retrieve the parent products:
$offersInColor = [];
$rs = \CIBlockElement::GetList(
[],
[
'IBLOCK_ID' => OFFERS_IBLOCK_ID,
'ACTIVE' => 'Y',
'PROPERTY_COLOR' => $selectedColorIds, // array of enumeration value IDs
],
false, false,
['ID', 'PROPERTY_CML2_LINK']
);
while ($offer = $rs->Fetch()) {
$offersInColor[] = (int)$offer['PROPERTY_CML2_LINK_VALUE'];
}
$filter['ID'] = array_unique($offersInColor);
Delivery Timeline
Color swatches with checkboxes, AJAX updates, and unavailable color states — 2–3 business days. Full implementation including SKU logic, texture images, and mobile adaptation — 3–5 business days.







