ModalCustom

ModalCustom — низкоуровневая версия Modal, которая не диктует структуру содержимого. Вы сами компонуете шапку, тело и футер из субкомпонентов ModalCustom.Header, .Body, .Footer или собственной разметки.

Используйте ModalCustom, когда стандартной шапки из Modal недостаточно — например, нужна своя раскладка заголовка с несколькими действиями, кастомный футер с группами кнопок или нестандартный порядок секций.

Когда использовать

  • Стандартная шапка / футер из Modal не подходят — нужна своя разметка.
  • Сложная раскладка нескольких секций внутри одного окна.
  • Кастомные слоты (например, фиксированный поиск между шапкой и телом).

Во всех остальных случаях предпочтительнее Modal — он дешевле в поддержке и даёт консистентные отступы.

Установка

pnpm add @cloud-ru/ds-modal
import { ModalCustom } from '@cloud-ru/ds-modal'

Примеры использования

Ручная композиция

Ручная композицияHeader + Body + Footer собираются вручную.
tsx
import { Button } from '@cloud-ru/ds-button';
import { ModalCustom } from '@cloud-ru/ds-modal';
import { useState } from 'react';

export function CustomComposition() {
  const [open, setOpen] = useState(false);
  const close = () => setOpen(false);

  return (
    <>
      <Button label='Открыть' appearance='primary' view='filled' onClick={() => setOpen(true)} />
      <ModalCustom open={open} onClose={close} width='m'>
        <ModalCustom.Header title='Ручная композиция' subtitle='Header, Body и Footer собираются вручную.' />
        <ModalCustom.Body
          content={
            <div style={{ padding: 24 }}>
              <p>В теле может быть любая разметка — скролл включается автоматически.</p>
              <p>Это нужно, когда пресетной структуры Modal недостаточно.</p>
            </div>
          }
        />
        <ModalCustom.Footer>
          <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
            <Button label='Закрыть' appearance='neutral' view='outline' onClick={close} />
            <Button label='Подтвердить' appearance='primary' view='filled' onClick={close} />
          </div>
        </ModalCustom.Footer>
      </ModalCustom>
    </>
  );
}

Props

Types

PropsModalCustomProps
PropTypeDefaultRequiredDescription
childrenReactNodeyesСодержимое окна (композиция Header/Body/Footer)
classNamestringnoCSS-класс окна
closeOnPopstatebooleannoЗакрытие при навигации по истории
containerModalContainernoЯвный DOM-контейнер для `createPortal`; иначе `usePortalContext()` или `document.body`.
data-test-idstringno
heightAutobooleannoРастягивать по высоте в пределах контейнера
mode"aggressive" | "forced" | "regular"MODE.RegularnoРежим закрытия: Regular — overlay/Esc/кнопка; Aggressive — только кнопка; Forced — без кнопки и overlay/Esc.
onClose() => voidyesКолбэк закрытия
openbooleanyesУправление состоянием показан/не показан
rootClassNamestringnoCSS-класс корневого слоя портала
safeAreabooleantruenoРезервировать ли место под iOS notch / home-indicator и Android nav-bar. Реализовано паддингом на `.content` через `env(safe-area-inset-*)`: на устройстве без выреза/индикатора (и на desktop) inset = 0, поэтому никакого «лишнего» отступа не появляется; на notched-устройстве — ровно нужный. Верхний отступ добавляется только когда sheet раскрыт на полный вьюпорт (его верх под notch).
showBackdropbooleantruenoОтображение тёмной подложки за sheet'ом. При `false` фон не затемняется и click-outside не закрывает sheet (нет backdrop-узла, по которому ловится клик).
snapPointsSnapPoint[]noМассив фиксированных позиций sheet'а от меньшей к большей. По дефолту `undefined` — sheet `height: auto` с одним snap'ом по высоте контента. Пример: `[0.5, 1]` — sheet открывается на половину экрана, drag вверх раскрывает до full-viewport; drag вниз ниже `0.5` ведёт к закрытию. Контракт массива (движок не сортирует и не дедуплицирует — порядок и различимость на стороне потребителя): - строго по возрастанию: индекс `0` — самая компактная позиция, последний — top / expanded; - значения должны резолвиться в различные высоты (`['50%', 0.5]` на типичном вьюпорте дадут одну высоту → дубль-индекс будет недостижим свайпом); - `'fit-content'` имеет смысл только как ЕДИНСТВЕННЫЙ snap (без `snapPoints`); внутри массива фиксированных позиций его «контентная» высота не определена.
swipeEnabledbooleantruenoВключает swipe-down для закрытия / swipe-up для раскрытия на следующий snap-point. При `swipeEnabled=false` snap-point по-прежнему можно переключить через controlled `snapIndex` prop'ом.
width"l" | "m" | "s"noРазмер окна

Unions

Types

ModalCustomProps

Unions

Storybook

Figma