diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/css/index.js b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/css/index.js
new file mode 100644
index 0000000000..31331694ff
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/css/index.js
@@ -0,0 +1,152 @@
+'use client';
+
+import * as React from 'react';
+import * as Checkbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledCheckboxIndeterminateGroup() {
+ return (
+
+
+
+ Colors
+
+
+
+
+
+
+
+ Red
+
+
+
+
+
+
+
+ Green
+
+
+
+
+
+
+
+ Blue
+
+
+
+
+
+
+ );
+}
+
+const grey = {
+ 100: '#E5EAF2',
+ 300: '#C7D0DD',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+function Styles() {
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/css/index.tsx b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/css/index.tsx
new file mode 100644
index 0000000000..31331694ff
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/css/index.tsx
@@ -0,0 +1,152 @@
+'use client';
+
+import * as React from 'react';
+import * as Checkbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledCheckboxIndeterminateGroup() {
+ return (
+
+
+
+ Colors
+
+
+
+
+
+
+
+ Red
+
+
+
+
+
+
+
+ Green
+
+
+
+
+
+
+
+ Blue
+
+
+
+
+
+
+ );
+}
+
+const grey = {
+ 100: '#E5EAF2',
+ 300: '#C7D0DD',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+function Styles() {
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/system/index.js b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/system/index.js
new file mode 100644
index 0000000000..26b73c5315
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/system/index.js
@@ -0,0 +1,125 @@
+'use client';
+
+import * as React from 'react';
+import { css, styled } from '@mui/system';
+import * as BaseCheckbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledCheckboxIndeterminateGroup() {
+ return (
+
+
+
+ Colors
+
+
+
+
+
+
+
+ Red
+
+
+
+
+
+
+
+ Green
+
+
+
+
+
+
+
+ Blue
+
+
+
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+ 400: '#B0B8C4',
+ 800: '#303740',
+};
+
+const labelStyles = css`
+ display: flex;
+ gap: 8px;
+ margin-bottom: 8px;
+`;
+
+const FieldRoot = styled(Field.Root)`
+ display: flex;
+`;
+
+const CheckboxLabel = styled(Field.Label)`
+ ${labelStyles}
+ padding-left: 8px;
+`;
+
+const CheckboxGroupLabel = styled(Field.Label)`
+ font-size: 17px;
+ font-weight: bold;
+ ${labelStyles}
+`;
+
+const Checkbox = styled(BaseCheckbox.Root)(
+ ({ theme }) => `
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border-radius: 4px;
+ border: 2px solid ${blue[600]};
+ background: none;
+ transition-property: background, border-color;
+ transition-duration: 0.15s;
+ outline: none;
+
+ &[data-disabled] {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ &:focus-visible {
+ outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]};
+ outline-offset: 2px;
+ }
+
+ &[data-state="checked"], &[data-state="mixed"] {
+ border-color: transparent;
+ background: ${blue[600]};
+ }
+ `,
+);
+
+const CheckIcon = styled(Check)`
+ height: 100%;
+ width: 100%;
+`;
+
+const Indicator = styled(BaseCheckbox.Indicator)`
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+ color: ${grey[100]};
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/system/index.tsx b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/system/index.tsx
new file mode 100644
index 0000000000..26b73c5315
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/system/index.tsx
@@ -0,0 +1,125 @@
+'use client';
+
+import * as React from 'react';
+import { css, styled } from '@mui/system';
+import * as BaseCheckbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledCheckboxIndeterminateGroup() {
+ return (
+
+
+
+ Colors
+
+
+
+
+
+
+
+ Red
+
+
+
+
+
+
+
+ Green
+
+
+
+
+
+
+
+ Blue
+
+
+
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+ 400: '#B0B8C4',
+ 800: '#303740',
+};
+
+const labelStyles = css`
+ display: flex;
+ gap: 8px;
+ margin-bottom: 8px;
+`;
+
+const FieldRoot = styled(Field.Root)`
+ display: flex;
+`;
+
+const CheckboxLabel = styled(Field.Label)`
+ ${labelStyles}
+ padding-left: 8px;
+`;
+
+const CheckboxGroupLabel = styled(Field.Label)`
+ font-size: 17px;
+ font-weight: bold;
+ ${labelStyles}
+`;
+
+const Checkbox = styled(BaseCheckbox.Root)(
+ ({ theme }) => `
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border-radius: 4px;
+ border: 2px solid ${blue[600]};
+ background: none;
+ transition-property: background, border-color;
+ transition-duration: 0.15s;
+ outline: none;
+
+ &[data-disabled] {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ &:focus-visible {
+ outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]};
+ outline-offset: 2px;
+ }
+
+ &[data-state="checked"], &[data-state="mixed"] {
+ border-color: transparent;
+ background: ${blue[600]};
+ }
+ `,
+);
+
+const CheckIcon = styled(Check)`
+ height: 100%;
+ width: 100%;
+`;
+
+const Indicator = styled(BaseCheckbox.Indicator)`
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+ color: ${grey[100]};
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/tailwind/index.js b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/tailwind/index.js
new file mode 100644
index 0000000000..41a8de1373
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/tailwind/index.js
@@ -0,0 +1,126 @@
+'use client';
+
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useTheme } from '@mui/system';
+import * as BaseCheckbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import Check from '@mui/icons-material/Check';
+
+function classNames(...classes) {
+ return classes.filter(Boolean).join(' ');
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+function Label(props) {
+ return (
+ // eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/no-noninteractive-element-interactions
+ e.preventDefault()}
+ {...props}
+ />
+ );
+}
+
+export default function UnstyledCheckboxIntroduction() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+
+
+ Colors
+
+
+
+
+
+
+
+ Red
+
+
+
+
+
+
+
+ Green
+
+
+
+
+
+
+
+ Blue
+
+
+
+
+
+ );
+}
+
+const Checkbox = React.forwardRef(function Checkbox(props, ref) {
+ return (
+
+ classNames(
+ 'w-6 h-6 p-0 rounded-md',
+ 'border-2 border-solid border-purple-500',
+ 'outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 focus-visible:ring-opacity-60',
+ 'transition-colors duration-150',
+ state.disabled && 'opacity-40 cursor-not-allowed',
+ state.checked && 'bg-purple-500',
+ !state.checked && 'bg-transparent',
+ typeof props.className === 'function'
+ ? props.className(state)
+ : props.className,
+ )
+ }
+ />
+ );
+});
+
+Checkbox.propTypes = {
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+};
+
+const Indicator = React.forwardRef(function Indicator(props, ref) {
+ return (
+
+ classNames(
+ 'h-full inline-block invisible data-[state=checked]:visible text-gray-100',
+ typeof props.className === 'function'
+ ? props.className(state)
+ : props.className,
+ )
+ }
+ >
+
+
+ );
+});
+
+Indicator.propTypes = {
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+};
diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/tailwind/index.tsx b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/tailwind/index.tsx
new file mode 100644
index 0000000000..e6f2e72b61
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupIntroduction/tailwind/index.tsx
@@ -0,0 +1,115 @@
+'use client';
+
+import * as React from 'react';
+import { useTheme } from '@mui/system';
+import * as BaseCheckbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import Check from '@mui/icons-material/Check';
+
+function classNames(...classes: Array) {
+ return classes.filter(Boolean).join(' ');
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+function Label(props: React.ComponentPropsWithoutRef<'label'>) {
+ return (
+ // eslint-disable-next-line jsx-a11y/label-has-associated-control, jsx-a11y/no-noninteractive-element-interactions
+ e.preventDefault()}
+ {...props}
+ />
+ );
+}
+
+export default function UnstyledCheckboxIntroduction() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+
+
+ Colors
+
+
+
+
+
+
+
+ Red
+
+
+
+
+
+
+
+ Green
+
+
+
+
+
+
+
+ Blue
+
+
+
+
+
+ );
+}
+
+const Checkbox = React.forwardRef(
+ function Checkbox(props, ref) {
+ return (
+
+ classNames(
+ 'w-6 h-6 p-0 rounded-md',
+ 'border-2 border-solid border-purple-500',
+ 'outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-purple-500 focus-visible:ring-opacity-60',
+ 'transition-colors duration-150',
+ state.disabled && 'opacity-40 cursor-not-allowed',
+ state.checked && 'bg-purple-500',
+ !state.checked && 'bg-transparent',
+ typeof props.className === 'function'
+ ? props.className(state)
+ : props.className,
+ )
+ }
+ />
+ );
+ },
+);
+
+const Indicator = React.forwardRef(
+ function Indicator(props, ref) {
+ return (
+
+ classNames(
+ 'h-full inline-block invisible data-[state=checked]:visible text-gray-100',
+ typeof props.className === 'function'
+ ? props.className(state)
+ : props.className,
+ )
+ }
+ >
+
+
+ );
+ },
+);
diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupNested.js b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupNested.js
new file mode 100644
index 0000000000..d7ce96b207
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupNested.js
@@ -0,0 +1,152 @@
+'use client';
+
+import * as React from 'react';
+import * as BaseCheckbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import { styled } from '@mui/system';
+import HorizontalRule from '@mui/icons-material/HorizontalRule';
+import Check from '@mui/icons-material/Check';
+
+const colors = ['red', 'green', 'blue'];
+
+export default function UnstyledCheckboxGroupNested() {
+ const [value, setValue] = React.useState([]);
+
+ return (
+
+
+ Colors
+ }>
+
+ (
+
+ {indeterminate ? : }
+
+ )}
+ />
+
+ All Colors
+
+
+ {colors.map((color) => (
+ }>
+
+
+
+
+
+ {color}
+
+ ))}
+
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+ 400: '#B0B8C4',
+ 800: '#303740',
+};
+
+const CheckboxGroupLabel = styled(Field.Label)`
+ display: block;
+ font-weight: bold;
+ font-size: 18px;
+ margin-bottom: 8px;
+`;
+
+const Checkbox = styled(BaseCheckbox.Root)(
+ ({ theme }) => `
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border-radius: 4px;
+ border: 2px solid ${blue[600]};
+ background: none;
+ transition-property: background, border-color;
+ transition-duration: 0.15s;
+ outline: none;
+
+ &[data-disabled] {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ &:focus-visible {
+ outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]};
+ outline-offset: 2px;
+ }
+
+ &[data-state="checked"], &[data-state="mixed"] {
+ border-color: transparent;
+ background: ${blue[600]};
+ }
+ `,
+);
+
+const HorizontalRuleIcon = styled(HorizontalRule)`
+ height: 100%;
+ width: 100%;
+`;
+
+const CheckIcon = styled(Check)`
+ height: 100%;
+ width: 100%;
+`;
+
+const Indicator = styled(BaseCheckbox.Indicator)`
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+ color: ${grey[100]};
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
+
+const FieldRoot = styled(Field.Root)`
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ padding: 0;
+`;
+
+const List = styled('ul')`
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ margin-left: 32px;
+`;
+
+const FieldListItem = styled(Field.Root)`
+ display: flex;
+ align-items: center;
+
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
+`;
+
+const CheckboxLabel = styled(Field.Label)`
+ display: flex;
+ gap: 8px;
+ text-transform: capitalize;
+ padding-left: 8px;
+`;
diff --git a/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupNested.tsx b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupNested.tsx
new file mode 100644
index 0000000000..a19cf961c3
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/UnstyledCheckboxGroupNested.tsx
@@ -0,0 +1,152 @@
+'use client';
+
+import * as React from 'react';
+import * as BaseCheckbox from '@base_ui/react/Checkbox';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Field from '@base_ui/react/Field';
+import { styled } from '@mui/system';
+import HorizontalRule from '@mui/icons-material/HorizontalRule';
+import Check from '@mui/icons-material/Check';
+
+const colors = ['red', 'green', 'blue'];
+
+export default function UnstyledCheckboxGroupNested() {
+ const [value, setValue] = React.useState([]);
+
+ return (
+
+
+ Colors
+ }>
+
+ (
+
+ {indeterminate ? : }
+
+ )}
+ />
+
+ All Colors
+
+
+ {colors.map((color) => (
+ }>
+
+
+
+
+
+ {color}
+
+ ))}
+
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+ 400: '#B0B8C4',
+ 800: '#303740',
+};
+
+const CheckboxGroupLabel = styled(Field.Label)`
+ display: block;
+ font-weight: bold;
+ font-size: 18px;
+ margin-bottom: 8px;
+`;
+
+const Checkbox = styled(BaseCheckbox.Root)(
+ ({ theme }) => `
+ width: 24px;
+ height: 24px;
+ padding: 0;
+ border-radius: 4px;
+ border: 2px solid ${blue[600]};
+ background: none;
+ transition-property: background, border-color;
+ transition-duration: 0.15s;
+ outline: none;
+
+ &[data-disabled] {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ &:focus-visible {
+ outline: 2px solid ${theme.palette.mode === 'dark' ? blue[800] : blue[400]};
+ outline-offset: 2px;
+ }
+
+ &[data-state="checked"], &[data-state="mixed"] {
+ border-color: transparent;
+ background: ${blue[600]};
+ }
+ `,
+);
+
+const HorizontalRuleIcon = styled(HorizontalRule)`
+ height: 100%;
+ width: 100%;
+`;
+
+const CheckIcon = styled(Check)`
+ height: 100%;
+ width: 100%;
+`;
+
+const Indicator = styled(BaseCheckbox.Indicator)`
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+ color: ${grey[100]};
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
+
+const FieldRoot = styled(Field.Root)`
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+ padding: 0;
+`;
+
+const List = styled('ul')`
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ margin-left: 32px;
+`;
+
+const FieldListItem = styled(Field.Root)`
+ display: flex;
+ align-items: center;
+
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
+`;
+
+const CheckboxLabel = styled(Field.Label)`
+ display: flex;
+ gap: 8px;
+ text-transform: capitalize;
+ padding-left: 8px;
+`;
diff --git a/docs/data/base/components/checkbox-group/checkbox-group.md b/docs/data/base/components/checkbox-group/checkbox-group.md
new file mode 100644
index 0000000000..d20c5af0f1
--- /dev/null
+++ b/docs/data/base/components/checkbox-group/checkbox-group.md
@@ -0,0 +1,136 @@
+---
+productId: base-ui
+title: React Checkbox Group component and hook
+components: CheckboxGroupRoot
+githubLabel: 'component: checkbox'
+waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/
+packageName: '@base_ui/react'
+---
+
+# Checkbox Group
+
+Checkbox Groups combine a series of checkboxes together.
+
+{{"component": "@mui/docs/ComponentLinkHeader", "design": false}}
+
+{{"component": "modules/components/ComponentPageTabs.js"}}
+
+{{"demo": "UnstyledCheckboxGroupIntroduction", "defaultCodeOpen": false, "bg": "gradient"}}
+
+## Installation
+
+Base UI components are all available as a single package.
+
+
+
+```bash npm
+npm install @base_ui/react
+```
+
+```bash yarn
+yarn add @base_ui/react
+```
+
+```bash pnpm
+pnpm add @base_ui/react
+```
+
+
+
+Once you have the package installed, import the components:
+
+```ts
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Checkbox from '@base_ui/react/Checkbox';
+```
+
+## Anatomy
+
+Checkbox Group is composed of a `Root` component and `Checkbox` components:
+
+- ` ` renders a `` with a `group` role.
+- `
` renders an individual `
` checkbox.
+
+```tsx
+
+
+
+
+```
+
+## Labeling
+
+`Field` components are used to label the Checkbox Group and individual Checkboxes:
+
+```jsx
+import * as Field from '@base_ui/react/Field';
+```
+
+```tsx
+
+
+ Colors
+
+
+ Red
+
+
+
+ Blue
+
+
+
+```
+
+## Controlled
+
+The `value` and `onValueChange` props control the Checkbox Group with external state. `value` is an array of strings matching the `name` props of Checkboxes that are currently checked:
+
+```jsx
+const [value, setValue] = useState(['red']);
+
+return (
+
+ {/* Checked */}
+
+
+
+);
+```
+
+## Parent Checkbox
+
+A Checkbox can control a group of child Checkboxes.
+
+1. Make `CheckboxGroup.Root` controlled and add `allValues` as a prop — an array of strings that contains the names of all the child checkboxes.
+2. Add a `parent` prop to the `Checkbox.Root` component that controls the other (child) Checkboxes inside the group.
+3. Give the child Checkboxes a `name` prop that identifies them inside the `allValues` array.
+
+```jsx
+const allValues = ['a', 'b', 'c'];
+
+function App() {
+ const [value, setValue] = useState([]);
+ return (
+
+
+ {allValues.map((value) => (
+
+ ))}
+
+ );
+}
+```
+
+{{"demo": "UnstyledCheckboxGroupNested.js"}}
+
+To preserve the initial state of the child checkboxes when the parent checkbox is toggled, set the `preserveChildStates` prop to `true`:
+
+```tsx
+
+
+ {allValues.map((value) => (
+
+ ))}
+
+```
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.js b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.js
index cf9ef46b15..2cd48c2a8c 100644
--- a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.js
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.js
@@ -7,10 +7,11 @@ import Check from '@mui/icons-material/Check';
const colors = ['Red', 'Green', 'Blue'];
export default function UnstyledCheckboxIndeterminateGroup() {
- const [checkedValues, setCheckedValues] = React.useState([false, true, false]);
+ const [checkedValues, setCheckedValues] = React.useState(['Green']);
- const isChecked = checkedValues.every((value) => value);
- const isIndeterminate = checkedValues.some((value) => value) && !isChecked;
+ const isChecked = checkedValues.length === colors.length;
+ const isIndeterminate =
+ checkedValues.length !== colors.length && checkedValues.length > 0;
const id = React.useId();
@@ -23,7 +24,7 @@ export default function UnstyledCheckboxIndeterminateGroup() {
indeterminate={isIndeterminate}
checked={isChecked}
onCheckedChange={(checked) => {
- setCheckedValues([checked, checked, checked]);
+ setCheckedValues(checked ? colors : []);
}}
>
@@ -35,14 +36,18 @@ export default function UnstyledCheckboxIndeterminateGroup() {
- {colors.map((color, index) => (
+ {colors.map((color) => (
{
const newCheckedValues = [...checkedValues];
- newCheckedValues[index] = checked;
+ if (checked) {
+ newCheckedValues.push(color);
+ } else {
+ newCheckedValues.splice(newCheckedValues.indexOf(color), 1);
+ }
setCheckedValues(newCheckedValues);
}}
>
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.tsx b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.tsx
index cf9ef46b15..2cd48c2a8c 100644
--- a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.tsx
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.tsx
@@ -7,10 +7,11 @@ import Check from '@mui/icons-material/Check';
const colors = ['Red', 'Green', 'Blue'];
export default function UnstyledCheckboxIndeterminateGroup() {
- const [checkedValues, setCheckedValues] = React.useState([false, true, false]);
+ const [checkedValues, setCheckedValues] = React.useState(['Green']);
- const isChecked = checkedValues.every((value) => value);
- const isIndeterminate = checkedValues.some((value) => value) && !isChecked;
+ const isChecked = checkedValues.length === colors.length;
+ const isIndeterminate =
+ checkedValues.length !== colors.length && checkedValues.length > 0;
const id = React.useId();
@@ -23,7 +24,7 @@ export default function UnstyledCheckboxIndeterminateGroup() {
indeterminate={isIndeterminate}
checked={isChecked}
onCheckedChange={(checked) => {
- setCheckedValues([checked, checked, checked]);
+ setCheckedValues(checked ? colors : []);
}}
>
@@ -35,14 +36,18 @@ export default function UnstyledCheckboxIndeterminateGroup() {
- {colors.map((color, index) => (
+ {colors.map((color) => (
{
const newCheckedValues = [...checkedValues];
- newCheckedValues[index] = checked;
+ if (checked) {
+ newCheckedValues.push(color);
+ } else {
+ newCheckedValues.splice(newCheckedValues.indexOf(color), 1);
+ }
setCheckedValues(newCheckedValues);
}}
>
diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts
index 9fa25d9d1d..3c94de0892 100644
--- a/docs/data/base/pages.ts
+++ b/docs/data/base/pages.ts
@@ -23,6 +23,7 @@ const pages: readonly MuiPage[] = [
children: [
// { pathname: '/base-ui/react-autocomplete', title: 'Autocomplete' },
{ pathname: '/base-ui/react-checkbox', title: 'Checkbox' },
+ { pathname: '/base-ui/react-checkbox-group', title: 'Checkbox Group' },
{ pathname: '/base-ui/react-number-field', title: 'Number Field' },
// { pathname: '/base-ui/react-radio-group', title: 'Radio Group', planned: true },
// { pathname: '/base-ui/react-select', title: 'Select' },
diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js
index 499fad712c..beafae6595 100644
--- a/docs/data/base/pagesApi.js
+++ b/docs/data/base/pagesApi.js
@@ -27,6 +27,10 @@ module.exports = [
pathname: '/base-ui/react-alert-dialog/components-api/#alert-dialog-trigger',
title: 'AlertDialogTrigger',
},
+ {
+ pathname: '/base-ui/react-checkbox-group/components-api/#checkbox-group-root',
+ title: 'CheckboxGroupRoot',
+ },
{
pathname: '/base-ui/react-checkbox/components-api/#checkbox-indicator',
title: 'CheckboxIndicator',
diff --git a/docs/pages/base-ui/api/checkbox-group-root.json b/docs/pages/base-ui/api/checkbox-group-root.json
new file mode 100644
index 0000000000..c6598e663f
--- /dev/null
+++ b/docs/pages/base-ui/api/checkbox-group-root.json
@@ -0,0 +1,25 @@
+{
+ "props": {
+ "allValues": { "type": { "name": "arrayOf", "description": "Array<string>" } },
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "defaultValue": { "type": { "name": "arrayOf", "description": "Array<string>" } },
+ "disabled": { "type": { "name": "bool" }, "default": "false" },
+ "onValueChange": { "type": { "name": "func" } },
+ "preserveChildStates": { "type": { "name": "bool" }, "default": "false" },
+ "render": { "type": { "name": "union", "description": "element | func" } },
+ "value": { "type": { "name": "arrayOf", "description": "Array<string>" } }
+ },
+ "name": "CheckboxGroupRoot",
+ "imports": [
+ "import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';\nconst CheckboxGroupRoot = CheckboxGroup.Root;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "CheckboxGroupRoot",
+ "forwardsRefTo": "HTMLDivElement",
+ "filename": "/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRoot.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/checkbox-root.json b/docs/pages/base-ui/api/checkbox-root.json
index 1c781a51cb..6e43c832b5 100644
--- a/docs/pages/base-ui/api/checkbox-root.json
+++ b/docs/pages/base-ui/api/checkbox-root.json
@@ -14,10 +14,11 @@
"onCheckedChange": {
"type": { "name": "func" },
"signature": {
- "type": "function(checked: boolean, event: React.ChangeEvent) => void",
+ "type": "function(checked: boolean, event: Event) => void",
"describedArgs": ["checked", "event"]
}
},
+ "parent": { "type": { "name": "bool" }, "default": "false" },
"readOnly": { "type": { "name": "bool" }, "default": "false" },
"render": { "type": { "name": "union", "description": "element | func" } },
"required": { "type": { "name": "bool" }, "default": "false" }
diff --git a/docs/pages/base-ui/api/use-checkbox-group-parent.json b/docs/pages/base-ui/api/use-checkbox-group-parent.json
new file mode 100644
index 0000000000..759c3c4ac8
--- /dev/null
+++ b/docs/pages/base-ui/api/use-checkbox-group-parent.json
@@ -0,0 +1,8 @@
+{
+ "parameters": {},
+ "returnValue": {},
+ "name": "useCheckboxGroupParent",
+ "filename": "/packages/mui-base/src/CheckboxGroup/Parent/useCheckboxGroupParent.ts",
+ "imports": ["import { useCheckboxGroupParent } from '@base_ui/react/CheckboxGroup';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-checkbox-group-root.json b/docs/pages/base-ui/api/use-checkbox-group-root.json
new file mode 100644
index 0000000000..cfdfa64a7d
--- /dev/null
+++ b/docs/pages/base-ui/api/use-checkbox-group-root.json
@@ -0,0 +1,8 @@
+{
+ "parameters": {},
+ "returnValue": {},
+ "name": "useCheckboxGroupRoot",
+ "filename": "/packages/mui-base/src/CheckboxGroup/Root/useCheckboxGroupRoot.tsx",
+ "imports": ["import { useCheckboxGroupRoot } from '@base_ui/react/CheckboxGroup';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-checkbox-root.json b/docs/pages/base-ui/api/use-checkbox-root.json
index 6eef9e5384..9626eece43 100644
--- a/docs/pages/base-ui/api/use-checkbox-root.json
+++ b/docs/pages/base-ui/api/use-checkbox-root.json
@@ -21,10 +21,11 @@
"name": { "type": { "name": "string", "description": "string" }, "default": "undefined" },
"onCheckedChange": {
"type": {
- "name": "(checked: boolean, event: React.ChangeEvent<HTMLInputElement>) => void",
- "description": "(checked: boolean, event: React.ChangeEvent<HTMLInputElement>) => void"
+ "name": "(checked: boolean, event: Event) => void",
+ "description": "(checked: boolean, event: Event) => void"
}
},
+ "parent": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" },
"readOnly": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" },
"required": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }
},
diff --git a/docs/pages/base-ui/react-checkbox-group/[docsTab]/index.js b/docs/pages/base-ui/react-checkbox-group/[docsTab]/index.js
new file mode 100644
index 0000000000..f439ceeb3e
--- /dev/null
+++ b/docs/pages/base-ui/react-checkbox-group/[docsTab]/index.js
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/checkbox-group/checkbox-group.md?@mui/markdown';
+import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+import CheckboxGroupRootApiJsonPageContent from '../../api/checkbox-group-root.json';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
+
+export const getStaticPaths = () => {
+ return {
+ paths: [{ params: { docsTab: 'components-api' } }, { params: { docsTab: 'hooks-api' } }],
+ fallback: false, // can also be true or 'blocking'
+ };
+};
+
+export const getStaticProps = () => {
+ const CheckboxGroupRootApiReq = require.context(
+ 'docs-base/translations/api-docs/checkbox-group-root',
+ false,
+ /\.\/checkbox-group-root.*.json$/,
+ );
+ const CheckboxGroupRootApiDescriptions = mapApiPageTranslations(CheckboxGroupRootApiReq);
+
+ return {
+ props: {
+ componentsApiDescriptions: { CheckboxGroupRoot: CheckboxGroupRootApiDescriptions },
+ componentsApiPageContents: { CheckboxGroupRoot: CheckboxGroupRootApiJsonPageContent },
+ hooksApiDescriptions: {},
+ hooksApiPageContents: {},
+ },
+ };
+};
diff --git a/docs/pages/base-ui/react-checkbox-group/index.js b/docs/pages/base-ui/react-checkbox-group/index.js
new file mode 100644
index 0000000000..a6fd05a9dd
--- /dev/null
+++ b/docs/pages/base-ui/react-checkbox-group/index.js
@@ -0,0 +1,13 @@
+import * as React from 'react';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocsV2';
+import AppFrame from 'docs/src/modules/components/AppFrame';
+import * as pageProps from 'docs-base/data/base/components/checkbox-group/checkbox-group.md?@mui/markdown';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page} ;
+};
diff --git a/docs/translations/api-docs/checkbox-group-root/checkbox-group-root.json b/docs/translations/api-docs/checkbox-group-root/checkbox-group-root.json
new file mode 100644
index 0000000000..b94a0873b5
--- /dev/null
+++ b/docs/translations/api-docs/checkbox-group-root/checkbox-group-root.json
@@ -0,0 +1,24 @@
+{
+ "componentDescription": "The foundation for building custom-styled checkbox groups.",
+ "propDescriptions": {
+ "allValues": { "description": "All values of the checkboxes in the group." },
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "defaultValue": {
+ "description": "The default checked values of the checkbox group. Use when uncontrolled."
+ },
+ "disabled": { "description": "Whether the checkbox group is disabled." },
+ "onValueChange": {
+ "description": "A callback function that is called when the value of the checkbox group changes. Use when controlled."
+ },
+ "preserveChildStates": {
+ "description": "Whether the parent checkbox should preserve its child states when checked/unchecked, leading to a tri-state checkbox group."
+ },
+ "render": { "description": "A function to customize rendering of the component." },
+ "value": {
+ "description": "The currently checked values of the checkbox group. Use when controlled."
+ }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/checkbox-root/checkbox-root.json b/docs/translations/api-docs/checkbox-root/checkbox-root.json
index 32b2ed1a49..5e949ea2b6 100644
--- a/docs/translations/api-docs/checkbox-root/checkbox-root.json
+++ b/docs/translations/api-docs/checkbox-root/checkbox-root.json
@@ -21,6 +21,9 @@
"event": "The event source of the callback."
}
},
+ "parent": {
+ "description": "If true
, the checkbox is a parent checkbox for a group of child checkboxes."
+ },
"readOnly": { "description": "If true
, the component is read only." },
"render": { "description": "A function to customize rendering of the component." },
"required": {
diff --git a/docs/translations/api-docs/use-checkbox-group-parent/use-checkbox-group-parent.json b/docs/translations/api-docs/use-checkbox-group-parent/use-checkbox-group-parent.json
new file mode 100644
index 0000000000..e3eb65c6e4
--- /dev/null
+++ b/docs/translations/api-docs/use-checkbox-group-parent/use-checkbox-group-parent.json
@@ -0,0 +1 @@
+{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} }
diff --git a/docs/translations/api-docs/use-checkbox-group-root/use-checkbox-group-root.json b/docs/translations/api-docs/use-checkbox-group-root/use-checkbox-group-root.json
new file mode 100644
index 0000000000..e3eb65c6e4
--- /dev/null
+++ b/docs/translations/api-docs/use-checkbox-group-root/use-checkbox-group-root.json
@@ -0,0 +1 @@
+{ "hookDescription": "", "parametersDescriptions": {}, "returnValueDescriptions": {} }
diff --git a/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json b/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json
index 5cc0725600..199905bc97 100644
--- a/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json
+++ b/docs/translations/api-docs/use-checkbox-root/use-checkbox-root.json
@@ -12,6 +12,9 @@
"inputRef": { "description": "The ref to the input element." },
"name": { "description": "Name of the underlying input element." },
"onCheckedChange": { "description": "Callback fired when the checked state is changed." },
+ "parent": {
+ "description": "If true
, the checkbox is a parent checkbox for a group of child checkboxes."
+ },
"readOnly": { "description": "If true
, the component is read only." },
"required": {
"description": "If true
, the input
element is required."
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index 20c6cc2adf..bd0909efd2 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -221,6 +221,7 @@
"/base-ui/all-components": "All components",
"inputs": "Inputs",
"/base-ui/react-checkbox": "Checkbox",
+ "/base-ui/react-checkbox-group": "Checkbox Group",
"/base-ui/react-number-field": "Number Field",
"/base-ui/react-slider": "Slider",
"/base-ui/react-switch": "Switch",
diff --git a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx
index 2deb7fa1c0..9b6ce2f4f5 100644
--- a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx
+++ b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.tsx
@@ -8,6 +8,7 @@ import { resolveClassName } from '../../utils/resolveClassName';
import { evaluateRenderProp } from '../../utils/evaluateRenderProp';
import { useRenderPropForkRef } from '../../utils/useRenderPropForkRef';
import { defaultRenderFunctions } from '../../utils/defaultRenderFunctions';
+import { useCheckboxGroupRootContext } from '../../CheckboxGroup/Root/CheckboxGroupRootContext';
import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
/**
@@ -29,6 +30,7 @@ const CheckboxRoot = React.forwardRef(function CheckboxRoot(
name,
onCheckedChange,
defaultChecked,
+ parent = false,
readOnly = false,
indeterminate = false,
required = false,
@@ -40,7 +42,34 @@ const CheckboxRoot = React.forwardRef(function CheckboxRoot(
} = props;
const render = renderProp ?? defaultRenderFunctions.button;
- const { checked, getInputProps, getButtonProps } = useCheckboxRoot(props);
+ const groupContext = useCheckboxGroupRootContext();
+ const isGrouped = groupContext?.parent && groupContext.allValues;
+
+ let groupProps: Partial> = {};
+ if (isGrouped) {
+ if (parent) {
+ groupProps = groupContext.parent.getParentProps();
+ } else if (name) {
+ groupProps = groupContext.parent.getChildProps(name);
+ }
+ }
+
+ const {
+ checked: groupChecked = checkedProp,
+ indeterminate: groupIndeterminate = indeterminate,
+ onCheckedChange: groupOnChange = onCheckedChange,
+ ...otherGroupProps
+ } = groupProps;
+
+ const { checked, getInputProps, getButtonProps } = useCheckboxRoot({
+ ...props,
+ checked: groupChecked,
+ indeterminate: groupIndeterminate,
+ onCheckedChange: groupOnChange,
+ });
+
+ const computedChecked = isGrouped ? Boolean(groupChecked) : checked;
+ const computedIndeterminate = isGrouped ? groupIndeterminate : indeterminate;
const { ownerState: fieldOwnerState, disabled: fieldDisabled } = useFieldRootContext();
const disabled = fieldDisabled || disabledProp;
@@ -48,13 +77,13 @@ const CheckboxRoot = React.forwardRef(function CheckboxRoot(
const ownerState: CheckboxRootOwnerState = React.useMemo(
() => ({
...fieldOwnerState,
- checked,
+ checked: computedChecked,
disabled,
readOnly,
required,
- indeterminate,
+ indeterminate: computedIndeterminate,
}),
- [checked, disabled, readOnly, required, indeterminate, fieldOwnerState],
+ [fieldOwnerState, computedChecked, disabled, readOnly, required, computedIndeterminate],
);
const styleHooks = useCheckboxStyleHooks(ownerState);
@@ -65,6 +94,7 @@ const CheckboxRoot = React.forwardRef(function CheckboxRoot(
ref: mergedRef,
...styleHooks,
...otherProps,
+ ...otherGroupProps,
});
return (
@@ -142,9 +172,14 @@ CheckboxRoot.propTypes /* remove-proptypes */ = {
* Callback fired when the checked state is changed.
*
* @param {boolean} checked The new checked state.
- * @param {React.ChangeEvent} event The event source of the callback.
+ * @param {Event} event The event source of the callback.
*/
onCheckedChange: PropTypes.func,
+ /**
+ * If `true`, the checkbox is a parent checkbox for a group of child checkboxes.
+ * @default false
+ */
+ parent: PropTypes.bool,
/**
* If `true`, the component is read only.
*
diff --git a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts
index 8a380d3cf0..74a34f1810 100644
--- a/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts
+++ b/packages/mui-base/src/Checkbox/Root/CheckboxRoot.types.ts
@@ -49,9 +49,9 @@ export interface UseCheckboxRootParameters {
* Callback fired when the checked state is changed.
*
* @param {boolean} checked The new checked state.
- * @param {React.ChangeEvent} event The event source of the callback.
+ * @param {Event} event The event source of the callback.
*/
- onCheckedChange?: (checked: boolean, event: React.ChangeEvent) => void;
+ onCheckedChange?: (checked: boolean, event: Event) => void;
/**
* If `true`, the component is read only.
*
@@ -80,6 +80,11 @@ export interface UseCheckboxRootParameters {
* The ref to the input element.
*/
inputRef?: React.Ref;
+ /**
+ * If `true`, the checkbox is a parent checkbox for a group of child checkboxes.
+ * @default false
+ */
+ parent?: boolean;
}
export interface UseCheckboxRootReturnValue {
diff --git a/packages/mui-base/src/Checkbox/Root/CheckboxRootContext.ts b/packages/mui-base/src/Checkbox/Root/CheckboxRootContext.ts
new file mode 100644
index 0000000000..fa36221dcd
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/Root/CheckboxRootContext.ts
@@ -0,0 +1,4 @@
+import * as React from 'react';
+import type { CheckboxContextValue } from './CheckboxRoot.types';
+
+export const CheckboxRootContext = React.createContext(null);
diff --git a/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts b/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts
index 0454f14da0..fc1266320f 100644
--- a/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts
+++ b/packages/mui-base/src/Checkbox/Root/useCheckboxRoot.ts
@@ -4,8 +4,8 @@ import { useControlled } from '../../utils/useControlled';
import { visuallyHidden } from '../../utils/visuallyHidden';
import { useForkRef } from '../../utils/useForkRef';
import { mergeReactProps } from '../../utils/mergeReactProps';
-import { useEventCallback } from '../../utils/useEventCallback';
import { useId } from '../../utils/useId';
+import { useEventCallback } from '../../utils/useEventCallback';
import { useEnhancedEffect } from '../../utils/useEnhancedEffect';
import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
import { useFieldControlValidation } from '../../Field/Control/useFieldControlValidation';
@@ -36,6 +36,13 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR
disabled = false,
} = params;
+ const [checked, setCheckedState] = useControlled({
+ controlled: externalChecked,
+ default: defaultChecked,
+ name: 'Checkbox',
+ state: 'checked',
+ });
+
const {
labelId,
setDisabled,
@@ -76,13 +83,6 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR
}
}, [indeterminate]);
- const [checked, setCheckedState] = useControlled({
- controlled: externalChecked,
- default: defaultChecked,
- name: 'Checkbox',
- state: 'checked',
- });
-
useEnhancedEffect(() => {
if (validityData.initialValue === null && checked !== validityData.initialValue) {
setValidityData((prev) => ({ ...prev, initialValue: checked }));
@@ -143,6 +143,8 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR
tabIndex: -1,
type: 'checkbox',
'aria-hidden': true,
+ // @ts-ignore
+ inert: 'true',
onChange(event) {
// Workaround for https://github.com/facebook/react/issues/9023
if (event.nativeEvent.defaultPrevented) {
@@ -153,7 +155,7 @@ export function useCheckboxRoot(params: UseCheckboxRootParameters): UseCheckboxR
setDirty(nextChecked !== validityData.initialValue);
setCheckedState(nextChecked);
- onCheckedChange?.(nextChecked, event);
+ onCheckedChange?.(nextChecked, event.nativeEvent);
},
}),
[
diff --git a/packages/mui-base/src/CheckboxGroup/Parent/useCheckboxGroupParent.test.tsx b/packages/mui-base/src/CheckboxGroup/Parent/useCheckboxGroupParent.test.tsx
new file mode 100644
index 0000000000..a3ae0e9fc7
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/Parent/useCheckboxGroupParent.test.tsx
@@ -0,0 +1,209 @@
+import * as React from 'react';
+import { createRenderer, fireEvent, screen } from '@mui/internal-test-utils';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Checkbox from '@base_ui/react/Checkbox';
+import { expect } from 'chai';
+
+describe('useCheckboxGroupParent', () => {
+ const { render } = createRenderer();
+ const allValues = ['a', 'b', 'c'];
+
+ it('should control child checkboxes', () => {
+ function App() {
+ const [value, setValue] = React.useState([]);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ const checkboxes = screen
+ .getAllByRole('checkbox')
+ .filter((v) => v.getAttribute('name') && v.tagName === 'BUTTON');
+ const parent = screen.getByTestId('parent');
+
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ });
+
+ fireEvent.click(parent);
+ expect(parent).to.have.attribute('aria-checked', 'true');
+
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).to.have.attribute('aria-checked', 'true');
+ });
+
+ fireEvent.click(parent);
+ expect(parent).to.have.attribute('aria-checked', 'false');
+
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ });
+ });
+
+ it('parent should be marked as mixed if some children are checked', () => {
+ function App() {
+ const [value, setValue] = React.useState([]);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ const checkboxes = screen
+ .getAllByRole('checkbox')
+ .filter((v) => v.getAttribute('name') && v.tagName === 'BUTTON');
+
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ });
+
+ fireEvent.click(checkboxes[0]);
+
+ expect(screen.getByTestId('parent')).to.have.attribute('aria-checked', 'mixed');
+ });
+
+ it('should correctly initialize the values array', () => {
+ function App() {
+ const [value, setValue] = React.useState(['a']);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ expect(screen.getByTestId('parent')).to.have.attribute('aria-checked', 'mixed');
+
+ const checkboxes = screen
+ .getAllByRole('checkbox')
+ .filter((v) => v.getAttribute('name') && v.tagName === 'BUTTON');
+ const checkboxA = checkboxes.find((v) => v.getAttribute('name') === 'a');
+
+ expect(checkboxA).to.have.attribute('aria-checked', 'true');
+ });
+
+ it('should update the values array when a child checkbox is clicked', () => {
+ function App() {
+ const [value, setValue] = React.useState(['a']);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ expect(screen.getByTestId('parent')).to.have.attribute('aria-checked', 'mixed');
+
+ const checkboxes = screen
+ .getAllByRole('checkbox')
+ .filter((v) => v.getAttribute('name') && v.tagName === 'BUTTON');
+ const checkboxA = checkboxes.find((v) => v.getAttribute('name') === 'a');
+
+ expect(checkboxA).to.have.attribute('aria-checked', 'true');
+
+ checkboxes.forEach((checkbox) => {
+ if (checkbox !== checkboxA) {
+ fireEvent.click(checkbox);
+ }
+ });
+
+ expect(screen.getByTestId('parent')).to.have.attribute('aria-checked', 'true');
+ });
+
+ it('should apply space-separated aria-controls attribute with child names', () => {
+ function App() {
+ const [value, setValue] = React.useState([]);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ const parent = screen.getByTestId('parent');
+ const id = parent.getAttribute('id');
+
+ expect(parent).to.have.attribute('aria-controls', allValues.map((v) => `${id}-${v}`).join(' '));
+ });
+
+ it('preserves initial state if mixed when parent is clicked with preserveChildStates=true', () => {
+ function App() {
+ const [value, setValue] = React.useState([]);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ const checkboxes = screen
+ .getAllByRole('checkbox')
+ .filter((v) => v.getAttribute('name') && v.tagName === 'BUTTON');
+ const checkboxA = checkboxes.find((v) => v.getAttribute('name') === 'a')!;
+ const parent = screen.getByTestId('parent');
+
+ fireEvent.click(checkboxA);
+
+ expect(screen.getByTestId('parent')).to.have.attribute('aria-checked', 'mixed');
+
+ fireEvent.click(parent);
+
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).to.have.attribute('aria-checked', 'true');
+ });
+
+ fireEvent.click(parent);
+
+ checkboxes.forEach((checkbox) => {
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ });
+
+ fireEvent.click(parent);
+
+ expect(parent).to.have.attribute('aria-checked', 'mixed');
+ expect(checkboxA).to.have.attribute('aria-checked', 'true');
+ checkboxes.forEach((checkbox) => {
+ if (checkbox !== checkboxA) {
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ }
+ });
+ });
+});
diff --git a/packages/mui-base/src/CheckboxGroup/Parent/useCheckboxGroupParent.ts b/packages/mui-base/src/CheckboxGroup/Parent/useCheckboxGroupParent.ts
new file mode 100644
index 0000000000..ba82b81f7e
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/Parent/useCheckboxGroupParent.ts
@@ -0,0 +1,138 @@
+import * as React from 'react';
+import { useId } from '../../utils/useId';
+import { useEventCallback } from '../../utils/useEventCallback';
+
+/**
+ *
+ * API:
+ *
+ * - [useCheckboxGroupParent API](https://mui.com/base-ui/api/use-checkbox-group-parent/)
+ */
+export function useCheckboxGroupParent(
+ params: UseCheckboxGroupParent.Parameters,
+): UseCheckboxGroupParent.ReturnValue {
+ const {
+ allValues = [],
+ value = [],
+ onValueChange: onValueChangeProp = () => {},
+ preserveChildStates = false,
+ } = params;
+
+ const uncontrolledStateRef = React.useRef(value);
+ const [status, setStatus] = React.useState<'on' | 'off' | 'mixed'>('mixed');
+
+ const id = useId();
+ const checked = value.length === allValues.length;
+ const indeterminate = value.length !== allValues.length && value.length > 0;
+
+ const onValueChange = useEventCallback(onValueChangeProp);
+
+ const getParentProps: UseCheckboxGroupParent.ReturnValue['getParentProps'] = React.useCallback(
+ () => ({
+ id,
+ indeterminate,
+ checked,
+ 'aria-controls': allValues.map((v) => `${id}-${v}`).join(' '),
+ onCheckedChange(_, event) {
+ const uncontrolledState = uncontrolledStateRef.current;
+ const allOnOrOff =
+ uncontrolledState.length === allValues.length || uncontrolledState.length === 0;
+
+ if (allOnOrOff) {
+ if (value.length === allValues.length) {
+ onValueChange([], event);
+ } else {
+ onValueChange(allValues, event);
+ }
+ return;
+ }
+
+ if (preserveChildStates) {
+ if (status === 'mixed') {
+ onValueChange(allValues, event);
+ setStatus('on');
+ } else if (status === 'on') {
+ onValueChange([], event);
+ setStatus('off');
+ } else if (status === 'off') {
+ onValueChange(uncontrolledState, event);
+ setStatus('mixed');
+ }
+ } else if (checked) {
+ onValueChange([], event);
+ setStatus('off');
+ } else {
+ onValueChange(allValues, event);
+ setStatus('on');
+ }
+ },
+ }),
+ [
+ allValues,
+ checked,
+ id,
+ indeterminate,
+ onValueChange,
+ preserveChildStates,
+ status,
+ value.length,
+ ],
+ );
+
+ const getChildProps: UseCheckboxGroupParent.ReturnValue['getChildProps'] = React.useCallback(
+ (name: string) => ({
+ name,
+ id: `${id}-${name}`,
+ checked: value.includes(name),
+ onCheckedChange(nextChecked, event) {
+ const newValue = [...value];
+ if (nextChecked) {
+ newValue.push(name);
+ } else {
+ newValue.splice(newValue.indexOf(name), 1);
+ }
+ uncontrolledStateRef.current = newValue;
+ onValueChange(newValue, event);
+ setStatus('mixed');
+ },
+ }),
+ [id, onValueChange, value],
+ );
+
+ return React.useMemo(
+ () => ({
+ id,
+ indeterminate,
+ getParentProps,
+ getChildProps,
+ }),
+ [id, indeterminate, getParentProps, getChildProps],
+ );
+}
+
+export namespace UseCheckboxGroupParent {
+ export interface Parameters {
+ allValues?: string[];
+ value?: string[];
+ onValueChange?: (value: string[], event: Event) => void;
+ preserveChildStates?: boolean;
+ }
+
+ export interface ReturnValue {
+ id: string | undefined;
+ indeterminate: boolean;
+ getParentProps: () => {
+ id: string | undefined;
+ indeterminate: boolean;
+ checked: boolean;
+ 'aria-controls': string;
+ onCheckedChange: (checked: boolean, event: Event) => void;
+ };
+ getChildProps: (name: string) => {
+ name: string;
+ id: string;
+ checked: boolean;
+ onCheckedChange: (checked: boolean, event: Event) => void;
+ };
+ }
+}
diff --git a/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRoot.test.tsx b/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRoot.test.tsx
new file mode 100644
index 0000000000..402769c6aa
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRoot.test.tsx
@@ -0,0 +1,134 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createRenderer, screen, fireEvent } from '@mui/internal-test-utils';
+import * as CheckboxGroup from '@base_ui/react/CheckboxGroup';
+import * as Checkbox from '@base_ui/react/Checkbox';
+import { describeConformance } from '../../../test/describeConformance';
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ inheritComponent: 'div',
+ refInstanceof: window.HTMLDivElement,
+ render,
+ }));
+
+ describe('prop: preserveChildStates', () => {
+ it('should not preserve child states when false', () => {
+ function App() {
+ const [value, setValue] = React.useState([]);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ const parent = screen.getByTestId('parent');
+ const red = screen.getByTestId('red');
+ const green = screen.getByTestId('green');
+ const blue = screen.getByTestId('blue');
+
+ expect(red).to.have.attribute('aria-checked', 'false');
+ expect(green).to.have.attribute('aria-checked', 'false');
+ expect(blue).to.have.attribute('aria-checked', 'false');
+
+ fireEvent.click(red);
+
+ expect(red).to.have.attribute('aria-checked', 'true');
+ expect(green).to.have.attribute('aria-checked', 'false');
+ expect(blue).to.have.attribute('aria-checked', 'false');
+ expect(parent).to.have.attribute('aria-checked', 'mixed');
+
+ fireEvent.click(parent);
+
+ expect(red).to.have.attribute('aria-checked', 'true');
+ expect(green).to.have.attribute('aria-checked', 'true');
+ expect(blue).to.have.attribute('aria-checked', 'true');
+ expect(parent).to.have.attribute('aria-checked', 'true');
+
+ fireEvent.click(parent);
+
+ expect(red).to.have.attribute('aria-checked', 'false');
+ expect(green).to.have.attribute('aria-checked', 'false');
+ expect(blue).to.have.attribute('aria-checked', 'false');
+ expect(parent).to.have.attribute('aria-checked', 'false');
+
+ fireEvent.click(parent);
+
+ expect(red).to.have.attribute('aria-checked', 'true');
+ expect(green).to.have.attribute('aria-checked', 'true');
+ expect(blue).to.have.attribute('aria-checked', 'true');
+ expect(parent).to.have.attribute('aria-checked', 'true');
+ });
+
+ it('should preserve child states when true', () => {
+ function App() {
+ const [value, setValue] = React.useState([]);
+ return (
+
+
+
+
+
+
+ );
+ }
+
+ render( );
+
+ const parent = screen.getByTestId('parent');
+ const red = screen.getByTestId('red');
+ const green = screen.getByTestId('green');
+ const blue = screen.getByTestId('blue');
+
+ expect(red).to.have.attribute('aria-checked', 'false');
+ expect(green).to.have.attribute('aria-checked', 'false');
+ expect(blue).to.have.attribute('aria-checked', 'false');
+
+ fireEvent.click(red);
+
+ expect(red).to.have.attribute('aria-checked', 'true');
+ expect(green).to.have.attribute('aria-checked', 'false');
+ expect(blue).to.have.attribute('aria-checked', 'false');
+ expect(parent).to.have.attribute('aria-checked', 'mixed');
+
+ fireEvent.click(parent);
+
+ expect(red).to.have.attribute('aria-checked', 'true');
+ expect(green).to.have.attribute('aria-checked', 'true');
+ expect(blue).to.have.attribute('aria-checked', 'true');
+ expect(parent).to.have.attribute('aria-checked', 'true');
+
+ fireEvent.click(parent);
+
+ expect(red).to.have.attribute('aria-checked', 'false');
+ expect(green).to.have.attribute('aria-checked', 'false');
+ expect(blue).to.have.attribute('aria-checked', 'false');
+ expect(parent).to.have.attribute('aria-checked', 'false');
+
+ fireEvent.click(parent);
+
+ expect(red).to.have.attribute('aria-checked', 'true');
+ expect(green).to.have.attribute('aria-checked', 'false');
+ expect(blue).to.have.attribute('aria-checked', 'false');
+ expect(parent).to.have.attribute('aria-checked', 'mixed');
+ });
+ });
+});
diff --git a/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRoot.tsx b/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRoot.tsx
new file mode 100644
index 0000000000..b2c6b6f7c6
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRoot.tsx
@@ -0,0 +1,167 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useCheckboxGroupRoot } from './useCheckboxGroupRoot';
+import { CheckboxGroupRootContext } from './CheckboxGroupRootContext';
+import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
+import type { BaseUIComponentProps } from '../../utils/types';
+import type { FieldRootOwnerState } from '../../Field/Root/FieldRoot.types';
+
+/**
+ * The foundation for building custom-styled checkbox groups.
+ *
+ * Demos:
+ *
+ * - [CheckboxGroup](https://mui.com/base-ui/react-checkbox-group/)
+ *
+ * API:
+ *
+ * - [CheckboxGroup API](https://mui.com/base-ui/react-checkbox/components-api/#checkbox-group-root)
+ */
+const CheckboxGroupRoot = React.forwardRef(function CheckboxGroupRoot(
+ props: CheckboxGroupRoot.Props,
+ forwardedRef: React.ForwardedRef,
+) {
+ const {
+ render,
+ className,
+ value: externalValue,
+ defaultValue,
+ onValueChange,
+ allValues,
+ disabled: disabledProp = false,
+ preserveChildStates = false,
+ ...otherProps
+ } = props;
+
+ const { disabled: fieldDisabled, ownerState: fieldOwnerState } = useFieldRootContext();
+
+ const disabled = fieldDisabled || disabledProp;
+
+ const { getRootProps, value, setValue, parent } = useCheckboxGroupRoot({
+ value: externalValue,
+ allValues,
+ defaultValue,
+ onValueChange,
+ preserveChildStates,
+ });
+
+ const ownerState = React.useMemo(
+ () => ({
+ ...fieldOwnerState,
+ disabled,
+ }),
+ [fieldOwnerState, disabled],
+ );
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'div',
+ className,
+ ownerState,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ });
+
+ const contextValue: CheckboxGroupRootContext = React.useMemo(
+ () => ({
+ allValues,
+ value,
+ setValue,
+ parent,
+ }),
+ [allValues, value, setValue, parent],
+ );
+
+ return (
+
+ {renderElement()}
+
+ );
+});
+
+namespace CheckboxGroupRoot {
+ export interface OwnerState extends FieldRootOwnerState {
+ disabled: boolean;
+ }
+ export interface Props extends BaseUIComponentProps<'div', OwnerState> {
+ /**
+ * The currently checked values of the checkbox group. Use when controlled.
+ */
+ value?: string[];
+ /**
+ * The default checked values of the checkbox group. Use when uncontrolled.
+ */
+ defaultValue?: string[];
+ /**
+ * A callback function that is called when the value of the checkbox group changes.
+ * Use when controlled.
+ */
+ onValueChange?: (value: string[], event: Event) => void;
+ /**
+ * All values of the checkboxes in the group.
+ */
+ allValues?: string[];
+ /**
+ * Whether the checkbox group is disabled.
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * Whether the parent checkbox should preserve its child states when checked/unchecked, leading
+ * to a tri-state checkbox group.
+ * @default false
+ */
+ preserveChildStates?: boolean;
+ }
+}
+
+CheckboxGroupRoot.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * All values of the checkboxes in the group.
+ */
+ allValues: PropTypes.arrayOf(PropTypes.string),
+ /**
+ * @ignore
+ */
+ children: PropTypes.node,
+ /**
+ * Class names applied to the element or a function that returns them based on the component's state.
+ */
+ className: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),
+ /**
+ * The default checked values of the checkbox group. Use when uncontrolled.
+ */
+ defaultValue: PropTypes.arrayOf(PropTypes.string),
+ /**
+ * Whether the checkbox group is disabled.
+ * @default false
+ */
+ disabled: PropTypes.bool,
+ /**
+ * A callback function that is called when the value of the checkbox group changes.
+ * Use when controlled.
+ */
+ onValueChange: PropTypes.func,
+ /**
+ * Whether the parent checkbox should preserve its child states when checked/unchecked, leading
+ * to a tri-state checkbox group.
+ * @default false
+ */
+ preserveChildStates: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+ /**
+ * The currently checked values of the checkbox group. Use when controlled.
+ */
+ value: PropTypes.arrayOf(PropTypes.string),
+} as any;
+
+export { CheckboxGroupRoot };
diff --git a/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRootContext.ts b/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRootContext.ts
new file mode 100644
index 0000000000..412faa8769
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/Root/CheckboxGroupRootContext.ts
@@ -0,0 +1,19 @@
+import * as React from 'react';
+import { UseCheckboxGroupParent } from '../Parent/useCheckboxGroupParent';
+
+export interface CheckboxGroupRootContext {
+ value: string[];
+ setValue: (value: string[], event: Event) => void;
+ allValues: string[] | undefined;
+ parent: UseCheckboxGroupParent.ReturnValue;
+}
+
+export const CheckboxGroupRootContext = React.createContext(null);
+
+export function useCheckboxGroupRootContext(optional = true) {
+ const context = React.useContext(CheckboxGroupRootContext);
+ if (context === null && !optional) {
+ throw new Error('Base UI: CheckboxGroupRootContext is not defined.');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/CheckboxGroup/Root/useCheckboxGroupRoot.tsx b/packages/mui-base/src/CheckboxGroup/Root/useCheckboxGroupRoot.tsx
new file mode 100644
index 0000000000..b5ebb05335
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/Root/useCheckboxGroupRoot.tsx
@@ -0,0 +1,84 @@
+'use client';
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import { useControlled } from '../../utils/useControlled';
+import { useEventCallback } from '../../utils/useEventCallback';
+import { useCheckboxGroupParent } from '../Parent/useCheckboxGroupParent';
+import { useFieldRootContext } from '../../Field/Root/FieldRootContext';
+import type { UseCheckboxGroupParent } from '../Parent/useCheckboxGroupParent';
+import type { GenericHTMLProps } from '../../utils/types';
+
+/**
+ *
+ * API:
+ *
+ * - [useCheckboxGroupRoot API](https://mui.com/base-ui/api/use-checkbox-group-root/)
+ */
+export function useCheckboxGroupRoot(
+ params: UseCheckboxGroupRoot.Parameters,
+): UseCheckboxGroupRoot.ReturnValue {
+ const {
+ allValues,
+ value: externalValue,
+ defaultValue,
+ onValueChange,
+ preserveChildStates,
+ } = params;
+
+ const { labelId } = useFieldRootContext();
+
+ const [value, setValueUnwrapped] = useControlled({
+ controlled: externalValue,
+ default: defaultValue,
+ name: 'CheckboxGroup',
+ state: 'value',
+ });
+
+ const setValue = useEventCallback((v: string[], event: Event) => {
+ setValueUnwrapped(v);
+ onValueChange?.(v, event);
+ });
+
+ const parent = useCheckboxGroupParent({
+ allValues,
+ value: externalValue,
+ onValueChange,
+ preserveChildStates,
+ });
+
+ const getRootProps = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps<'div'>(externalProps, {
+ role: 'group',
+ 'aria-labelledby': labelId,
+ }),
+ [labelId],
+ );
+
+ return React.useMemo(
+ () => ({
+ getRootProps,
+ value,
+ setValue,
+ parent,
+ }),
+ [getRootProps, value, setValue, parent],
+ );
+}
+
+namespace UseCheckboxGroupRoot {
+ export interface Parameters {
+ value?: string[];
+ defaultValue?: string[];
+ onValueChange?: (value: string[], event: Event) => void;
+ allValues?: string[];
+ preserveChildStates?: boolean;
+ }
+
+ export interface ReturnValue {
+ getRootProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
+ value: string[];
+ setValue: (value: string[], event: Event) => void;
+ parent: UseCheckboxGroupParent.ReturnValue;
+ }
+}
diff --git a/packages/mui-base/src/CheckboxGroup/index.barrel.ts b/packages/mui-base/src/CheckboxGroup/index.barrel.ts
new file mode 100644
index 0000000000..2f4786b2a0
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/index.barrel.ts
@@ -0,0 +1 @@
+export * from './Root/CheckboxGroupRoot';
diff --git a/packages/mui-base/src/CheckboxGroup/index.ts b/packages/mui-base/src/CheckboxGroup/index.ts
new file mode 100644
index 0000000000..dd08fe5b48
--- /dev/null
+++ b/packages/mui-base/src/CheckboxGroup/index.ts
@@ -0,0 +1 @@
+export { CheckboxGroupRoot as Root } from './Root/CheckboxGroupRoot';
diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts
index 7f29893f01..9a3bda5f70 100644
--- a/packages/mui-base/src/index.ts
+++ b/packages/mui-base/src/index.ts
@@ -1,5 +1,6 @@
export * from './AlertDialog/index.barrel';
export * from './Checkbox/index.barrel';
+export * from './CheckboxGroup/index.barrel';
export * from './Dialog/index.barrel';
export * from './NumberField/index.barrel';
export * from './Popover/index.barrel';