@@ -50,8 +50,12 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
5050 node . indeterminate = indeterminate || false
5151 }
5252 } ,
53- // `checked` is intentionally included: browsers clear the indeterminate state
54- // when checked changes, so we need the callback to re-run to restore it.
53+ // `checked` is intentionally included even though it is not used inside
54+ // the callback: browsers silently clear the `indeterminate` property
55+ // whenever the checked state changes, so the callback must re-run to
56+ // restore it. The exhaustive-deps rule flags this as an unnecessary
57+ // dependency (because `checked` isn't referenced in the body), hence the
58+ // suppression below.
5559 // eslint-disable-next-line react-hooks/exhaustive-deps
5660 [ indeterminate , checked ] ,
5761 )
@@ -62,6 +66,10 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
6266 checkboxGroupContext . onChange && checkboxGroupContext . onChange ( e )
6367 onChange && onChange ( e )
6468 }
69+
70+ // aria-checked must reflect three states: mixed (indeterminate), true, or false.
71+ const ariaChecked = indeterminate ? ( 'mixed' as const ) : checked ? ( 'true' as const ) : ( 'false' as const )
72+
6573 const inputProps = {
6674 type : 'checkbox' ,
6775 disabled,
@@ -71,7 +79,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
7179 required,
7280 [ 'aria-required' ] : required ? ( 'true' as const ) : ( 'false' as const ) ,
7381 [ 'aria-invalid' ] : validationStatus === 'error' ? ( 'true' as const ) : ( 'false' as const ) ,
74- [ 'aria-checked' ] : indeterminate ? ( 'mixed' as const ) : checked ? ( 'true' as const ) : ( 'false' as const ) ,
82+ [ 'aria-checked' ] : ariaChecked ,
7583 onChange : handleOnChange ,
7684 value,
7785 name : value ,
0 commit comments