Development of color filtering with visual display in 1C-Bitrix

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.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • 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
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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.