Toolbar
Toolbar — композитная панель над таблицей или списком: поиск, обновление, фильтры, переключатель вида данных, массовые действия и overflow-меню «⋯». Панель адаптивна: на mobile меню «⋯» и bulk-действия переезжают в BottomSheet. Раскладку компонент берёт из AdaptiveProvider — отдельного пропа layoutType нет.
Когда использовать
- Над таблицей или списком с поиском, фильтрами и действиями над выбранными строками.
- Когда нужно сохранять состояние фильтров и поиска в URL или
localStorage(persist).
Когда не нужен:
- Для одиночного поля поиска без остальных слотов —
SearchилиSearchPrivate. - Для переключения вкладок раздела —
Tabs. - Для произвольного меню действий без контекста списка —
Dropdown.
Рекомендации
- ✅ Один
Toolbarна экран над данными. - ❌ Дублировать поиск и фильтры в header и в теле страницы.
- ✅ Controlled
searchчерезvalue+onChange. - ❌ No-op
onChange— строка поиска не реагирует на ввод. - ✅ Один
<AdaptiveProvider>в корне приложения — mobile-перестроение включается само. - ❌ Ручное ветвление desktop/mobile-вёрстки тулбара в обход контекста раскладки.
- ✅ Уникальный
persist.idна каждый инстанс. - ❌ Один
idна несколько тулбаров — состояние фильтров смешается.
Анатомия
Слоты сверху вниз:
- Строка панели —
onRefresh,search,after,dataView, кнопка фильтров,moreActions. - Строка фильтров —
ChipChoiceRow/MobileChipChoiceRowприfilterRow. - Bulk-панель — чекбокс, счётчик выбранных, tonal-кнопки; не влезшие действия — в «⋯».
Outline (default false)
true— внешний бордер через отдельный слойborderна контейнере.false— панель без внешнего бордера.
Установка
pnpm add @cloud-ru/ds-toolbar
import { Toolbar } from '@cloud-ru/ds-toolbar';
Примеры использования
Базовый desktop
import { Toolbar } from '@cloud-ru/ds-toolbar';
import { useState } from 'react';
export function Basic() {
const [search, setSearch] = useState('');
return (
<div style={{ width: '100%', maxWidth: 720 }}>
<Toolbar
search={{ value: search, onChange: setSearch, placeholder: 'Поиск' }}
onRefresh={() => setSearch('')}
moreActions={[
{ content: { label: 'Экспорт' }, onClick: () => undefined },
{ content: { label: 'Настройки' }, onClick: () => undefined },
]}
/>
</div>
);
}Фильтры
import { Toolbar } from '@cloud-ru/ds-toolbar';
import { useState } from 'react';
export function WithFilters() {
const [search, setSearch] = useState('');
const [filtersOpen, setFiltersOpen] = useState(true);
const [filterValue, setFilterValue] = useState<Record<string, unknown>>({});
return (
<div style={{ width: '100%', maxWidth: 720 }}>
<Toolbar
search={{ value: search, onChange: setSearch, placeholder: 'Поиск' }}
onRefresh={() => setSearch('')}
filterRow={{
open: filtersOpen,
onOpenChange: setFiltersOpen,
value: filterValue,
onChange: setFilterValue,
filters: [
{
id: 'status',
type: 'single',
label: 'Статус',
options: [
{ value: 'active', label: 'Активные' },
{ value: 'archived', label: 'Архив' },
],
},
],
defaultValue: {},
}}
/>
</div>
);
}Массовые действия
import { CheckSVG, CopySVG, CrossSVG } from '@cloud-ru/ds-icons/interface/system';
import { Toolbar } from '@cloud-ru/ds-toolbar';
import { useState } from 'react';
export function BulkActions() {
const [search, setSearch] = useState('');
const [checked, setChecked] = useState(true);
return (
<div style={{ width: '100%', maxWidth: 720 }}>
<Toolbar
search={{ value: search, onChange: setSearch, placeholder: 'Поиск' }}
checked={checked}
indeterminate={false}
selectedCount={checked ? 5 : 0}
totalCount={100}
onCheck={() => setChecked(value => !value)}
bulkActions={[
{ label: 'Подтвердить', icon: CheckSVG, onClick: () => undefined },
{ label: 'Отклонить', icon: CrossSVG, onClick: () => undefined },
{ label: 'Копировать', icon: CopySVG, onClick: () => undefined },
]}
/>
</div>
);
}Слоты after и dataView
import { Button } from '@cloud-ru/ds-button';
import { PlaceholderSVG } from '@cloud-ru/ds-icons/interface/system';
import { Toolbar } from '@cloud-ru/ds-toolbar';
import { useState } from 'react';
export function WithDataView() {
const [search, setSearch] = useState('');
return (
<div style={{ width: '100%', maxWidth: 720 }}>
<Toolbar
search={{ value: search, onChange: setSearch, placeholder: 'Поиск' }}
onRefresh={() => setSearch('')}
after={
<Button
view='function'
appearance='neutral'
icon={<PlaceholderSVG />}
size='m'
aria-label='Дополнительное действие'
onClick={() => undefined}
/>
}
dataView={{ show: true }}
moreActions={[{ content: { label: 'Ещё' }, onClick: () => undefined }]}
/>
</div>
);
}Props
Types
ToolbarProps| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
after | ReactNode | — | no | Дополнительный слот между поиском и переключателем вида (+ slotExtraButton в Figma). <br> На mobile-раскладке (из `AdaptiveProvider`) не рендерится в строке — кнопки переносятся в меню «⋯» (`Button` с `onClick` и `label` / `icon` / `aria-label`, одна обёртка вокруг кнопки или элемент с `data-toolbar-after-overflow`). Иначе — в `moreActions`. |
bulkActions | BulkAction[] | — | no | Список массовых действий |
checked | boolean | — | no | Значение чекбокса |
className | string | — | no | К ласснейм |
data-test-id | string | — | no | |
dataView | ToolbarDataViewProps | — | no | Переключатель вида данных — SegmentControl (showDataView в Figma) |
filterRow | FilterRow<TState> | — | no | |
indeterminate | boolean | — | no | Состояние частичного выбора |
moreActions | Action[] | — | no | Элементы выпадающего списка кнопки с действиями |
onCheck | (() => void) | — | no | Колбек смены значения чекбокса |
onRefresh | (() => void) | — | no | Колбек обновления |
outline | boolean | true | no | Внешний бордер |
persist | ToolbarPersistConfig<TState> | — | no | Конфиг сохранения состояния в URL, localStorage и sessionStorage. <br> Поле id должно быть уникальным для каждого инстанса компонента. <br> |
search | SearchProps | — | no | Параметры отвечают за строку поиска <br> <strong>value</strong>: Значение строки поиска <br> <strong>onChange</strong>: Колбэк смены значения <br> <strong>onSubmit</strong>: Колбэк на подтверждение поиска по строке <strong>placeholder</strong>: Плейсхолдер <br> <strong>loading</strong>: Состояние загрузки <br> |
selectedCount | number | — | no | Количество выбранных элементов (для подписи Selected: N) |
showBulkCheckbox | boolean | true | no | Показывать чекбокс слева (Figma: showBulkCheckbox) |
totalCount | number | — | no | Общее количество элементов (для подписи Selected: N of M) |
Types
ToolbarProps
BulkActionsProps
DataViewBaseProps
FilterRow
MoreActionsProps
SearchProps
ToolbarDataViewProps
ToolbarPersistConfig
Адаптивность
Toolbar — адаптивный компонент: DOM остаётся единым, но при mobile-раскладке панель перестраивается. Раскладку он берёт из AdaptiveProvider (контекст @cloud-ru/ds-adaptive); публичный API единый для обеих платформ:
- desktop (по умолчанию) — overflow «⋯» открывается в
Droplist, bulk-действия идут строкой под чипами фильтров. - mobile — overflow «⋯» и bulk-действия переезжают в
BottomSheet(панель снизу без backdrop при активном выборе).
Верстайте под desktop и поставьте один <AdaptiveProvider> в корне приложения — mobile-перестроение включается автоматически (desktop-first). Пропа layoutType у компонента нет: источник раскладки — только контекст.
Mobile layout
import { AdaptiveProvider, LAYOUT_TYPE } from '@cloud-ru/ds-adaptive';
import { CheckSVG, CrossSVG } from '@cloud-ru/ds-icons/interface/system';
import { Checkbox } from '@cloud-ru/ds-toggles';
import { Toolbar } from '@cloud-ru/ds-toolbar';
import { useId, useState } from 'react';
import { MobilePreview } from '../MobilePreview';
export function MobileLayout() {
const selectionToggleId = useId();
const [search, setSearch] = useState('');
const [checked, setChecked] = useState(true);
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12, alignItems: 'flex-start' }}>
<label
htmlFor={selectionToggleId}
style={{ display: 'inline-flex', gap: 8, alignItems: 'center', cursor: 'pointer' }}
>
<Checkbox id={selectionToggleId} size='s' checked={checked} onChange={setChecked} />
<span>Есть выбранные строки таблицы</span>
</label>
<MobilePreview>
<AdaptiveProvider layoutType={LAYOUT_TYPE.Mobile}>
<Toolbar
search={{ value: search, onChange: setSearch, placeholder: 'Поиск' }}
onRefresh={() => setSearch('')}
moreActions={[{ content: { label: 'Действие' }, onClick: () => undefined }]}
checked={checked}
onCheck={() => setChecked(value => !value)}
selectedCount={checked ? 12 : 0}
totalCount={100}
bulkActions={[
{ label: 'Подтвердить', icon: CheckSVG, onClick: () => undefined },
{ label: 'Отклонить', icon: CrossSVG, onClick: () => undefined },
]}
/>
</AdaptiveProvider>
</MobilePreview>
</div>
);
}Как форсировать платформу
Форс — только контекстом, не пропом:
- Поддерево — вложенный провайдер:
import { AdaptiveProvider } from '@cloud-ru/ds-adaptive' <AdaptiveProvider layoutType='mobile'> <Toolbar search={search} onRefresh={refresh} moreActions={actions} /> </AdaptiveProvider> - Отдельный компонент —
withLayoutType(module-scope, сахар над провайдером):import { withLayoutType } from '@cloud-ru/ds-adaptive' import { Toolbar } from '@cloud-ru/ds-toolbar' const MobileToolbar = withLayoutType(Toolbar, 'mobile')
Подробнее о модели адаптивности — Адаптивность — паттерн.
Storybook
Figma
Смотри также
Search— поле поиска внутри тулбара (background={false}).SegmentControl— типичный контент слотаdataView.
Хранилища состояния
persist.storages задаёт хранилища общего объекта: фильтров, поиска, пагинации и сортировки.
Для сохранения и восстановления необходимы одновременно id и filterQueryKey, даже если URL отключён.
persist={{
id: 'orders',
filterQueryKey: 'ordersState',
storages: ['queryParams', 'sessionStorage'],
}}
Значение storages | Результат |
|---|---|
| Не задано | URL и localStorage — прежнее поведение |
['queryParams'] | Только URL |
['localStorage'] | Только localStorage |
['sessionStorage'] | Только sessionStorage |
['queryParams', 'sessionStorage'] | URL и sessionStorage |
[] | Сохранение и восстановление отключены |
При восстановлении используется первый валидный объект целиком: URL → sessionStorage → localStorage. Отключённые источники не читаются и не изменяются. Порядок списка не влияет на приоритет, повторы игнорируются. Если данных нет или они невалидны, сохраняется исходное состояние компонента.
localStorage и sessionStorage используют ключ ${id}_filter и JSON.
Пользовательские parser и serializer применяются только к URL, ключ которого задаёт filterQueryKey.
Ошибка одного источника не препятствует работе остальных.
Сброс фильтров или поиска записывает обновлённое состояние; старые и отключённые хранилища не очищаются.
Конфигурация задаётся при монтировании. Переключение хранилищ после монтирования не поддерживается.