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

Базовый desktopПоиск, обновление и меню «⋯»
tsx
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>
  );
}

Фильтры

ФильтрыКнопка фильтров и строка ChipChoiceRow
tsx
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>
  );
}

Массовые действия

Массовые действияBulk-панель под фильтрами с чекбоксом и tonal-кнопками
tsx
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

Слоты after и dataViewДополнительная кнопка и SegmentControl
tsx
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

PropsToolbarProps
PropTypeDefaultRequiredDescription
afterReactNodenoДополнительный слот между поиском и переключателем вида (+ 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-режиме.
bulkActionsBulkAction[]noСписок массовых действий
checkedbooleannoЗначение чекбокса
classNamestringnoКласснейм
data-test-idstringno
dataViewToolbarDataViewPropsnoПереключатель вида данных — SegmentControl (showDataView в Figma)
filterRowFilterRow<TState>no
indeterminatebooleannoСостояние частичного выбора
itemsToolbarItemId[]yes
moreActionsAction[]noЭлементы выпадающего списка кнопки с действиями
onCheck(() => void)noКолбек смены значения чекбокса
onRefresh(() => void)noКолбек обновления
outlinebooleantruenoВнешний бордер
persistToolbarPersistConfig<TState>noКонфиг для сохранения состояния в localStorage и queryParams. <br> Поле id должно быть уникальным для каждого инстанса компонента. <br>
searchSearchPropsnoПараметры отвечают за строку поиска <br> <strong>value</strong>: Значение строки поиска <br> <strong>onChange</strong>: Колбэк смены значения <br> <strong>onSubmit</strong>: Колбэк на подтверждение поиска по строке <strong>placeholder</strong>: Плейсхолдер <br> <strong>loading</strong>: Состояние загрузки <br>
selectedCountnumbernoКоличество выбранных элементов (для подписи Selected: N)
showBulkCheckboxbooleantruenoПоказывать чекбокс слева (Figma: showBulkCheckbox)
totalCountnumbernoОбщее количество элементов (для подписи Selected: N of M)

Types

ToolbarProps

Адаптивность

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

Mobile layoutMobile-раскладка — включите чекбокс, чтобы открыть bulk-панель в BottomSheet внутри рамки
tsx
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.