Пошаговое создание кастомного селекта на React и CSS с примерами
#CSS и верстка #React #Фронтенд CSSДля кого эта статья:
- Фронтенд-разработчики, работающие с 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 компонента для селекта
Прежде чем приступить к кодированию, важно продумать архитектуру компонента. Хорошо спроектированный кастомный селект должен быть гибким, доступным и легко интегрируемым.
Базовая структура нашего компонента будет состоять из трех основных частей:
- Контейнер селекта — основной видимый элемент, который пользователь нажимает для открытия списка
- Выпадающий список — контейнер для отображения опций
- Опции — отдельные элементы списка, которые можно выбрать
Вот простая структура файлов для нашего кастомного селекта:
CustomSelect/index.jsx— основной компонентSelectOption.jsx— компонент для отдельной опцииstyles.css— стили компонентаtypes.js— типы данных (если используется TypeScript)
Начнем с создания базовой структуры компонента:
// 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;
Теперь создадим компонент для отдельных опций:
// 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 | Отслеживает, находится ли компонент в фокусе |
Расширим наш компонент, добавив клавиатурную навигацию и доступность:
// 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;
Для корректной работы необходимо также обновить компонент опции:
// 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
Теперь, когда у нас есть функциональный компонент с хорошо организованной структурой и логикой, добавим стили, чтобы наш кастомный селект выглядел привлекательно и соответствовал современным стандартам дизайна.
Вот базовые стили для нашего компонента:
/* 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;
}
Эти стили обеспечивают следующие визуальные аспекты:
- 📐 Современный минималистичный дизайн — чистые линии, тонкие рамки, сдержанные цвета
- 💫 Плавные анимации — для более приятного взаимодействия
- ✨ Визуальная обратная связь — состояния наведения, выбора, фокуса
- 📱 Адаптивный дизайн — корректное отображение на разных устройствах
- 🧩 Стилизованный скроллбар — для более эстетичного вида при прокрутке большого списка
Для создания более уникальных дизайнов можно применять различные стилистические решения:
- Материальный дизайн — с характерными тенями, анимациями и рипл-эффектами
- Неоморфизм — с объемными элементами, создающими иллюзию выдавленных или утопленных компонентов
- Стекломорфизм — с эффектом полупрозрачного стекла и размытия
Пример модификации стилей под материальный дизайн:
/* Дополнительные стили для материального дизайна */
.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);
}
Расширение функционала: мультивыбор и фильтрация опций
Базовый кастомный селект отлично справляется с простыми сценариями, но на практике часто требуется расширенная функциональность. Добавим поддержку множественного выбора и фильтрацию опций по мере ввода текста.
Сначала модифицируем наш компонент для поддержки мультивыбора:
// 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;
Добавим дополнительные стили для поддержки мультивыбора и поиска:
/* Дополнительные стили для мультивыбора и поиска */
/* Стили для мультиселекта */
.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 — это не просто элемент формы, а инструмент, который определяет качество взаимодействия пользователя с вашим приложением. Когда вы создаете свой собственный компонент, вы получаете полный контроль над его внешним видом и поведением, что открывает безграничные возможности для создания уникального пользовательского опыта. Инвестиции в качественную разработку кастомных компонентов интерфейса всегда окупаются лояльностью пользователей и высоким качеством конечного продукта.
Читайте также
Владимир Лисицын
разработчик фронтенда