Materials

Пакет экспортирует SCSS-миксины без JS runtime — используется другими пакетами как @use '@cloud-ru/ds-materials' as m.

Состав

  • material/_acrylic.scss — акриловый эффект (blur + полупрозрачный фон)
  • stateLayer/index.scss — state-layer для hover/pressed состояний
  • focusFrame/index.scss — рамка фокуса (with-focus-frame)
  • utils/_mixins.scss — вспомогательные миксины (has-state-layer-as-child, has-content-with-text-opacity)

Использование

// packages/button/src/styles.module.scss
@use '@cloud-ru/ds-materials' as m;

.root {
  @include m.has-state-layer-as-child(#{stateLayer});
}

Overview

Пакет построен вокруг корневого миксина материалов и двух наборов миксинов:

  • Material (корень) — миксин with-material($material, $props...) применяет выбранный материал к элементу; аргументы после $material передаются в реализацию материала (для 'acrylic' — селекторы дочерних слоёв). Со временем появятся другие материалы (например gradient). Разметка и параметры зависят от материала.
  • Acrylic — материал «матового стекла» (backdrop blur + полупрозрачный слой). Вызывается через with-material('acrylic', …) с селекторами слоёв. На корне — data-acrylic-appearance и data-acrylic-level; фон и опциональный эффект — дочерние элементы с классами, имена которых передаются вторым и третьим аргументом. На эти классы пакет навешивает только «акриловые» свойства (opacity, фон, blend modes); position: relative на корне с материалом пакет не задаёт — его при необходимости добавляет потребитель (например, для абсолютно позиционированных слоёв и стека). Позиционирование и pointer-events самих слоёв тоже задаёт потребитель.
  • State layer — слой состояний (hover, active) для кнопок, карточек и других интерактивных элементов. Поддерживает варианты: обычный фон/бордер, активированный, onColor, onAccent и прозрачность текста/иконок (textOpacity). Использует переменные из @cloud-ru/ds-figma-variables.

Подключение в SCSS:

@use '@design-system/materials' as m;

Через namespace m доступны миксины материалов (with-material), state layer (has-state-layer-as-child, has-content-with-text-opacity) и при необходимости низкоуровневые утилиты.

Usage

Material (корень) и Acrylic

Акрил подключают вызовом with-material('acrylic', $bgLayerSelector, $effectLayerSelector?):

  • $bgLayerSelector (обязательный) — имя локального класса дочернего фонового слоя в том же SCSS-модуле, обычно в CSS Modules задают как #{acrylic} при классе .acrylic в разметке.
  • $effectLayerSelector (опциональный) — то же для слоя color-dodge; если не передавать, стили эффекта не генерируются.

На корневом элементе задайте атрибуты:

  • data-acrylic-appearance — вариант оформления: 'primary', 'neutral', 'blue', 'red', 'yellow', 'green'.
  • data-acrylic-level — уровень глубины: 'default', '1Level', '2Level'.

Миксин акрила не выставляет на этом корне position: relative (чтобы не ломать layout в сложных обёртках, например у Drawer). Если дочерние слои позиционируются относительно корня (position: absolute и т.п.) или нужен явный контекст наложения — задайте position: relative (или иной подходящий position) на классе того же элемента, куда подключаете with-material, в своём SCSS.

В разметке обязателен один дочерний элемент с классом, совпадающим с первым аргументом (например <div className={styles.acrylic} />). Опционально — второй дочерний элемент с классом для эффекта. Контент размещайте поверх фонового слоя (например, в отдельном блоке с position: relative и z-index).

Внутри with-material для слоёв вызываются миксины has-acrylic-background-layer-as-child и при необходимости has-acrylic-effect-layer-as-child. Они не подключают absolute-layer и не выставляют pointer-events: none — только opacity / background для фона и режимы наложения для эффекта. Чтобы слой заполнял корень и не перехватывал события, задайте на своих классах слоёв позиционирование (часто position: absolute, inset: 0), при необходимости border-radius: inherit, pointer-events: none и порядок в стеке — как в примере ниже для .acrylic.

Разметка (JSX):

<div className={styles.card} data-acrylic-appearance='neutral' data-acrylic-level='1Level'>
  <div className={styles.acrylic} />
  <div className={styles.content}>{children}</div>
</div>

SCSS:

@use '@design-system/materials' as m;

.acrylic {
  pointer-events: none;
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

.card {
  position: relative;
  @include m.with-material('acrylic', #{acrylic});
}

.content {
  position: relative;
  z-index: 1;
}

С опциональным слоем эффекта (третий аргумент и второй декоративный div с собственным классом в том же модуле). Для слоя эффекта так же задайте позиционирование и при необходимости pointer-events: none в своём модуле — пакет добавляет только blend modes.

.acrylicEffect {
  pointer-events: none;
  position: absolute;
  inset: 0;
  border-radius: inherit;
}

@include m.with-material('acrylic', #{acrylic}, #{acrylicEffect});

State layer — слой состояний

Миксин has-state-layer-as-child($stateLayerSelector) подключают в классе корневого элемента, с которым пользователь взаимодействует (hover, active и т.д.): в этот момент стили применяются к потомку, чей класс передан аргументом. В CSS Modules аргумент задают как #{stateLayer}, если в том же файле объявлен класс .stateLayer.

На элементе слоя указывают data-state — имя совпадает с именем сета в Figma (material/stateLayer/<имя>):

  • emptyNeutralOnBackground — нейтральный слой поверх фона страницы, по умолчанию прозрачный.
  • borderOnBackground — слой обводки вместо заливки.
  • activatedOnBackground — слой выбранного (активированного) элемента.
  • versionOnColor / emptyVersionOnColor — слой поверх цветной подложки; вариант с префиксом empty прозрачен по умолчанию.
  • inversionOnColor / emptyInversionOnColor — то же для инверсной подложки.
  • emptyDarkOnAccent — тёмный слой поверх акцентной подложки: filled/tonal-кнопки, теги, промотеги, ручка слайдера.
  • textOpacity — не заливка, а прозрачность текста и иконок (миксин has-content-with-text-opacity).

Набор состояний из пропа нельзя сгенерировать автоматически — только ручной маппинг к структуре Figma Variables.

Примеры: Playground, SampleBlock, StateSquare (packages/materials/stories/…).

Разметка (JSX):

<button type='button' className={styles.button}>
  <span className={styles.stateLayer} data-state='emptyNeutralOnBackground' aria-hidden />
  <span className={styles.label}>Текст кнопки</span>
</button>

SCSS:

@use '@design-system/materials' as m;

.button {
  position: relative;
  @include m.has-state-layer-as-child(#{stateLayer});
}

.label {
  position: relative;
  z-index: 1;
}

State layer — прозрачность текста (textOpacity)

Миксин has-content-with-text-opacity($contentLayerSelector) подключают в классе корневого элемента: при взаимодействии с ним меняется внешний вид потомка с классом из аргумента. На элементах внутри этого потомка, отмеченных [data-text-opacity], задаётся opacity (default / hover / active).

Пример: Playground, SampleBlock (#{contentLayer}).

Разметка (JSX):

<div className={styles.button}>
  <div className={styles.contentLayer}>
    <span data-text-opacity>Click me</span>
    <Icon data-text-opacity />
  </div>
</div>

SCSS:

@use '@design-system/materials' as m;

.button {
  position: relative;
  @include m.has-content-with-text-opacity(#{contentLayer});
}

API миксинов

Material (корень)

миксинпараметрыописание
with-material($material, $props...)$material — название материала (сейчас только 'acrylic'). Для 'acrylic' в $props: обязательный $bgLayerSelector, опционально $effectLayerSelector (имена классов дочерних слоёв)Применяет выбранный материал к элементу. Для acrylic: на корне data-acrylic-appearance и data-acrylic-level; дочерний слой фона — элемент с классом, совпадающим с $bgLayerSelector (в модулях — #{имяКласса}); опционально слой эффекта — класс $effectLayerSelector. Blur на корне, opacity/фон на фоновом классе и blend modes на классе эффекта берутся из @cloud-ru/ds-figma-variables и внутренних миксинов слоёв. position: relative на корне с материалом не выставляется — при необходимости задайте сами. Позиционирование и pointer-events для дочерних слоёв пакет не задаёт — их описывает потребитель на тех же классах.

Acrylic

Акрил подключается только через with-material('acrylic', …) из публичного API пакета. Атрибуты на корне задают appearance и level; селекторы слоёв — строки имён классов для вложенных правил вида .$bgLayerSelector / .$effectLayerSelector относительно корня с материалом.

  • Разметка: корень с data-acrylic-appearance и data-acrylic-level; один дочерний элемент с классом фона; опционально дочерний элемент с классом слоя эффекта (color-dodge), если передан третий аргумент в with-material.
  • Корень: position: relative с акрилом не приходит из пакета; задавайте на классе корня при необходимости (типичный случай — абсолютные дочерние слои фона/эффекта).
  • Стили слоёв: пакетные миксины слоёв не дублируют absolute-layer и не выставляют pointer-events: none; эти и другие layout-правила добавляйте на классах .acrylic / слоя эффекта в своём SCSS.

State layer

миксинпараметрыразметкаописание
has-state-layer-as-child($stateLayerSelector)Имя класса целевого потомка в том же модуле (часто #{stateLayer})Корень (миксин здесь) + потомок с этим классом и data-stateПри взаимодействии с корнем стили state layer уходят на потомка $stateLayerSelector
has-content-with-text-opacity($contentLayerSelector)Имя класса обёртки (часто #{contentLayer})Корень + потомок с этим классом; у его потомков — [data-text-opacity]При взаимодействии с корнем opacity меняется у [data-text-opacity] внутри $contentLayerSelector

Переменные цветов и состояний берутся из @cloud-ru/ds-figma-variables (stateLayer подключает этот пакет внутри себя).

Focus frame

миксинпараметрыописание
with-focus-frame($appearance, $position)$appearanceregular (по умолчанию), primary, destructive, warning, success, regularInversion. $positioninside, outside (по умолчанию), outsideOffsetОсновной путь. Выводит правило &:focus-visible целиком — рамка появляется только на клавиатурном фокусе
focus-frame-styles($appearance, $position)те жеТе же свойства без селектора — когда рамка вешается не на сам фокусируемый элемент

Обычный случай — миксин ставит псевдокласс сам:

@use '@cloud-ru/ds-materials' as m;

.root {
  @include m.with-focus-frame('regular', 'outside');
}

Когда рамка не на самом фокусируемом элементе — на потомке, на соседе, на псевдоэлементе или по собственному флагу — берут focus-frame-styles и пишут селектор руками:

.root:focus-visible .container {
  @include m.focus-frame-styles('primary', 'inside');
}
  • Положение. inside — штрих внутрь бокса, outside — наружу вплотную, outsideOffset — наружу с зазором в толщину штриха. Соответствуют мастерам focusedFrame/…/inside|outside|outsideOffset.
  • Радиус не задаётся: outline повторяет border-radius элемента, а мастер помечен «apply radius» — скругление приходит от потребителя.
  • outline-offset выводится всегда, включая нулевой. Без явного значения Chromium подставляет собственный отступ в 1px, которого в макете нет.
  • regularInversion — белый штрих для тёмных поверхностей.

Accessibility

  • Разметка с декоративными слоями акрила (фон/эффект) и state layer не меняет семантику: у визуальных слоёв по возможности задавайте aria-hidden; на корне используйте корректные теги и атрибуты (button, aria-*, role).
  • Слои состояний визуально не должны подменять индикацию фокуса: сохраняйте видимый focus outline для клавиатурной навигации.
  • Контент поверх акрилового фона должен сохранять достаточный контраст для читаемости.

Best practices

  1. Один фоновый слой — в одном блоке один дочерний элемент с классом, переданным в with-material('acrylic', #{…}) как фон; контент размещайте в соседних дочерних элементах с вышележащим z-index. Для декоративного фона/эффекта явно задавайте заполнение корня и pointer-events: none на классах слоёв — пакет этого не делает. Если слои абсолютные, на корне с with-material обычно нужен свой position: relative (пакет его не добавляет).
  2. Корень взаимодействияhas-state-layer-as-child и has-content-with-text-opacity вешайте на класс того элемента, с которым пользователь взаимодействует; аргумент — класс целевого потомка в разметке. Несколько state-слоёв — разные классы и при необходимости несколько вызовов первого миксина.
  3. Переменные дизайн-системы — для acrylic используйте значения data-acrylic-appearance и data-acrylic-level из палитры дизайн-системы; пакет подставляет blur, opacity и цвета из @cloud-ru/ds-figma-variables.
  4. textOpacity только для нужного контента — помечайте data-text-opacity только у текста и иконок, которые должны менять прозрачность при наведении/нажатии.
  5. Не дублировать логику — если используете готовый компонент (например, Block из @cloud-ru/ds-block), он уже может использовать эти миксины; не подключайте materials повторно для того же визуального эффекта.

Установка

pnpm add @cloud-ru/ds-materials

Figma