Пошаговое создание кастомного селекта на React и CSS с примерами
Перейти

Пошаговое создание кастомного селекта на React и CSS с примерами

#CSS и верстка  #React  #Фронтенд CSS  
Пройдите тест, узнайте какой профессии подходите
Сколько вам лет
0%
До 18
От 18 до 24
От 25 до 34
От 35 до 44
От 45 до 49
От 50 до 54
Больше 55

Для кого эта статья:

  • Фронтенд-разработчики, работающие с React
  • Дизайнеры и специалисты по UX/UI, заинтересованные в создании кастомных интерфейсов
  • Технические директора и менеджеры проектов, стремящиеся улучшить пользовательский опыт в приложениях

Нативный select выглядит одинаково унылым в любом браузере, и с ним невозможно создать по-настоящему стильный интерфейс. Как разработчики, мы часто сталкиваемся с необходимостью полностью контролировать внешний вид и функциональность селекта, особенно в сложных React-приложениях. Создание кастомного селекта – это не просто дань трендам UI/UX, это способ обеспечить полный контроль над пользовательским опытом. Давайте разберем, как создать компонент, который будет выглядеть именно так, как нужно вашему проекту, и при этом сохранит всю функциональность, к которой привыкли пользователи. 🚀

Почему стоит создавать custom select в React проектах

Стандартные HTML-селекты обладают серьезными ограничениями, которые мешают создавать действительно качественные интерфейсы. Нативные выпадающие списки сложно стилизовать последовательно между браузерами, они не поддерживают расширенную функциональность, и их внешний вид трудно согласовать с современным дизайном приложений.

Александр Петров, ведущий фронтенд-разработчик

Однажды наша команда работала над крупной CRM-системой с десятками форм. Заказчик требовал уникальный дизайн всех элементов управления, включая выпадающие списки с анимациями и кастомными иконками. Мы попытались использовать нативные select-элементы с CSS-стилизацией, но столкнулись с кошмаром кросс-браузерной совместимости. В Chrome всё выглядело прилично, в Firefox уже похуже, а в Safari... это был просто провал. После дней фрустрации мы решили создать собственный компонент кастомного селекта на React. Это потребовало дополнительного времени на разработку, но полностью оправдало себя — компонент работал идентично во всех браузерах и поддерживал все требования дизайна, включая сложные анимации и группировку опций.

Давайте сравним стандартный select и кастомное решение:

Характеристика Нативный HTML select Custom select на React
Кросс-браузерная совместимость стилей Низкая Высокая
Возможности кастомизации внешнего вида Ограниченные Полные
Поддержка сложных операций (группировка, поиск) Минимальная или отсутствует Неограниченная
Контроль над поведением Ограниченный Полный
Возможности для анимации Практически отсутствуют Неограниченные

Основные преимущества кастомного селекта на React:

  • 🎨 Полная визуальная кастомизация — от стилей контейнера до отдельных опций
  • 🔄 Программное управление состоянием — можно создавать сложную логику работы компонента
  • 🧩 Интеграция с экосистемой React — легко вписывается в архитектуру приложения
  • Расширяемая функциональность — мультивыбор, поиск, группировка, виртуализация и др.
  • 🌐 Единое поведение во всех браузерах — пользовательский опыт предсказуем

Использование кастомного селекта становится особенно обоснованным, когда ваш проект:

  • Имеет строгие требования к дизайну
  • Содержит сложные формы с зависимостями между полями
  • Требует специфических интерактивных паттернов
  • Должен обеспечивать безупречный UX на всех платформах
Пошаговый план для смены профессии

Проектирование структуры React компонента для селекта

Прежде чем приступить к кодированию, важно продумать архитектуру компонента. Хорошо спроектированный кастомный селект должен быть гибким, доступным и легко интегрируемым.

Базовая структура нашего компонента будет состоять из трех основных частей:

  1. Контейнер селекта — основной видимый элемент, который пользователь нажимает для открытия списка
  2. Выпадающий список — контейнер для отображения опций
  3. Опции — отдельные элементы списка, которые можно выбрать

Вот простая структура файлов для нашего кастомного селекта:

  • CustomSelect/
  • index.jsx — основной компонент
  • SelectOption.jsx — компонент для отдельной опции
  • styles.css — стили компонента
  • types.js — типы данных (если используется TypeScript)

Начнем с создания базовой структуры компонента:

jsx
Скопировать код
// CustomSelect/index.jsx
import React, { useState, useRef, useEffect } from 'react';
import SelectOption from './SelectOption';
import './styles.css';

const CustomSelect = ({ 
options, 
defaultValue = null,
placeholder = 'Выберите опцию...',
onChange,
disabled = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(defaultValue);
const selectRef = useRef(null);

// Логика закрытия списка при клике вне компонента
useEffect(() => {
const handleClickOutside = (event) => {
if (selectRef.current && !selectRef.current.contains(event.target)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

// Обработчик выбора опции
const handleOptionSelect = (option) => {
setSelectedOption(option);
setIsOpen(false);
if (onChange) {
onChange(option);
}
};

return (
<div 
className={`custom-select ${disabled ? 'disabled' : ''}`}
ref={selectRef}
>
<div 
className={`select-header ${isOpen ? 'open' : ''}`}
onClick={() => !disabled && setIsOpen(!isOpen)}
>
<span className="selected-value">
{selectedOption ? selectedOption.label : placeholder}
</span>
<span className={`arrow ${isOpen ? 'up' : 'down'}`}></span>
</div>

{isOpen && !disabled && (
<ul className="options-container">
{options.map((option) => (
<SelectOption
key={option.value}
option={option}
isSelected={selectedOption?.value === option.value}
onSelect={() => handleOptionSelect(option)}
/>
))}
</ul>
)}
</div>
);
};

export default CustomSelect;

Теперь создадим компонент для отдельных опций:

jsx
Скопировать код
// CustomSelect/SelectOption.jsx
import React from 'react';

const SelectOption = ({ option, isSelected, onSelect }) => {
return (
<li 
className={`option ${isSelected ? 'selected' : ''}`}
onClick={onSelect}
>
{option.label}
</li>
);
};

export default SelectOption;

Эта базовая структура обеспечивает следующие возможности:

  • Отображение выбранного значения или плейсхолдера
  • Открытие/закрытие списка при клике
  • Закрытие списка при клике вне компонента
  • Выбор опции из списка
  • Передача выбранного значения через коллбэк
  • Поддержка неактивного состояния (disabled)

Управление состоянием и обработка событий в custom select

Эффективное управление состоянием — ключ к созданию отзывчивых и предсказуемых интерфейсов. В случае с кастомным селектом необходимо отслеживать несколько важных состояний и реагировать на различные пользовательские события.

Михаил Соколов, React-разработчик

В одном проекте для крупного интернет-магазина мы столкнулись с интересной проблемой: кастомные селекты в фильтрах товаров работали медленно при большом количестве опций (более 500 категорий). При каждом клике компонент перерисовывался полностью, создавая заметные задержки. Мы применили оптимизации с использованием React.memo для отдельных опций и useMemo для фильтрации списка. Также внедрили виртуализацию списка с помощью react-window, чтобы рендерить только видимые опции. Это привело к впечатляющему результату — скорость работы селекта с 500+ опциями стала неотличима от селекта с 10 опциями. Этот опыт показал, насколько важно думать о производительности при управлении состоянием в компонентах с большим объемом данных.

Ключевые состояния, которые необходимо отслеживать в кастомном селекте:

Состояние Тип данных Назначение
isOpen boolean Контролирует, открыт ли выпадающий список
selectedOption object null Хранит информацию о выбранной опции
hoveredIndex number null Отслеживает индекс опции, на которую наведен курсор или которая выбрана с клавиатуры
isFocused boolean Отслеживает, находится ли компонент в фокусе

Расширим наш компонент, добавив клавиатурную навигацию и доступность:

jsx
Скопировать код
// CustomSelect/index.jsx (с расширенным управлением состоянием)
import React, { useState, useRef, useEffect } from 'react';
import SelectOption from './SelectOption';
import './styles.css';

const CustomSelect = ({ 
options,
defaultValue = null,
placeholder = 'Выберите опцию...',
onChange,
disabled = false,
name
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOption, setSelectedOption] = useState(defaultValue);
const [hoveredIndex, setHoveredIndex] = useState(null);
const [isFocused, setIsFocused] = useState(false);

const selectRef = useRef(null);
const optionsRef = useRef([]);

// Сброс индекса наведения при закрытии списка
useEffect(() => {
if (!isOpen) {
setHoveredIndex(null);
} else if (options.length > 0) {
const defaultIndex = selectedOption
? options.findIndex(opt => opt.value === selectedOption.value)
: 0;
setHoveredIndex(defaultIndex >= 0 ? defaultIndex : 0);
}
}, [isOpen, options, selectedOption]);

// Обработчик кликов вне компонента
useEffect(() => {
const handleClickOutside = (event) => {
if (selectRef.current && !selectRef.current.contains(event.target)) {
setIsOpen(false);
}
};

document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

// Обработка клавиатурной навигации
const handleKeyDown = (e) => {
if (disabled) return;

switch (e.key) {
case 'ArrowDown':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setHoveredIndex(prevIndex => 
prevIndex < options.length – 1 ? prevIndex + 1 : 0
);
}
break;

case 'ArrowUp':
e.preventDefault();
if (!isOpen) {
setIsOpen(true);
} else {
setHoveredIndex(prevIndex => 
prevIndex > 0 ? prevIndex – 1 : options.length – 1
);
}
break;

case 'Enter':
case ' ': // Пробел
e.preventDefault();
if (isOpen && hoveredIndex !== null) {
handleOptionSelect(options[hoveredIndex]);
} else {
setIsOpen(true);
}
break;

case 'Escape':
e.preventDefault();
setIsOpen(false);
break;

case 'Tab':
setIsOpen(false);
break;

default:
if (/^[a-zA-Z0-9]$/.test(e.key)) {
const firstMatchIndex = options.findIndex(option => 
option.label.toLowerCase().startsWith(e.key.toLowerCase())
);
if (firstMatchIndex !== -1) {
setHoveredIndex(firstMatchIndex);
if (!isOpen) {
handleOptionSelect(options[firstMatchIndex]);
}
}
}
break;
}
};

// Прокручиваем список к выделенной опции
useEffect(() => {
if (isOpen && hoveredIndex !== null && optionsRef.current[hoveredIndex]) {
optionsRef.current[hoveredIndex].scrollIntoView({
behavior: 'smooth',
block: 'nearest'
});
}
}, [hoveredIndex, isOpen]);

const handleOptionSelect = (option) => {
setSelectedOption(option);
setIsOpen(false);
if (onChange) {
onChange(option);
}
};

const setOptionRef = (element, index) => {
optionsRef.current[index] = element;
};

return (
<div 
className={`custom-select ${disabled ? 'disabled' : ''} ${isFocused ? 'focused' : ''}`}
ref={selectRef}
onKeyDown={handleKeyDown}
tabIndex={disabled ? -1 : 0}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
role="combobox"
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-disabled={disabled}
aria-labelledby={`${name}-label`}
>
<div 
className={`select-header ${isOpen ? 'open' : ''}`}
onClick={() => !disabled && setIsOpen(!isOpen)}
id={`${name}-label`}
>
<span className="selected-value">
{selectedOption ? selectedOption.label : placeholder}
</span>
<span className={`arrow ${isOpen ? 'up' : 'down'}`}></span>
</div>

{isOpen && !disabled && (
<ul 
className="options-container"
role="listbox"
aria-activedescendant={hoveredIndex !== null ? `${name}-option-${hoveredIndex}` : undefined}
>
{options.map((option, index) => (
<SelectOption
key={option.value}
option={option}
isSelected={selectedOption?.value === option.value}
isHovered={index === hoveredIndex}
onSelect={() => handleOptionSelect(option)}
onMouseEnter={() => setHoveredIndex(index)}
ref={el => setOptionRef(el, index)}
id={`${name}-option-${index}`}
role="option"
aria-selected={selectedOption?.value === option.value}
/>
))}
</ul>
)}
</div>
);
};

export default CustomSelect;

Для корректной работы необходимо также обновить компонент опции:

jsx
Скопировать код
// CustomSelect/SelectOption.jsx (с поддержкой ref)
import React, { forwardRef } from 'react';

const SelectOption = forwardRef(({ 
option, 
isSelected, 
isHovered,
onSelect, 
onMouseEnter,
...rest
}, ref) => {
return (
<li 
className={`option ${isSelected ? 'selected' : ''} ${isHovered ? 'hovered' : ''}`}
onClick={onSelect}
onMouseEnter={onMouseEnter}
ref={ref}
{...rest}
>
{option.label}
</li>
);
});

export default SelectOption;

Эти улучшения добавляют следующие возможности:

  • 🎮 Клавиатурная навигация — пользователи могут перемещаться по опциям и выбирать их с помощью клавиш стрелок, Enter и пробела
  • 🔍 Быстрый поиск — введя первый символ, пользователь может быстро найти соответствующую опцию
  • ARIA-атрибуты — для обеспечения доступности компонента
  • 📜 Автоматическая прокрутка — выделенные опции автоматически прокручиваются в видимую область
  • 🎯 Визуальная обратная связь — подсветка активных и наведенных элементов

Стилизация кастомного селекта с помощью CSS

Теперь, когда у нас есть функциональный компонент с хорошо организованной структурой и логикой, добавим стили, чтобы наш кастомный селект выглядел привлекательно и соответствовал современным стандартам дизайна.

Вот базовые стили для нашего компонента:

CSS
Скопировать код
/* CustomSelect/styles.css */
.custom-select {
position: relative;
width: 100%;
max-width: 300px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 16px;
color: #333;
outline: none;
}

/* Стили для основного контейнера */
.select-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 12px;
background-color: #fff;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s ease;
}

.select-header:hover {
border-color: #999;
}

.select-header.open {
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
}

/* Стили для стрелки */
.arrow {
display: inline-block;
width: 0;
height: 0;
margin-left: 8px;
transition: transform 0.2s ease;
}

.arrow.down {
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 5px solid #999;
}

.arrow.up {
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-bottom: 5px solid #4a90e2;
}

/* Контейнер для опций */
.options-container {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 5px;
max-height: 200px;
overflow-y: auto;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 10;
padding: 0;
list-style: none;
animation: fadeIn 0.2s ease;
}

@keyframes fadeIn {
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}

/* Скроллбар для списка опций */
.options-container::-webkit-scrollbar {
width: 8px;
}

.options-container::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}

.options-container::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 4px;
}

.options-container::-webkit-scrollbar-thumb:hover {
background: #999;
}

/* Стили для отдельных опций */
.option {
padding: 8px 12px;
cursor: pointer;
transition: background-color 0.2s ease;
}

.option:hover, .option.hovered {
background-color: #f2f8ff;
}

.option.selected {
background-color: #e6f2ff;
font-weight: 500;
}

/* Стили для состояния фокуса */
.custom-select.focused .select-header {
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.2);
}

/* Стили для неактивного состояния */
.custom-select.disabled {
opacity: 0.6;
pointer-events: none;
}

.custom-select.disabled .select-header {
background-color: #f5f5f5;
cursor: not-allowed;
}

/* Стили для плейсхолдера */
.selected-value {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: #333;
}

.custom-select:not(.has-value) .selected-value {
color: #999;
}

Эти стили обеспечивают следующие визуальные аспекты:

  • 📐 Современный минималистичный дизайн — чистые линии, тонкие рамки, сдержанные цвета
  • 💫 Плавные анимации — для более приятного взаимодействия
  • Визуальная обратная связь — состояния наведения, выбора, фокуса
  • 📱 Адаптивный дизайн — корректное отображение на разных устройствах
  • 🧩 Стилизованный скроллбар — для более эстетичного вида при прокрутке большого списка

Для создания более уникальных дизайнов можно применять различные стилистические решения:

  1. Материальный дизайн — с характерными тенями, анимациями и рипл-эффектами
  2. Неоморфизм — с объемными элементами, создающими иллюзию выдавленных или утопленных компонентов
  3. Стекломорфизм — с эффектом полупрозрачного стекла и размытия

Пример модификации стилей под материальный дизайн:

CSS
Скопировать код
/* Дополнительные стили для материального дизайна */
.custom-select.material .select-header {
border: none;
border-bottom: 1px solid #ccc;
border-radius: 0;
padding: 12px 16px;
box-shadow: none;
transition: border-bottom-color 0.2s;
}

.custom-select.material .select-header:hover,
.custom-select.material.focused .select-header {
border-bottom-color: #4a90e2;
}

.custom-select.material .select-header.open {
box-shadow: none;
}

.custom-select.material .options-container {
margin-top: 0;
border-radius: 0 0 4px 4px;
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
}

.custom-select.material .option {
padding: 12px 16px;
}

.custom-select.material .option.selected {
background-color: rgba(74, 144, 226, 0.1);
}

Расширение функционала: мультивыбор и фильтрация опций

Базовый кастомный селект отлично справляется с простыми сценариями, но на практике часто требуется расширенная функциональность. Добавим поддержку множественного выбора и фильтрацию опций по мере ввода текста.

Сначала модифицируем наш компонент для поддержки мультивыбора:

jsx
Скопировать код
// CustomSelect/index.jsx (с поддержкой мультивыбора)
import React, { useState, useRef, useEffect } from 'react';
import SelectOption from './SelectOption';
import './styles.css';

const CustomSelect = ({ 
options,
defaultValue = null,
placeholder = 'Выберите опции...',
onChange,
disabled = false,
name,
multiple = false,
searchable = false
}) => {
const [isOpen, setIsOpen] = useState(false);
const [selectedOptions, setSelectedOptions] = useState(
multiple 
? (Array.isArray(defaultValue) ? defaultValue : [])
: (defaultValue ? [defaultValue] : [])
);
const [hoveredIndex, setHoveredIndex] = useState(null);
const [isFocused, setIsFocused] = useState(false);
const [searchTerm, setSearchTerm] = useState('');

const selectRef = useRef(null);
const searchInputRef = useRef(null);
const optionsRef = useRef([]);

const filteredOptions = searchable && searchTerm
? options.filter(option => 
option.label.toLowerCase().includes(searchTerm.toLowerCase())
)
: options;

const handleOptionSelect = (option) => {
if (multiple) {
setSelectedOptions(prevSelected => {
const isAlreadySelected = prevSelected.some(item => item.value === option.value);

if (isAlreadySelected) {
const newSelection = prevSelected.filter(item => item.value !== option.value);
if (onChange) onChange(newSelection);
return newSelection;
} else {
const newSelection = [...prevSelected, option];
if (onChange) onChange(newSelection);
return newSelection;
}
});

if (searchable && isOpen) {
searchInputRef.current?.focus();
}
} else {
setSelectedOptions([option]);
setIsOpen(false);
if (onChange) onChange(option);
}
};

const handleKeyDown = (e) => {
if (disabled) return;

// Логика клавиатурной навигации
// (та же, что и в предыдущей версии)
// ...

if (multiple && e.key === 'Backspace' && searchTerm === '' && selectedOptions.length > 0) {
const newSelection = selectedOptions.slice(0, -1);
setSelectedOptions(newSelection);
if (onChange) onChange(newSelection);
}
};

const renderSelectedValues = () => {
if (!multiple) {
return selectedOptions.length > 0 
? selectedOptions[0].label 
: placeholder;
}

if (selectedOptions.length === 0) {
return <span className="placeholder">{placeholder}</span>;
}

return (
<div className="selected-tags">
{selectedOptions.map(option => (
<div key={option.value} className="selected-tag">
<span>{option.label}</span>
<button
type="button"
className="tag-remove"
onClick={(e) => {
e.stopPropagation();
handleOptionSelect(option);
}}
>
×
</button>
</div>
))}
</div>
);
};

const renderSearchInput = () => {
if (!searchable || !isOpen) return null;

return (
<div className="search-container" onClick={e => e.stopPropagation()}>
<input
ref={searchInputRef}
type="text"
className="search-input"
placeholder="Поиск..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
onKeyDown={handleKeyDown}
autoFocus
/>
</div>
);
};

useEffect(() => {
if (!isOpen) {
setSearchTerm('');
}
}, [isOpen]);

useEffect(() => {
if (isOpen && searchable) {
searchInputRef.current?.focus();
}
}, [isOpen, searchable]);

return (
<div 
className={`
custom-select 
${disabled ? 'disabled' : ''} 
${isFocused ? 'focused' : ''} 
${multiple ? 'multiple' : ''} 
${searchable ? 'searchable' : ''}
`}
ref={selectRef}
onKeyDown={handleKeyDown}
tabIndex={!isOpen && !disabled ? 0 : -1}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
role="combobox"
aria-haspopup="listbox"
aria-expanded={isOpen}
aria-disabled={disabled}
aria-labelledby={`${name}-label`}
>
<div 
className={`select-header ${isOpen ? 'open' : ''}`}
onClick={() => !disabled && setIsOpen(!isOpen)}
id={`${name}-label`}
>
<div className="selected-value-container">
{renderSelectedValues()}
</div>
<span className={`arrow ${isOpen ? 'up' : 'down'}`}></span>
</div>

{isOpen && !disabled && (
<div className="dropdown-container">
{renderSearchInput()}

<ul 
className="options-container"
role="listbox"
aria-multiselectable={multiple}
aria-activedescendant={hoveredIndex !== null ? `${name}-option-${hoveredIndex}` : undefined}
>
{filteredOptions.length === 0 ? (
<li className="no-options">Нет результатов</li>
) : (
filteredOptions.map((option, index) => (
<SelectOption
key={option.value}
option={option}
isSelected={selectedOptions.some(item => item.value === option.value)}
isHovered={index === hoveredIndex}
onSelect={() => handleOptionSelect(option)}
onMouseEnter={() => setHoveredIndex(index)}
ref={el => {
optionsRef.current[index] = el;
}}
id={`${name}-option-${index}`}
role="option"
aria-selected={selectedOptions.some(item => item.value === option.value)}
/>
))
)}
</ul>
</div>
)}
</div>
);
};

export default CustomSelect;

Добавим дополнительные стили для поддержки мультивыбора и поиска:

CSS
Скопировать код
/* Дополнительные стили для мультивыбора и поиска */

/* Стили для мультиселекта */
.custom-select.multiple .select-header {
height: auto;
min-height: 38px;
flex-wrap: wrap;
gap: 4px;
}

.selected-value-container {
display: flex;
flex: 1;
flex-wrap: wrap;
gap: 4px;
overflow: hidden;
}

.selected-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}

.selected-tag {
display: inline-flex;
align-items: center;
background-color: #e6f2ff;
border-radius: 3px;
padding: 2px 8px;
font-size: 14px;
}

.tag-remove {
display: inline-flex;
align-items: center;
justify-content: center;
width: 16px;
height: 16px;
margin-left: 4px;
background: none;
border: none;
border-radius: 50%;
padding: 0;
font-size: 14px;
line-height: 1;
cursor: pointer;
opacity: 0.7;
}

.tag-remove:hover {
background-color: rgba(0, 0, 0, 0.1);
opacity: 1;
}

/* Стили для поиска */
.dropdown-container {
position: absolute;
top: 100%;
left: 0;
right: 0;
margin-top: 5px;
background-color: #fff;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
z-index: 10;
animation: fadeIn 0.2s ease;
}

.search-container {
padding: 8px;
border-bottom: 1px solid #eee;
}

.search-input {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}

.search-input:focus {
outline: none;
border-color: #4a90e2;
}

.custom-select.searchable .options-container {
position: static;
max-height: 180px;
margin-top: 0;
border: none;
box-shadow: none;
}

.no-options {
padding: 10px;
color: #999;
text-align: center;
font-style: italic;
}

Наш расширенный компонент теперь поддерживает следующие возможности:

  • Множественный выбор — возможность выбора нескольких опций с отображением в виде тегов
  • 🔍 Поиск по опциям — фильтрация списка опций по мере ввода текста
  • 🗑️ Удаление выбранных опций — как кликом, так и с клавиатуры
  • 📢 Обратная связь — сообщение, если поиск не дал результатов
  • ⌨️ Расширенная клавиатурная навигация — включая специфичные для мультивыбора действия

Custom Select — это не просто элемент формы, а инструмент, который определяет качество взаимодействия пользователя с вашим приложением. Когда вы создаете свой собственный компонент, вы получаете полный контроль над его внешним видом и поведением, что открывает безграничные возможности для создания уникального пользовательского опыта. Инвестиции в качественную разработку кастомных компонентов интерфейса всегда окупаются лояльностью пользователей и высоким качеством конечного продукта.

Читайте также

Проверь как ты усвоил материалы статьи
Пройди тест и узнай насколько ты лучше других читателей
Какой хук используется для управления состоянием выбранной опции в компоненте CustomSelect?
1 / 5

Владимир Лисицын

разработчик фронтенда

Свежие материалы

Загрузка...