ui: add indeterminate state to Checkbox component

Add support for the indeterminate state in the Checkbox component,
displaying a horizontal dash icon instead of a checkmark. This is
useful for "select all" scenarios in tables where only some rows
are selected.

The indeterminate state maintains the unchecked visual style
(white background with border) while showing the dash icon.

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-01-19 16:39:08 +01:00
parent cfac8a460d
commit d2fddedd3e
6 changed files with 46 additions and 8 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/ui': patch
---
Added indeterminate state support to the Checkbox component for handling partial selection scenarios like table header checkboxes.
Affected components: Checkbox
+1
View File
@@ -460,6 +460,7 @@ export const CheckboxDefinition: {
};
readonly dataAttributes: {
readonly selected: readonly [true, false];
readonly indeterminate: readonly [true, false];
};
};
@@ -62,7 +62,14 @@
color: var(--bui-fg-solid);
}
.bui-Checkbox[data-hovered]:not([data-selected]) & {
.bui-Checkbox[data-indeterminate] & {
background-color: var(--bui-bg-surface-1);
box-shadow: inset 0 0 0 1px var(--bui-border);
color: var(--bui-fg-primary);
}
.bui-Checkbox[data-hovered]:not([data-selected]):not([data-indeterminate])
& {
box-shadow: inset 0 0 0 1px var(--bui-border-hover);
}
@@ -28,16 +28,27 @@ export const Default = meta.story({
},
});
export const Indeterminate = meta.story({
args: {
children: 'Select all',
isIndeterminate: true,
},
});
export const AllVariants = meta.story({
...Default.input,
render: () => (
<Flex direction="column" gap="2">
<Checkbox>Unchecked</Checkbox>
<Checkbox isSelected>Checked</Checkbox>
<Checkbox isIndeterminate>Indeterminate</Checkbox>
<Checkbox isDisabled>Disabled</Checkbox>
<Checkbox isSelected isDisabled>
Checked & Disabled
</Checkbox>
<Checkbox isIndeterminate isDisabled>
Indeterminate & Disabled
</Checkbox>
</Flex>
),
});
@@ -21,7 +21,7 @@ import { useStyles } from '../../hooks/useStyles';
import { CheckboxDefinition } from './definition';
import clsx from 'clsx';
import styles from './Checkbox.module.css';
import { RiCheckLine } from '@remixicon/react';
import { RiCheckLine, RiSubtractLine } from '@remixicon/react';
/** @public */
export const Checkbox = forwardRef<HTMLLabelElement, CheckboxProps>(
@@ -35,12 +35,23 @@ export const Checkbox = forwardRef<HTMLLabelElement, CheckboxProps>(
className={clsx(classNames.root, styles[classNames.root], className)}
{...rest}
>
<div
className={clsx(classNames.indicator, styles[classNames.indicator])}
>
<RiCheckLine size={12} />
</div>
{children}
{({ isIndeterminate }) => (
<>
<div
className={clsx(
classNames.indicator,
styles[classNames.indicator],
)}
>
{isIndeterminate ? (
<RiSubtractLine size={12} />
) : (
<RiCheckLine size={12} />
)}
</div>
{children}
</>
)}
</RACheckbox>
);
},
@@ -27,5 +27,6 @@ export const CheckboxDefinition = {
},
dataAttributes: {
selected: [true, false] as const,
indeterminate: [true, false] as const,
},
} as const satisfies ComponentDefinition;