Промежуточное состояние indeterminate

В интерфейсах с иерархическими наборами данных часто возникает ситуация, когда родительский элемент связан с несколькими дочерними элементами выбора. Если часть дочерних элементов выбрана, а часть — нет, возникает состояние, которое нельзя корректно представить обычным логическим значением true или false. Для этого используется промежуточное состояниеindeterminate.

В библиотеке Radix UI такое состояние поддерживается в компонентах выбора, прежде всего в Checkbox. Оно позволяет явно показывать пользователю, что выбран только некоторый поднабор элементов.


Назначение состояния indeterminate

Традиционный чекбокс работает в бинарной логике:

Состояние Значение
checked элемент выбран
unchecked элемент не выбран

Однако в интерфейсах со списками вложенных элементов бинарной логики недостаточно. Появляется третье состояние.

Состояние Значение
true выбраны все элементы
false не выбран ни один
indeterminate выбрана только часть

Такое поведение характерно для:

  • списков с группировкой
  • деревьев файлов
  • таблиц с множественным выбором
  • панелей настроек
  • списков разрешений

Radix UI реализует это состояние на уровне API компонента, позволяя управлять им декларативно.


Представление состояния в Radix UI

Компонент Checkbox поддерживает три значения состояния:

true
false
"indeterminate"

Свойство checked принимает именно эти три значения.

Базовый пример

import * as Checkbox from "@radix-ui/react-checkbox"

function Example() {
  return (
    <Checkbox.Root checked="indeterminate">
      <Checkbox.Indicator />
    </Checkbox.Root>
  )
}

Значение "indeterminate" сообщает компоненту, что чекбокс находится в промежуточном состоянии.


Визуальное отображение

В большинстве интерфейсов indeterminate отображается не галочкой, а горизонтальной линией.

Типичная визуальная модель:

Состояние Отображение
unchecked пустой квадрат
checked галочка
indeterminate горизонтальная линия

Radix UI не навязывает конкретный стиль и позволяет реализовать любую визуализацию через Checkbox.Indicator.

Пример кастомного индикатора

import { CheckIcon, DividerHorizontalIcon } from "@radix-ui/react-icons"

<Checkbox.Root checked="indeterminate">
  <Checkbox.Indicator>
    <DividerHorizontalIcon />
  </Checkbox.Indicator>
</Checkbox.Root>

Управление состоянием

Состояние может быть:

  • контролируемым
  • неконтролируемым

Для сложных интерфейсов используется контролируемый режим, поскольку состояние родительского чекбокса зависит от дочерних.


Контролируемый пример

function Example() {
  const [checked, setChecked] = React.useState("indeterminate")

  return (
    <Checkbox.Root
      checked={checked}
      onCheckedCha nge={setChecked}
    >
      <Checkbox.Indicator />
    </Checkbox.Root>
  )
}

Функция onCheckedChange получает новое состояние:

true
false
"indeterminate"

Логика вычисления промежуточного состояния

В интерфейсах с группами элементов indeterminate обычно вычисляется автоматически.

Пусть есть группа чекбоксов:

Parent
 ├ Child 1
 ├ Child 2
 └ Child 3

Правила вычисления:

Выбранные дочерние Состояние родителя
0 false
все true
часть indeterminate

Реализация

function CheckboxGroup() {
  const [items, setItems] = React.useState({
    a: false,
    b: false,
    c: false
  })

  const values = Object.values(items)

  const parentState =
    values.every(Boolean)
      ? true
      : values.some(Boolean)
      ? "indeterminate"
      : false

  return (
    <>
      <Checkbox.Root checked={parentState}>
        <Checkbox.Indicator />
      </Checkbox.Root>

      {Object.entries(items).map(([key, value]) => (
        <Checkbox.Root
          key={key}
          checked={value}
          onCheckedCha nge={(state) =>
            setItems(prev => ({
              ...prev,
              [key]: state === true
            }))
          }
        >
          <Checkbox.Indicator />
        </Checkbox.Root>
      ))}
    </>
  )
}

Поведение при клике

Особенность indeterminate заключается в том, что это не конечное состояние взаимодействия.

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

indeterminate → checked

При клике пользователь ожидает, что родительский элемент выберет все дочерние элементы.

Это поведение реализуется логикой приложения.


Типичная логика

if parent clicked:
    if state !== true:
        select all
    else:
        deselect all

Пример

function toggleAll() {
  const values = Object.values(items)
  const allChecked = values.every(Boolean)

  const next = Object.fromEntries(
    Object.keys(items).map(key => [key, !allChecked])
  )

  setItems(next)
}

Связь с ARIA

Radix UI автоматически добавляет правильные ARIA-атрибуты, что критически важно для доступности.

Для indeterminate используется:

aria-checked="mixed"

Это позволяет скринридерам корректно озвучивать состояние.

Пример DOM-разметки:

<button role="checkbox" aria-checked="mixed"></button>

Скринридер сообщает:

partially checked

Data-атрибут состояния

Radix UI добавляет атрибут data-state, позволяющий стилизовать компонент через CSS.

Состояние data-state
checked checked
unchecked unchecked
indeterminate indeterminate

Пример стилизации

[data-state="checked"] {
  background: black;
}

[data-state="indeterminate"] {
  background: gray;
}

Индикатор также может реагировать на состояние.


Индикатор только для выбранных состояний

Иногда требуется показывать разные иконки.

checked → галочка
indeterminate → линия

Пример

<Checkbox.Root checked={state}>
  <Checkbox.Indicator>
    {state === "indeterminate"
      ? <DividerHorizontalIcon />
      : <CheckIcon />
    }
  </Checkbox.Indicator>
</Checkbox.Root>

Промежуточное состояние в древовидных структурах

Одно из самых распространённых применений — tree view.

Пример структуры:

Documents
 ├ Work
 │   ├ Contract.pdf
 │   └ Report.docx
 └ Personal
     ├ Photo.jpg
     └ Notes.txt

Если выбран только Contract.pdf, то:

Work → indeterminate
Documents → indeterminate

Таким образом, состояние распространяется вверх по дереву.


Алгоритм обновления

  1. Пользователь изменяет дочерний элемент
  2. Пересчитывается состояние родителя
  3. Пересчёт продолжается вверх

Алгоритм:

function calculateState(children):
    if all checked:
        return true
    if none checked:
        return false
    return indeterminate

Оптимизация вычислений

В больших деревьях пересчёт может быть дорогим.

Оптимизации:

  • хранение количества выбранных элементов
  • мемоизация
  • вычисление только затронутых веток

Пример хранения счётчиков:

node.checkedCount
node.totalChildren

Состояние определяется быстро:

if checkedCount == 0 → false
if checkedCount == totalChildren → true
else → indeterminate

Синхронизация с серверными данными

Если состояние приходит с сервера, indeterminate также может быть частью модели данных.

Пример JSON:

{
  "permissions": {
    "read": true,
    "write": false,
    "admin": "indeterminate"
  }
}

Radix UI позволяет напрямую передать это значение в checked.


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

По умолчанию indeterminate не отправляется как значение формы, поскольку HTML-чекбоксы не имеют такого состояния.

Radix UI работает поверх кнопки (button), поэтому разработчик сам определяет поведение.

Распространённый подход:

true → "checked"
false → "unchecked"
indeterminate → "partial"

Управление через useState

Тип состояния:

type CheckedState = boolean | "indeterminate"

Это встроенный тип Radix.

Пример:

const [checked, setChecked] = React.useState<CheckedState>("indeterminate")

Частые ошибки

Ошибка 1. Использование null

Некоторые разработчики пытаются использовать:

null
undefined

Radix UI ожидает строгое значение:

"indeterminate"

Ошибка 2. Неверная логика родителя

Иногда родительский чекбокс устанавливается так:

checked = children.some(Boolean)

Это приводит к неправильному состоянию.

Корректная логика требует проверки:

every()
some()

Ошибка 3. Потеря состояния при клике

Если не обрабатывать onCheckedChange, компонент может выйти из indeterminate.

Необходимо явно контролировать состояние.


Стилизация разных состояний

Radix позволяет удобно оформлять состояния.

Пример:

.checkbox {
  border: 1px solid black;
}

.checkbox[data-state="checked"] {
  background: black;
}

.checkbox[data-state="indeterminate"] {
  background: #999;
}

Применение в интерфейсах таблиц

В таблицах часто используется чекбокс выбора всех строк.

[ ] Select All

Состояния:

Состояние Значение
unchecked ни одна строка
checked все строки
indeterminate выбраны некоторые

Radix UI делает реализацию такого поведения предсказуемой благодаря трёхзначной модели состояния.


Архитектурные преимущества

Поддержка indeterminate в Radix UI даёт несколько преимуществ:

  • единая модель состояния
  • корректная доступность (ARIA)
  • декларативное управление
  • возможность стилизации через data-state
  • независимость от DOM-чекбокса

Благодаря этому компонент можно использовать в сложных интерфейсах:

  • деревья файлов
  • панели разрешений
  • редакторы ролей
  • административные панели
  • интерфейсы управления данными.