В интерфейсах с иерархическими наборами данных часто возникает
ситуация, когда родительский элемент связан с несколькими дочерними
элементами выбора. Если часть дочерних элементов выбрана, а часть — нет,
возникает состояние, которое нельзя корректно представить обычным
логическим значением true или false. Для этого
используется промежуточное состояние —
indeterminate.
В библиотеке Radix UI такое состояние поддерживается в компонентах
выбора, прежде всего в Checkbox. Оно позволяет явно
показывать пользователю, что выбран только некоторый поднабор
элементов.
indeterminateТрадиционный чекбокс работает в бинарной логике:
| Состояние | Значение |
|---|---|
checked |
элемент выбран |
unchecked |
элемент не выбран |
Однако в интерфейсах со списками вложенных элементов бинарной логики недостаточно. Появляется третье состояние.
| Состояние | Значение |
|---|---|
true |
выбраны все элементы |
false |
не выбран ни один |
indeterminate |
выбрана только часть |
Такое поведение характерно для:
Radix UI реализует это состояние на уровне API компонента, позволяя управлять им декларативно.
Компонент 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)
}
Radix UI автоматически добавляет правильные ARIA-атрибуты, что критически важно для доступности.
Для indeterminate используется:
aria-checked="mixed"
Это позволяет скринридерам корректно озвучивать состояние.
Пример DOM-разметки:
<button role="checkbox" aria-checked="mixed"></button>
Скринридер сообщает:
partially checked
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
Таким образом, состояние распространяется вверх по дереву.
Алгоритм:
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")
nullНекоторые разработчики пытаются использовать:
null
undefined
Radix UI ожидает строгое значение:
"indeterminate"
Иногда родительский чекбокс устанавливается так:
checked = children.some(Boolean)
Это приводит к неправильному состоянию.
Корректная логика требует проверки:
every()
some()
Если не обрабатывать 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 даёт несколько
преимуществ:
data-stateБлагодаря этому компонент можно использовать в сложных интерфейсах: