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`. |
api | { mode: "wysiwyg" | "raw"; focus(): void; isActive(id: ToolbarItemId): boolean; isHeadingActive(level: HeadingLevel): boolean; toggle(id: ToolbarItemId): void; toggleHeading(level: HeadingLevel): void; setParagraph(): void; getLinkHref(): string | undefined; getLinkTitle(): string | undefined; setLink(props: LinkProps): void; insertImage(src: string, alt: string): void; insertTable(rows: number, cols: number): void; subscribe(callback: () => void): () => void; } | — | yes | Бэкенд команд: WYSIWYG (TipTap) в preview-режиме либо markdown-исходник (textarea) в raw-режиме. |
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 | Состояние частичного выбора |
items | ToolbarItemId[] | — | yes | |
moreActions | Action[] | — | no | Элементы выпадающего списка кнопки с действиями |
onCheck | (() => void) | — | no | Колбек смены значения чекбокса |
onRefresh | (() => void) | — | no | Колбек обновления |
outline | boolean | true | no | Внешний бордер |
persist | ToolbarPersistConfig<TState> | — | no | Конфиг для сохранения состояния в localStorage и queryParams. <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
Related props
ToolbarApi
ToolbarItemId
Адаптивность
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.