Skip to content

CORE-2901: Add an indeterminate state to Checkbox - #163

Merged
RoyEJohnson merged 5 commits into
mainfrom
CORE-2901-checkbox-indeterminate
Oct 8, 2026
Merged

RoyEJohnson merged 5 commits into
mainfrom
CORE-2901-checkbox-indeterminate

Conversation

@RoyEJohnson

@RoyEJohnson RoyEJohnson commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Jira: CORE-2901

This is the first of several small PRs for CORE-2901; the plan is on the ticket. It adds the
Checkbox piece that the Assignable fix needs. Nothing in assignments changes here.

Why

A checkbox that summarises a set of others, like a group's checkbox above its items, can only
be checked or unchecked. When some items are chosen and some are not, assistive tech announces
a fully checked box (WCAG 4.1.2, Name, Role, Value). assignments' ActivityGroup has this
problem: its checkbox is checked as soon as any one item is selected.

What changes

Checkbox takes a new indeterminate prop.

  • It sets the input's indeterminate property, which browsers expose as the "mixed" state.
    There is no aria-checked to keep in sync.
  • It draws a dash in place of the checkmark, with a colour for each variant. It reuses the
    --checkbox-indeterminate-bg and --checkbox-indeterminate-icon variables that
    TreeCheckbox already sets, and the existing dash icon.
  • It does not change checked. The parent decides what a click on a mixed box does;
    selecting everything is what a native checkbox does.
  • A disabled indeterminate checkbox draws no dash, the same as a disabled checked one.
  • The property is reapplied after every render, because the browser clears it when the box
    is clicked. The prop stays the source of truth.

The variables are only set when indeterminate is true, so no existing snapshot changes.

Tests

New tests in Checkbox.spec.tsx check that:

  • the box is not indeterminate by default,
  • indeterminate leaves checked alone,
  • the property follows the prop in both directions,
  • it is restored after the browser clears it on a click,
  • each variant draws a dash, and a disabled one does not.

Making the effect run only when the prop changes fails the click test, and removing the dash
variable fails the variant tests. There are also two new stories: Indeterminate for every
variant and size, and ParentOfOptions, a parent that is mixed while only some options are
chosen.

I checked both stories in Chrome. The dash looks right for each variant and size. In
ParentOfOptions, clicking the mixed parent selects all, clicking it again clears all, and
picking one option returns it to mixed.

Before merging

Next

The assignments PR that uses this (ActivityGroup showing the mixed state) needs a release
that includes this change.

🤖 Generated with Claude Code

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The reviewed changes include comprehensive behavior, styling, and regression coverage with no unresolved blocking issues.

0 open findings

🧠 Review effort: Lite

RoyEJohnson and others added 4 commits October 7, 2026 12:23
A checkbox that summarises a set of others had no way to report that
only some of them are chosen, so assistive tech announced a fully
checked box when it was not.

Checkbox takes an `indeterminate` prop that sets the input's
`indeterminate` property, which browsers expose as the mixed state, and
draws a dash in place of the checkmark for each variant. The property is
reapplied after every render because the browser clears it on click.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The browser clears `indeterminate` when a checkbox is clicked, and the
effect that reapplies it only runs on a render. A mixed Checkbox whose
click did not cause a render, such as one with no state behind it,
stopped being exposed as mixed even though the prop was still true.

While the box is mixed, onClick now puts the property back after the
caller's own handler has run. When the parent clears the prop in
response to the click, the render that follows still applies it.

The Indeterminate story now covers every variant at every size.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The version bump was made with `npm install`, which dropped the `libc` constraints from the
six @swc/core-linux-* packages. With them, npm on Linux x64 glibc skips the musl binary;
without them it installs both. The lockfile now matches main apart from the two version
lines, which `npm version 1.26.0 --no-git-tag-version` changes on its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@RoyEJohnson
RoyEJohnson requested a review from Dantemss October 7, 2026 17:28

@Dantemss Dantemss left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indeterminate situation is crazy 😂

See the comment below about the checkbox's state potentially flashing but otherwise it seems fine. Well, as fine as this weird prop can be I guess.

Comment thread src/components/Checkbox/Checkbox.tsx Outdated
const inputRef = useRef<HTMLInputElement>(null);

// `indeterminate` is a DOM property with no attribute, so it is applied after every render.
useEffect(() => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently this might have to be a useLayoutEffect to prevent the checkbox state flashing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 4eddd79. The effect is now a layout effect, so the property is set before paint. It falls back to useEffect when there is no window, because React warns about useLayoutEffect during server rendering.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@RoyEJohnson
RoyEJohnson merged commit 5eceeaa into main Oct 8, 2026
3 checks passed
@RoyEJohnson
RoyEJohnson deleted the CORE-2901-checkbox-indeterminate branch October 8, 2026 14:54

This branch was successfully deployed

1 active deployment
refs/heads/CORE-2901-checkbox-indeterminate — 4eddd798 Deployed Oct 8, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants