diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.js b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.js
new file mode 100644
index 0000000000..423a49a9ce
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.js
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import { Checkbox as BaseCheckbox } from '@mui/base/Checkbox';
+import HorizontalRule from '@mui/icons-material/HorizontalRule';
+
+export default function UnstyledCheckboxIndeterminate() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+};
+
+const Checkbox = styled(BaseCheckbox)(
+ ({ 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 Indicator = styled(BaseCheckbox.Indicator)`
+ color: ${grey[100]};
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.tsx b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.tsx
new file mode 100644
index 0000000000..423a49a9ce
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.tsx
@@ -0,0 +1,78 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import { Checkbox as BaseCheckbox } from '@mui/base/Checkbox';
+import HorizontalRule from '@mui/icons-material/HorizontalRule';
+
+export default function UnstyledCheckboxIndeterminate() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+};
+
+const Checkbox = styled(BaseCheckbox)(
+ ({ 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 Indicator = styled(BaseCheckbox.Indicator)`
+ color: ${grey[100]};
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.tsx.preview b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.tsx.preview
new file mode 100644
index 0000000000..c2de858786
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminate.tsx.preview
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.js b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.js
new file mode 100644
index 0000000000..12091ea170
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.js
@@ -0,0 +1,157 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import { Checkbox as BaseCheckbox } from '@mui/base/Checkbox';
+import HorizontalRule from '@mui/icons-material/HorizontalRule';
+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 isChecked = checkedValues.every((value) => value);
+ const isIndeterminate = checkedValues.some((value) => value) && !isChecked;
+
+ const id = React.useId();
+
+ return (
+
+
+ `${id}-${color}`).join(' ')}
+ indeterminate={isIndeterminate}
+ checked={isChecked}
+ onChange={(event) => {
+ const checked = event.target.checked;
+ setCheckedValues([checked, checked, checked]);
+ }}
+ >
+
+ {isIndeterminate ? : }
+
+
+ e.preventDefault()}>
+ Colors
+
+
+
+ {colors.map((color, index) => (
+
+ {
+ const newCheckedValues = [...checkedValues];
+ newCheckedValues[index] = event.target.checked;
+ setCheckedValues(newCheckedValues);
+ }}
+ >
+
+
+
+
+ e.preventDefault()}
+ >
+ {color}
+
+
+ ))}
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+ 400: '#B0B8C4',
+ 800: '#303740',
+};
+
+const Checkbox = styled(BaseCheckbox)(
+ ({ 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 ListRoot = styled('div')`
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+`;
+
+const List = styled('ul')`
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ margin-left: 32px;
+`;
+
+const ListItem = styled('li')`
+ display: flex;
+ align-items: center;
+
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
+`;
+
+const Label = styled('label')(
+ ({ theme }) => `
+ padding-left: 8px;
+ color: ${theme.palette.mode === 'dark' ? grey[400] : grey[800]};
+ `,
+);
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.tsx b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.tsx
new file mode 100644
index 0000000000..12091ea170
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIndeterminateGroup.tsx
@@ -0,0 +1,157 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import { Checkbox as BaseCheckbox } from '@mui/base/Checkbox';
+import HorizontalRule from '@mui/icons-material/HorizontalRule';
+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 isChecked = checkedValues.every((value) => value);
+ const isIndeterminate = checkedValues.some((value) => value) && !isChecked;
+
+ const id = React.useId();
+
+ return (
+
+
+ `${id}-${color}`).join(' ')}
+ indeterminate={isIndeterminate}
+ checked={isChecked}
+ onChange={(event) => {
+ const checked = event.target.checked;
+ setCheckedValues([checked, checked, checked]);
+ }}
+ >
+
+ {isIndeterminate ? : }
+
+
+ e.preventDefault()}>
+ Colors
+
+
+
+ {colors.map((color, index) => (
+
+ {
+ const newCheckedValues = [...checkedValues];
+ newCheckedValues[index] = event.target.checked;
+ setCheckedValues(newCheckedValues);
+ }}
+ >
+
+
+
+
+ e.preventDefault()}
+ >
+ {color}
+
+
+ ))}
+
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const grey = {
+ 100: '#E5EAF2',
+ 400: '#B0B8C4',
+ 800: '#303740',
+};
+
+const Checkbox = styled(BaseCheckbox)(
+ ({ 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 ListRoot = styled('div')`
+ display: flex;
+ align-items: center;
+ margin-bottom: 8px;
+`;
+
+const List = styled('ul')`
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ margin-left: 32px;
+`;
+
+const ListItem = styled('li')`
+ display: flex;
+ align-items: center;
+
+ &:not(:last-child) {
+ margin-bottom: 8px;
+ }
+`;
+
+const Label = styled('label')(
+ ({ theme }) => `
+ padding-left: 8px;
+ color: ${theme.palette.mode === 'dark' ? grey[400] : grey[800]};
+ `,
+);
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/css/index.js b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/css/index.js
new file mode 100644
index 0000000000..188c6cecb4
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/css/index.js
@@ -0,0 +1,134 @@
+import * as React from 'react';
+import { Checkbox } from '@mui/base/Checkbox';
+import { useTheme } from '@mui/system';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledCheckboxIntroduction() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const grey = {
+ 100: '#E5EAF2',
+ 300: '#C7D0DD',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+function Styles() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/css/index.tsx b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/css/index.tsx
new file mode 100644
index 0000000000..188c6cecb4
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/css/index.tsx
@@ -0,0 +1,134 @@
+import * as React from 'react';
+import { Checkbox } from '@mui/base/Checkbox';
+import { useTheme } from '@mui/system';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledCheckboxIntroduction() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const grey = {
+ 100: '#E5EAF2',
+ 300: '#C7D0DD',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+function Styles() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+ );
+}
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/system/index.js b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/system/index.js
new file mode 100644
index 0000000000..e6ec77f797
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/system/index.js
@@ -0,0 +1,91 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import { Checkbox as BaseCheckbox } from '@mui/base/Checkbox';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledSwitchIntroduction() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const grey = {
+ 100: '#E5EAF2',
+};
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const Checkbox = styled(BaseCheckbox)(
+ ({ 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)`
+ width: 100%;
+ height: 100%;
+`;
+
+const Indicator = styled(BaseCheckbox.Indicator)`
+ color: ${grey[100]};
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/system/index.tsx b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/system/index.tsx
new file mode 100644
index 0000000000..e6ec77f797
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/system/index.tsx
@@ -0,0 +1,91 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import { Checkbox as BaseCheckbox } from '@mui/base/Checkbox';
+import Check from '@mui/icons-material/Check';
+
+export default function UnstyledSwitchIntroduction() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+const grey = {
+ 100: '#E5EAF2',
+};
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+const Checkbox = styled(BaseCheckbox)(
+ ({ 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)`
+ width: 100%;
+ height: 100%;
+`;
+
+const Indicator = styled(BaseCheckbox.Indicator)`
+ color: ${grey[100]};
+ height: 100%;
+ display: inline-block;
+ visibility: hidden;
+
+ &[data-state='checked'],
+ &[data-state='mixed'] {
+ visibility: visible;
+ }
+`;
diff --git a/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/tailwind/index.js b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/tailwind/index.js
new file mode 100644
index 0000000000..3e180a6936
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/tailwind/index.js
@@ -0,0 +1,96 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { Checkbox as BaseCheckbox } from '@mui/base/Checkbox';
+import { useTheme } from '@mui/system';
+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';
+}
+
+export default function UnstyledCheckboxIntroduction() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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/UnstyledCheckboxIntroduction/tailwind/index.tsx b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/tailwind/index.tsx
new file mode 100644
index 0000000000..922dee5864
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/tailwind/index.tsx
@@ -0,0 +1,89 @@
+import * as React from 'react';
+import {
+ Checkbox as BaseCheckbox,
+ type CheckboxIndicatorProps,
+ type CheckboxProps,
+} from '@mui/base/Checkbox';
+import { useTheme } from '@mui/system';
+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';
+}
+
+export default function UnstyledCheckboxIntroduction() {
+ // Replace this with your app logic for determining dark mode
+ const isDarkMode = useIsDarkMode();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+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/UnstyledCheckboxIntroduction/tailwind/index.tsx.preview b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/tailwind/index.tsx.preview
new file mode 100644
index 0000000000..8a53b1cf9f
--- /dev/null
+++ b/docs/data/base/components/checkbox/UnstyledCheckboxIntroduction/tailwind/index.tsx.preview
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/base/components/checkbox/checkbox.md b/docs/data/base/components/checkbox/checkbox.md
index 56ce9d252a..7c0870bd31 100644
--- a/docs/data/base/components/checkbox/checkbox.md
+++ b/docs/data/base/components/checkbox/checkbox.md
@@ -1,14 +1,122 @@
---
productId: base-ui
-title: React Checkbox component
+title: React Checkbox component and hook
+components: Checkbox, CheckboxIndicator
+hooks: useCheckbox
githubLabel: 'component: checkbox'
waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/checkbox/
---
-# Checkbox π§
+# Checkbox
Checkboxes give users binary choices when presented with multiple options in a series.
-:::warning
-The BaseΒ UI Checkbox component isn't available yet, but you can upvote [this GitHub issue](https://github.com/mui/base-ui/issues/24) to see it arrive sooner.
+{{"component": "modules/components/ComponentLinkHeader.js", "design": false}}
+
+{{"component": "modules/components/ComponentPageTabs.js"}}
+
+## Introduction
+
+The Checkbox component provides users with a checkbox for toggling a checked state.
+
+{{"demo": "UnstyledCheckboxIntroduction", "defaultCodeOpen": false, "bg": "gradient"}}
+
+## Component
+
+```jsx
+import { Checkbox } from '@mui/base/Checkbox';
+```
+
+### Anatomy
+
+The `Checkbox` component is composed of a root component and an indicator child component:
+
+```tsx
+
+
+
+```
+
+The indicator can contain children, such as an icon:
+
+```tsx
+
+
+
+
+
+```
+
+The indicator conditionally unmounts its children when the checkbox is unchecked. For CSS animations, you can use the `keepMounted` prop to transition `visibility` and `opacity` for example:
+
+```tsx
+
+
+
+
+
+```
+
+### Custom structure
+
+Use the `render` prop to override the rendered checkbox or indicator element with your own components:
+
+```jsx
+ }>
+ } />
+
+```
+
+To ensure behavior works as expected:
+
+- **Forward all props**: Your component should spread all props to the underlying element.
+- **Forward the `ref`**: Your component should use [`forwardRef`](https://react.dev/reference/react/forwardRef) to ensure the Checkbox components can access the element via a ref.
+
+A custom component that adheres to these two principles looks like this:
+
+```jsx
+const MyCheckbox = React.forwardRef(function MyCheckbox(props, ref) {
+ return ;
+});
+```
+
+### Indeterminate state
+
+To make the checkbox indeterminate, add the `indeterminate` prop to override the appearance of the checkbox. The checkbox remains in an indeterminate state regardless of user interaction until set back to `false`.
+
+{{"demo": "UnstyledCheckboxIndeterminate.js"}}
+
+An indeterminate checkbox's main use case is representing the state of a parent checkbox where only some of its children are checked:
+
+{{"demo": "UnstyledCheckboxIndeterminateGroup.js", "defaultCodeOpen": false}}
+
+It's a **visual-only** state, so it can still have its internal `checked` state change.
+
+## Hook
+
+```js
+import { useCheckbox } from '@mui/base/useCheckbox';
+```
+
+The `useCheckbox` hook lets you apply the functionality of a Checkbox to a fully custom component.
+It returns props to be placed on the custom component, along with fields representing the component's internal state.
+
+:::info
+Hooks give you the most room for customization, but require more work to implement.
+With hooks, you can take full control over how your component is rendered, and define all the custom props and CSS classes you need.
+
+You may not need to use hooks unless you find that you're limited by the customization options of their component counterpartsβfor instance, if your component requires significantly different [HTML structure](#anatomy).
:::
+
+## Accessibility
+
+Ensure the checkbox has an accessible name via a `label` element.
+
+```jsx
+
+
+
+
+ My label
+
+```
diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts
index cc7e0c556b..a507d37b30 100644
--- a/docs/data/base/pages.ts
+++ b/docs/data/base/pages.ts
@@ -25,7 +25,7 @@ const pages: readonly MuiPage[] = [
children: [
{ pathname: '/base-ui/react-autocomplete', title: 'Autocomplete' },
{ pathname: '/base-ui/react-button', title: 'Button' },
- { pathname: '/base-ui/react-checkbox', title: 'Checkbox', planned: true },
+ { pathname: '/base-ui/react-checkbox', title: 'Checkbox' },
{ pathname: '/base-ui/react-input', title: 'Input' },
{ pathname: '/base-ui/react-number-input', title: 'Number Input', unstable: true },
{ pathname: '/base-ui/react-radio-group', title: 'Radio Group', planned: true },
diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js
index 456a2dd6ab..58829d7f65 100644
--- a/docs/data/base/pagesApi.js
+++ b/docs/data/base/pagesApi.js
@@ -1,6 +1,14 @@
module.exports = [
{ pathname: '/base-ui/react-badge/components-api/#badge', title: 'Badge' },
{ pathname: '/base-ui/react-button/components-api/#button', title: 'Button' },
+ {
+ pathname: '/base-ui/react-checkbox/components-api/#checkbox',
+ title: 'Checkbox',
+ },
+ {
+ pathname: '/base-ui/react-checkbox/components-api/#checkbox-indicator',
+ title: 'CheckboxIndicator',
+ },
{
pathname:
'/base-ui/react-click-away-listener/components-api/#click-away-listener',
@@ -73,6 +81,10 @@ module.exports = [
},
{ pathname: '/base-ui/react-badge/hooks-api/#use-badge', title: 'useBadge' },
{ pathname: '/base-ui/react-button/hooks-api/#use-button', title: 'useButton' },
+ {
+ pathname: '/base-ui/react-checkbox/hooks-api/#use-checkbox',
+ title: 'useCheckbox',
+ },
{ pathname: '/base-ui/react-menu/hooks-api/#use-dropdown', title: 'useDropdown' },
{
pathname: '/base-ui/react-form-control/hooks-api/#use-form-control-context',
diff --git a/docs/pages/base-ui/api/checkbox-indicator.json b/docs/pages/base-ui/api/checkbox-indicator.json
new file mode 100644
index 0000000000..1a095cad05
--- /dev/null
+++ b/docs/pages/base-ui/api/checkbox-indicator.json
@@ -0,0 +1,21 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "keepMounted": { "type": { "name": "bool" }, "default": "false" },
+ "render": { "type": { "name": "func" } }
+ },
+ "name": "CheckboxIndicator",
+ "imports": [
+ "import { CheckboxIndicator } from '@mui/base/Checkbox';",
+ "import { CheckboxIndicator } from '@mui/base';"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "CheckboxIndicator",
+ "forwardsRefTo": "HTMLSpanElement",
+ "filename": "/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/checkbox.json b/docs/pages/base-ui/api/checkbox.json
new file mode 100644
index 0000000000..5753f8b053
--- /dev/null
+++ b/docs/pages/base-ui/api/checkbox.json
@@ -0,0 +1,34 @@
+{
+ "props": {
+ "checked": { "type": { "name": "bool" }, "default": "undefined" },
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "defaultChecked": { "type": { "name": "bool" }, "default": "false" },
+ "disabled": { "type": { "name": "bool" }, "default": "false" },
+ "indeterminate": { "type": { "name": "bool" }, "default": "false" },
+ "name": { "type": { "name": "string" }, "default": "undefined" },
+ "onChange": {
+ "type": { "name": "func" },
+ "signature": {
+ "type": "function(event: React.ChangeEvent) => void",
+ "describedArgs": ["event"]
+ }
+ },
+ "readOnly": { "type": { "name": "bool" }, "default": "false" },
+ "render": { "type": { "name": "func" } },
+ "required": { "type": { "name": "bool" }, "default": "false" }
+ },
+ "name": "Checkbox",
+ "imports": [
+ "import { Checkbox } from '@mui/base/Checkbox';",
+ "import { Checkbox } from '@mui/base';"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "Checkbox",
+ "forwardsRefTo": "HTMLButtonElement",
+ "filename": "/packages/mui-base/src/Checkbox/Checkbox.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/use-checkbox.json b/docs/pages/base-ui/api/use-checkbox.json
new file mode 100644
index 0000000000..b9a0ee4c1e
--- /dev/null
+++ b/docs/pages/base-ui/api/use-checkbox.json
@@ -0,0 +1,54 @@
+{
+ "parameters": {
+ "autoFocus": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" },
+ "checked": { "type": { "name": "boolean", "description": "boolean" }, "default": "undefined" },
+ "defaultChecked": {
+ "type": { "name": "boolean", "description": "boolean" },
+ "default": "false"
+ },
+ "disabled": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" },
+ "indeterminate": {
+ "type": { "name": "boolean", "description": "boolean" },
+ "default": "false"
+ },
+ "inputRef": {
+ "type": {
+ "name": "React.Ref<HTMLInputElement>",
+ "description": "React.Ref<HTMLInputElement>"
+ }
+ },
+ "name": { "type": { "name": "string", "description": "string" }, "default": "undefined" },
+ "onChange": {
+ "type": {
+ "name": "React.ChangeEventHandler<HTMLInputElement>",
+ "description": "React.ChangeEventHandler<HTMLInputElement>"
+ }
+ },
+ "readOnly": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" },
+ "required": { "type": { "name": "boolean", "description": "boolean" }, "default": "false" }
+ },
+ "returnValue": {
+ "checked": { "type": { "name": "boolean", "description": "boolean" }, "required": true },
+ "getButtonProps": {
+ "type": {
+ "name": "(externalProps?: React.ComponentPropsWithRef<'button'>) => React.ComponentPropsWithRef<'button'>",
+ "description": "(externalProps?: React.ComponentPropsWithRef<'button'>) => React.ComponentPropsWithRef<'button'>"
+ },
+ "required": true
+ },
+ "getInputProps": {
+ "type": {
+ "name": "(externalProps?: React.ComponentPropsWithRef<'input'>) => React.ComponentPropsWithRef<'input'>",
+ "description": "(externalProps?: React.ComponentPropsWithRef<'input'>) => React.ComponentPropsWithRef<'input'>"
+ },
+ "required": true
+ }
+ },
+ "name": "useCheckbox",
+ "filename": "/packages/mui-base/src/useCheckbox/useCheckbox.ts",
+ "imports": [
+ "import { useCheckbox } from '@mui/base/useCheckbox';",
+ "import { useCheckbox } from '@mui/base';"
+ ],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/react-checkbox/[docsTab]/index.js b/docs/pages/base-ui/react-checkbox/[docsTab]/index.js
new file mode 100644
index 0000000000..54b9ae7edf
--- /dev/null
+++ b/docs/pages/base-ui/react-checkbox/[docsTab]/index.js
@@ -0,0 +1,62 @@
+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/data/base/components/checkbox/checkbox.md?@mui/markdown';
+import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+import CheckboxApiJsonPageContent from '../../api/checkbox.json';
+import CheckboxIndicatorApiJsonPageContent from '../../api/checkbox-indicator.json';
+import useCheckboxApiJsonPageContent from '../../api/use-checkbox.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 CheckboxApiReq = require.context(
+ 'docs/translations/api-docs-base/checkbox',
+ false,
+ /checkbox.*.json$/,
+ );
+ const CheckboxApiDescriptions = mapApiPageTranslations(CheckboxApiReq);
+
+ const CheckboxIndicatorApiReq = require.context(
+ 'docs/translations/api-docs-base/checkbox-indicator',
+ false,
+ /checkbox-indicator.*.json$/,
+ );
+ const CheckboxIndicatorApiDescriptions = mapApiPageTranslations(CheckboxIndicatorApiReq);
+
+ const useCheckboxApiReq = require.context(
+ 'docs/translations/api-docs/use-checkbox',
+ false,
+ /use-checkbox.*.json$/,
+ );
+ const useCheckboxApiDescriptions = mapApiPageTranslations(useCheckboxApiReq);
+
+ return {
+ props: {
+ componentsApiDescriptions: {
+ Checkbox: CheckboxApiDescriptions,
+ CheckboxIndicator: CheckboxIndicatorApiDescriptions,
+ },
+ componentsApiPageContents: {
+ Checkbox: CheckboxApiJsonPageContent,
+ CheckboxIndicator: CheckboxIndicatorApiJsonPageContent,
+ },
+ hooksApiDescriptions: { useCheckbox: useCheckboxApiDescriptions },
+ hooksApiPageContents: { useCheckbox: useCheckboxApiJsonPageContent },
+ },
+ };
+};
diff --git a/docs/translations/api-docs-base/checkbox-indicator/checkbox-indicator.json b/docs/translations/api-docs-base/checkbox-indicator/checkbox-indicator.json
new file mode 100644
index 0000000000..bd044ef43c
--- /dev/null
+++ b/docs/translations/api-docs-base/checkbox-indicator/checkbox-indicator.json
@@ -0,0 +1,13 @@
+{
+ "componentDescription": "The indicator part of the Checkbox.",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "keepMounted": {
+ "description": "If true
, the indicator stays mounted when unchecked. Useful for CSS animations."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs-base/checkbox/checkbox.json b/docs/translations/api-docs-base/checkbox/checkbox.json
new file mode 100644
index 0000000000..d90583b81d
--- /dev/null
+++ b/docs/translations/api-docs-base/checkbox/checkbox.json
@@ -0,0 +1,27 @@
+{
+ "componentDescription": "The foundation for building custom-styled checkboxes.",
+ "propDescriptions": {
+ "checked": { "description": "If true
, the component is checked." },
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "defaultChecked": {
+ "description": "The default checked state. Use when the component is not controlled."
+ },
+ "disabled": { "description": "If true
, the component is disabled." },
+ "indeterminate": { "description": "If true
, the checkbox will be indeterminate." },
+ "name": { "description": "Name of the underlying input element." },
+ "onChange": {
+ "description": "Callback fired when the state is changed.",
+ "typeDescriptions": {
+ "event": "The event source of the callback. You can pull out the new value by accessing event.target.value
(string). You can pull out the new checked state by accessing event.target.checked
(boolean)."
+ }
+ },
+ "readOnly": { "description": "If true
, the component is read only." },
+ "render": { "description": "A function to customize rendering of the component." },
+ "required": {
+ "description": "If true
, the input
element is required."
+ }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/use-checkbox/use-checkbox.json b/docs/translations/api-docs/use-checkbox/use-checkbox.json
new file mode 100644
index 0000000000..f534acaa60
--- /dev/null
+++ b/docs/translations/api-docs/use-checkbox/use-checkbox.json
@@ -0,0 +1,24 @@
+{
+ "hookDescription": "The basic building block for creating custom checkboxes.",
+ "parametersDescriptions": {
+ "autoFocus": { "description": "If true
, the checkbox is focused on mount." },
+ "checked": { "description": "If true
, the component is checked." },
+ "defaultChecked": {
+ "description": "The default checked state. Use when the component is not controlled."
+ },
+ "disabled": { "description": "If true
, the component is disabled." },
+ "indeterminate": { "description": "If true
, the checkbox will be indeterminate." },
+ "inputRef": { "description": "The ref to the input element." },
+ "name": { "description": "Name of the underlying input element." },
+ "onChange": { "description": "Callback fired when the state is changed." },
+ "readOnly": { "description": "If true
, the component is read only." },
+ "required": {
+ "description": "If true
, the input
element is required."
+ }
+ },
+ "returnValueDescriptions": {
+ "checked": { "description": "If true
, the checkbox is checked." },
+ "getButtonProps": { "description": "Resolver for the button element's props." },
+ "getInputProps": { "description": "Resolver for the input element's props." }
+ }
+}
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index c7ee56a9da..4e3309788c 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -258,6 +258,8 @@
"/base-ui/react-transitions": "Transitions",
"/base-ui/react-badge/components-api/#badge": "Badge",
"/base-ui/react-button/components-api/#button": "Button",
+ "/base-ui/react-checkbox/components-api/#checkbox": "Checkbox",
+ "/base-ui/react-checkbox/components-api/#checkbox-indicator": "CheckboxIndicator",
"/base-ui/react-click-away-listener/components-api/#click-away-listener": "ClickAwayListener",
"/base-ui/react-transitions/components-api/#css-animation": "CssAnimation",
"/base-ui/react-transitions/components-api/#css-transition": "CssTransition",
@@ -290,6 +292,7 @@
"/base-ui/react-autocomplete/hooks-api/#use-autocomplete": "useAutocomplete",
"/base-ui/react-badge/hooks-api/#use-badge": "useBadge",
"/base-ui/react-button/hooks-api/#use-button": "useButton",
+ "/base-ui/react-checkbox/hooks-api/#use-checkbox": "useCheckbox",
"/base-ui/react-menu/hooks-api/#use-dropdown": "useDropdown",
"/base-ui/react-form-control/hooks-api/#use-form-control-context": "useFormControlContext",
"/base-ui/react-input/hooks-api/#use-input": "useInput",
diff --git a/packages/mui-base/src/Checkbox/Checkbox.test.tsx b/packages/mui-base/src/Checkbox/Checkbox.test.tsx
new file mode 100644
index 0000000000..e2e0471167
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/Checkbox.test.tsx
@@ -0,0 +1,307 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { spy } from 'sinon';
+import { createRenderer, act } from '@mui/internal-test-utils';
+import { Checkbox } from '.';
+import { describeConformance } from '../../test/describeConformance';
+
+const isJSDOM = /jsdom/.test(window.navigator.userAgent);
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ inheritComponent: 'button',
+ refInstanceof: window.HTMLButtonElement,
+ render,
+ }));
+
+ describe('extra props', () => {
+ it('can override the built-in attributes', () => {
+ const { container } = render( );
+ expect(container.firstElementChild as HTMLElement).to.have.attribute('role', 'switch');
+ });
+ });
+
+ it('should change its state when clicked', () => {
+ const { getAllByRole, container } = render( );
+ const [checkbox] = getAllByRole('checkbox');
+ const input = container.querySelector('input[type=checkbox]') as HTMLInputElement;
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ expect(input.checked).to.equal(false);
+
+ act(() => {
+ checkbox.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'true');
+ expect(input.checked).to.equal(true);
+
+ act(() => {
+ checkbox.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ expect(input.checked).to.equal(false);
+ });
+
+ it('should update its state when changed from outside', () => {
+ function Test() {
+ const [checked, setChecked] = React.useState(false);
+ return (
+
+ setChecked((c) => !c)}>Toggle
+ ;
+
+ );
+ }
+
+ const { getAllByRole, getByText } = render( );
+ const [checkbox] = getAllByRole('checkbox');
+ const button = getByText('Toggle');
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ act(() => {
+ button.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'true');
+
+ act(() => {
+ button.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ });
+
+ it('should call onChange when clicked', () => {
+ const handleChange = spy();
+ const { getAllByRole, container } = render( );
+ const [checkbox] = getAllByRole('checkbox');
+ const input = container.querySelector('input[type=checkbox]') as HTMLInputElement;
+
+ act(() => {
+ checkbox.click();
+ });
+
+ expect(handleChange.callCount).to.equal(1);
+ expect(handleChange.firstCall.args[0].target).to.equal(input);
+ });
+
+ describe('prop: disabled', () => {
+ it('should have the `aria-disabled` attribute', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-disabled', 'true');
+ });
+
+ it('should not have the aria attribute when `disabled` is not set', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).not.to.have.attribute('aria-disabled');
+ });
+
+ it('should not change its state when clicked', () => {
+ const { getAllByRole } = render( );
+ const [checkbox] = getAllByRole('checkbox');
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+
+ act(() => {
+ checkbox.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ });
+ });
+
+ describe('prop: readOnly', () => {
+ it('should have the `aria-readonly` attribute', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-readonly', 'true');
+ });
+
+ it('should not have the aria attribute when `readOnly` is not set', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).not.to.have.attribute('aria-readonly');
+ });
+
+ it('should not change its state when clicked', () => {
+ const { getAllByRole } = render( );
+ const [checkbox] = getAllByRole('checkbox');
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+
+ act(() => {
+ checkbox.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+ });
+ });
+
+ describe('prop: indeterminate', () => {
+ it('should set the `aria-checked` attribute as "mixed"', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-checked', 'mixed');
+ });
+
+ it('should not change its state when clicked', () => {
+ const { getAllByRole } = render( );
+ const [checkbox] = getAllByRole('checkbox');
+
+ expect(checkbox).to.have.attribute('aria-checked', 'mixed');
+
+ act(() => {
+ checkbox.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'mixed');
+ });
+
+ it('should not set the `data-indeterminate` attribute', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).to.not.have.attribute('data-indeterminate', 'true');
+ });
+
+ it('should not have the aria attribute when `indeterminate` is not set', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).not.to.have.attribute('aria-checked', 'mixed');
+ });
+
+ it('should not be overridden by `checked` prop', () => {
+ const { getAllByRole } = render( );
+ expect(getAllByRole('checkbox')[0]).to.have.attribute('aria-checked', 'mixed');
+ });
+ });
+
+ it('should update its state if the underlying input is toggled', () => {
+ const { getAllByRole, container } = render( );
+ const [checkbox] = getAllByRole('checkbox');
+ const input = container.querySelector('input[type=checkbox]') as HTMLInputElement;
+
+ act(() => {
+ input.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'true');
+ });
+
+ it('should place the style hooks on the root and the indicator', () => {
+ const { getAllByRole } = render(
+
+
+ ,
+ );
+
+ const [checkbox] = getAllByRole('checkbox');
+ const indicator = checkbox.querySelector('span');
+
+ expect(checkbox).to.have.attribute('data-state', 'checked');
+ expect(checkbox).to.have.attribute('data-disabled', 'true');
+ expect(checkbox).to.have.attribute('data-readonly', 'true');
+ expect(checkbox).to.have.attribute('data-required', 'true');
+
+ expect(indicator).to.have.attribute('data-state', 'checked');
+ expect(indicator).to.have.attribute('data-disabled', 'true');
+ expect(indicator).to.have.attribute('data-readonly', 'true');
+ expect(indicator).to.have.attribute('data-required', 'true');
+ });
+
+ it('should set the name attribute on the input', () => {
+ const { container } = render( );
+ const input = container.querySelector('input[type="checkbox"]')! as HTMLInputElement;
+
+ expect(input).to.have.attribute('name', 'checkbox-name');
+ });
+
+ describe('form handling', () => {
+ it('should toggle the checkbox when a parent label is clicked', function test() {
+ // Clicking the label causes unrelated browser tests to fail.
+ if (!isJSDOM) {
+ this.skip();
+ }
+
+ const { getByTestId, getAllByRole } = render(
+
+
+ Toggle
+ ,
+ );
+
+ const [checkbox] = getAllByRole('checkbox');
+ const label = getByTestId('label');
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+
+ act(() => {
+ label.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'true');
+ });
+
+ it('should toggle the checkbox when a linked label is clicked', function test() {
+ // Clicking the label causes unrelated browser tests to fail.
+ if (!isJSDOM) {
+ this.skip();
+ }
+
+ const { getByTestId, getAllByRole } = render(
+
+
+ Toggle
+
+
+
,
+ );
+
+ const [checkbox] = getAllByRole('checkbox');
+ const label = getByTestId('label');
+
+ expect(checkbox).to.have.attribute('aria-checked', 'false');
+
+ act(() => {
+ label.click();
+ });
+
+ expect(checkbox).to.have.attribute('aria-checked', 'true');
+ });
+ });
+
+ it('should include the checkbox value in the form submission', function test() {
+ if (isJSDOM) {
+ // FormData is not available in JSDOM
+ this.skip();
+ }
+
+ let stringifiedFormData = '';
+
+ const { getAllByRole, getByRole } = render(
+ ,
+ );
+
+ const [checkbox] = getAllByRole('checkbox');
+ const submitButton = getByRole('button')!;
+
+ submitButton.click();
+
+ expect(stringifiedFormData).to.equal('test-checkbox=off');
+
+ act(() => {
+ checkbox.click();
+ });
+
+ submitButton.click();
+
+ expect(stringifiedFormData).to.equal('test-checkbox=on');
+ });
+});
diff --git a/packages/mui-base/src/Checkbox/Checkbox.tsx b/packages/mui-base/src/Checkbox/Checkbox.tsx
new file mode 100644
index 0000000000..0cbf27e590
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/Checkbox.tsx
@@ -0,0 +1,143 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type { CheckboxOwnerState, CheckboxProps } from './Checkbox.types';
+import { resolveClassName } from '../utils/resolveClassName';
+import { CheckboxContext } from './CheckboxContext';
+import { useCheckbox } from '../useCheckbox';
+import { useCheckboxStyleHooks } from './utils';
+
+function defaultRender(props: React.ComponentPropsWithRef<'button'>) {
+ return ;
+}
+
+/**
+ * The foundation for building custom-styled checkboxes.
+ *
+ * Demos:
+ *
+ * - [Checkbox](https://mui.com/base-ui/react-checkbox/)
+ *
+ * API:
+ *
+ * - [Checkbox API](https://mui.com/base-ui/react-checkbox/components-api/#checkbox)
+ */
+const Checkbox = React.forwardRef(function Checkbox(
+ props: CheckboxProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const {
+ name,
+ onChange,
+ defaultChecked,
+ disabled = false,
+ readOnly = false,
+ indeterminate = false,
+ required = false,
+ checked: checkedProp,
+ render: renderProp,
+ className,
+ ...otherProps
+ } = props;
+ const render = renderProp ?? defaultRender;
+
+ const { checked, getInputProps, getButtonProps } = useCheckbox(props);
+
+ const ownerState: CheckboxOwnerState = React.useMemo(
+ () => ({
+ checked,
+ disabled,
+ readOnly,
+ required,
+ indeterminate,
+ }),
+ [checked, disabled, readOnly, required, indeterminate],
+ );
+
+ const styleHooks = useCheckboxStyleHooks(ownerState);
+
+ const buttonProps = {
+ className: resolveClassName(className, ownerState),
+ ref: forwardedRef,
+ ...styleHooks,
+ ...otherProps,
+ };
+
+ return (
+
+ {render(getButtonProps(buttonProps), ownerState)}
+ {!checked && props.name && }
+
+
+ );
+});
+
+Checkbox.propTypes /* remove-proptypes */ = {
+ // βββββββββββββββββββββββββββββββ Warning βββββββββββββββββββββββββββββββ
+ // β These PropTypes are generated from the TypeScript type definitions. β
+ // β To update them, edit the TypeScript types and run `pnpm proptypes`. β
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ /**
+ * If `true`, the component is checked.
+ *
+ * @default undefined
+ */
+ checked: PropTypes.bool,
+ /**
+ * @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 state. Use when the component is not controlled.
+ *
+ * @default false
+ */
+ defaultChecked: PropTypes.bool,
+ /**
+ * If `true`, the component is disabled.
+ *
+ * @default false
+ */
+ disabled: PropTypes.bool,
+ /**
+ * If `true`, the checkbox will be indeterminate.
+ *
+ * @default false
+ */
+ indeterminate: PropTypes.bool,
+ /**
+ * Name of the underlying input element.
+ *
+ * @default undefined
+ */
+ name: PropTypes.string,
+ /**
+ * Callback fired when the state is changed.
+ *
+ * @param {React.ChangeEvent} event The event source of the callback.
+ * You can pull out the new value by accessing `event.target.value` (string).
+ * You can pull out the new checked state by accessing `event.target.checked` (boolean).
+ */
+ onChange: PropTypes.func,
+ /**
+ * If `true`, the component is read only.
+ *
+ * @default false
+ */
+ readOnly: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.func,
+ /**
+ * If `true`, the `input` element is required.
+ *
+ * @default false
+ */
+ required: PropTypes.bool,
+} as any;
+
+export { Checkbox };
diff --git a/packages/mui-base/src/Checkbox/Checkbox.types.ts b/packages/mui-base/src/Checkbox/Checkbox.types.ts
new file mode 100644
index 0000000000..58aa3c6f63
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/Checkbox.types.ts
@@ -0,0 +1,22 @@
+import type { BaseUIComponentProps } from '../utils/BaseUI.types';
+import type { UseCheckboxParameters } from '../useCheckbox';
+
+export type CheckboxOwnerState = {
+ checked: boolean;
+ disabled: boolean;
+ readOnly: boolean;
+ required: boolean;
+ indeterminate: boolean;
+};
+
+export interface CheckboxProps
+ extends UseCheckboxParameters,
+ Omit, 'onChange'> {}
+
+export interface CheckboxIndicatorProps extends BaseUIComponentProps<'span', CheckboxOwnerState> {
+ /**
+ * If `true`, the indicator stays mounted when unchecked. Useful for CSS animations.
+ * @default false
+ */
+ keepMounted?: boolean;
+}
diff --git a/packages/mui-base/src/Checkbox/CheckboxContext.ts b/packages/mui-base/src/Checkbox/CheckboxContext.ts
new file mode 100644
index 0000000000..2b73bb848f
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/CheckboxContext.ts
@@ -0,0 +1,6 @@
+import * as React from 'react';
+import type { CheckboxOwnerState } from './Checkbox.types';
+
+export type CheckboxContextValue = CheckboxOwnerState;
+
+export const CheckboxContext = React.createContext(null);
diff --git a/packages/mui-base/src/Checkbox/CheckboxIndicator.test.tsx b/packages/mui-base/src/Checkbox/CheckboxIndicator.test.tsx
new file mode 100644
index 0000000000..5f2441dd89
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/CheckboxIndicator.test.tsx
@@ -0,0 +1,91 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createRenderer } from '@mui/internal-test-utils';
+import { describeConformance } from '../../test/describeConformance';
+import { Checkbox } from '.';
+import { CheckboxContext } from './CheckboxContext';
+
+const testContext = {
+ checked: true,
+ disabled: false,
+ readOnly: false,
+ required: false,
+ indeterminate: false,
+};
+
+describe(' ', () => {
+ const { render } = createRenderer();
+
+ describeConformance( , () => ({
+ inheritComponent: 'span',
+ refInstanceof: window.HTMLSpanElement,
+ render(node) {
+ return render(
+ {node} ,
+ );
+ },
+ skip: ['reactTestRenderer'],
+ }));
+
+ it('should not render indicator by default', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ const indicator = container.querySelector('span');
+ expect(indicator).to.equal(null);
+ });
+
+ it('should render indicator when checked', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ const indicator = container.querySelector('span');
+ expect(indicator).not.to.equal(null);
+ });
+
+ it('should spread extra props', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ const indicator = container.querySelector('span');
+ expect(indicator).to.have.attribute('data-extra-prop', 'Lorem ipsum');
+ });
+
+ describe('keepMounted prop', () => {
+ it('should keep indicator mounted when unchecked', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ const indicator = container.querySelector('span');
+ expect(indicator).not.to.equal(null);
+ });
+
+ it('should keep indicator mounted when checked', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ const indicator = container.querySelector('span');
+ expect(indicator).not.to.equal(null);
+ });
+
+ it('should keep indicator mounted when indeterminate', () => {
+ const { container } = render(
+
+
+ ,
+ );
+ const indicator = container.querySelector('span');
+ expect(indicator).not.to.equal(null);
+ });
+ });
+});
diff --git a/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx b/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx
new file mode 100644
index 0000000000..587d81f746
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/CheckboxIndicator.tsx
@@ -0,0 +1,75 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type { CheckboxIndicatorProps } from './Checkbox.types';
+import { CheckboxContext } from './CheckboxContext';
+import { resolveClassName } from '../utils/resolveClassName';
+import { useCheckboxStyleHooks } from './utils';
+
+function defaultRender(props: React.ComponentPropsWithRef<'span'>) {
+ return ;
+}
+
+/**
+ * The indicator part of the Checkbox.
+ *
+ * Demos:
+ *
+ * - [Checkbox](https://mui.com/base-ui/react-checkbox/)
+ *
+ * API:
+ *
+ * - [CheckboxIndicator API](https://mui.com/base-ui/react-checkbox/components-api/#checkbox-indicator)
+ */
+const CheckboxIndicator = React.forwardRef(function CheckboxIndicator(
+ props: CheckboxIndicatorProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render: renderProp, className, keepMounted = false, ...otherProps } = props;
+ const render = renderProp ?? defaultRender;
+
+ const ownerState = React.useContext(CheckboxContext);
+ if (ownerState === null) {
+ throw new Error('Base UI: Checkbox.Indicator is not placed inside the Checkbox component.');
+ }
+
+ const styleHooks = useCheckboxStyleHooks(ownerState);
+
+ if (!keepMounted && !ownerState.checked && !ownerState.indeterminate) {
+ return null;
+ }
+
+ const elementProps = {
+ className: resolveClassName(className, ownerState),
+ ref: forwardedRef,
+ ...styleHooks,
+ ...otherProps,
+ };
+
+ return render(elementProps, ownerState);
+});
+
+CheckboxIndicator.propTypes /* remove-proptypes */ = {
+ // βββββββββββββββββββββββββββββββ Warning βββββββββββββββββββββββββββββββ
+ // β These PropTypes are generated from the TypeScript type definitions. β
+ // β To update them, edit the TypeScript types and run `pnpm proptypes`. β
+ // βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ /**
+ * @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]),
+ /**
+ * If `true`, the indicator stays mounted when unchecked. Useful for CSS animations.
+ * @default false
+ */
+ keepMounted: PropTypes.bool,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.func,
+} as any;
+
+export { CheckboxIndicator };
diff --git a/packages/mui-base/src/Checkbox/index.ts b/packages/mui-base/src/Checkbox/index.ts
new file mode 100644
index 0000000000..66e7a057b6
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/index.ts
@@ -0,0 +1,10 @@
+'use client';
+import { Checkbox as CheckboxRoot } from './Checkbox';
+import { CheckboxIndicator } from './CheckboxIndicator';
+import { combineComponentExports } from '../utils/combineComponentExports';
+
+export * from './Checkbox.types';
+
+export const Checkbox = combineComponentExports(CheckboxRoot, {
+ Indicator: CheckboxIndicator,
+});
diff --git a/packages/mui-base/src/Checkbox/utils.ts b/packages/mui-base/src/Checkbox/utils.ts
new file mode 100644
index 0000000000..c52e1d216a
--- /dev/null
+++ b/packages/mui-base/src/Checkbox/utils.ts
@@ -0,0 +1,22 @@
+import * as React from 'react';
+import { getStyleHookProps } from '../utils/getStyleHookProps';
+import type { CheckboxOwnerState } from './Checkbox.types';
+
+export function useCheckboxStyleHooks(ownerState: CheckboxOwnerState) {
+ return React.useMemo(() => {
+ return getStyleHookProps(ownerState, {
+ // `data-state="mixed"` is used to style the checkbox when it's indeterminate
+ indeterminate: () => null,
+ checked(value) {
+ let state = value ? 'checked' : 'unchecked';
+ if (ownerState.indeterminate) {
+ state = 'mixed';
+ }
+
+ return {
+ 'data-state': state,
+ };
+ },
+ });
+ }, [ownerState]);
+}
diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts
index 9f29437e3b..233d193879 100644
--- a/packages/mui-base/src/index.ts
+++ b/packages/mui-base/src/index.ts
@@ -1,6 +1,7 @@
export * from './utils';
export * from './Badge';
export * from './Button';
+export * from './Checkbox';
export * from './ClickAwayListener';
export * from './composeClasses';
export * from './Dropdown';
@@ -32,6 +33,7 @@ export * from './Transitions';
export * from './useAutocomplete';
export * from './useBadge';
export * from './useButton';
+export * from './useCheckbox';
export * from './useDropdown';
export * from './useInput';
export * from './useMenu';
diff --git a/packages/mui-base/src/useCheckbox/index.ts b/packages/mui-base/src/useCheckbox/index.ts
new file mode 100644
index 0000000000..395bcb56cc
--- /dev/null
+++ b/packages/mui-base/src/useCheckbox/index.ts
@@ -0,0 +1,3 @@
+'use client';
+export { useCheckbox } from './useCheckbox';
+export * from './useCheckbox.types';
diff --git a/packages/mui-base/src/useCheckbox/useCheckbox.ts b/packages/mui-base/src/useCheckbox/useCheckbox.ts
new file mode 100644
index 0000000000..5828604a8d
--- /dev/null
+++ b/packages/mui-base/src/useCheckbox/useCheckbox.ts
@@ -0,0 +1,104 @@
+import * as React from 'react';
+import { useControlled } from '../utils/useControlled';
+import type { UseCheckboxParameters, UseCheckboxReturnValue } from './useCheckbox.types';
+import { visuallyHidden } from '../utils/visuallyHidden';
+import { useForkRef } from '../utils/useForkRef';
+import { mergeReactProps } from '../utils/mergeReactProps';
+
+/**
+ * The basic building block for creating custom checkboxes.
+ *
+ * Demos:
+ *
+ * - [Checkbox](https://mui.com/base-ui/react-checkbox/#hook)
+ *
+ * API:
+ *
+ * - [useCheckbox API](https://mui.com/base-ui/react-checkbox/hooks-api/#use-checkbox)
+ */
+export function useCheckbox(params: UseCheckboxParameters): UseCheckboxReturnValue {
+ const {
+ checked: externalChecked,
+ inputRef: externalInputRef,
+ name,
+ onChange,
+ defaultChecked = false,
+ disabled = false,
+ readOnly = false,
+ required = false,
+ autoFocus = false,
+ indeterminate = false,
+ } = params;
+
+ const inputRef = React.useRef(null);
+ const mergedInputRef = useForkRef(externalInputRef, inputRef);
+
+ React.useEffect(() => {
+ if (inputRef.current) {
+ inputRef.current.indeterminate = indeterminate;
+ }
+ }, [indeterminate]);
+
+ const [checked, setCheckedState] = useControlled({
+ controlled: externalChecked,
+ default: defaultChecked,
+ name: 'Checkbox',
+ state: 'checked',
+ });
+
+ const getButtonProps: UseCheckboxReturnValue['getButtonProps'] = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps<'button'>(externalProps, {
+ value: 'off',
+ type: 'button',
+ role: 'checkbox',
+ 'aria-checked': indeterminate ? 'mixed' : checked,
+ 'aria-disabled': disabled || undefined,
+ 'aria-readonly': readOnly || undefined,
+ ...externalProps,
+ onClick(event) {
+ if (event.defaultPrevented || readOnly) {
+ return;
+ }
+
+ inputRef.current?.click();
+ },
+ }),
+ [checked, disabled, indeterminate, readOnly],
+ );
+
+ const getInputProps: UseCheckboxReturnValue['getInputProps'] = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps<'input'>(externalProps, {
+ checked,
+ disabled,
+ name,
+ required,
+ autoFocus,
+ ref: mergedInputRef,
+ style: visuallyHidden,
+ tabIndex: -1,
+ type: 'checkbox',
+ 'aria-hidden': true,
+ onChange(event) {
+ // Workaround for https://github.com/facebook/react/issues/9023
+ if (event.nativeEvent.defaultPrevented) {
+ return;
+ }
+
+ setCheckedState(event.target.checked);
+ onChange?.(event);
+ },
+ }),
+ [autoFocus, checked, disabled, name, onChange, required, setCheckedState, mergedInputRef],
+ );
+
+ return React.useMemo(
+ () => ({
+ checked,
+ getButtonProps,
+ getInputProps,
+ }),
+ [checked, getButtonProps, getInputProps],
+ );
+}
diff --git a/packages/mui-base/src/useCheckbox/useCheckbox.types.ts b/packages/mui-base/src/useCheckbox/useCheckbox.types.ts
new file mode 100644
index 0000000000..a81afba044
--- /dev/null
+++ b/packages/mui-base/src/useCheckbox/useCheckbox.types.ts
@@ -0,0 +1,87 @@
+import * as React from 'react';
+
+export interface UseCheckboxParameters {
+ /**
+ * Name of the underlying input element.
+ *
+ * @default undefined
+ */
+ name?: string;
+ /**
+ * If `true`, the component is checked.
+ *
+ * @default undefined
+ */
+ checked?: boolean;
+ /**
+ * The default checked state. Use when the component is not controlled.
+ *
+ * @default false
+ */
+ defaultChecked?: boolean;
+ /**
+ * If `true`, the component is disabled.
+ *
+ * @default false
+ */
+ disabled?: boolean;
+ /**
+ * Callback fired when the state is changed.
+ *
+ * @param {React.ChangeEvent} event The event source of the callback.
+ * You can pull out the new value by accessing `event.target.value` (string).
+ * You can pull out the new checked state by accessing `event.target.checked` (boolean).
+ */
+ onChange?: React.ChangeEventHandler;
+ /**
+ * If `true`, the component is read only.
+ *
+ * @default false
+ */
+ readOnly?: boolean;
+ /**
+ * If `true`, the `input` element is required.
+ *
+ * @default false
+ */
+ required?: boolean;
+ /**
+ * If `true`, the checkbox is focused on mount.
+ *
+ * @default false
+ */
+ autoFocus?: boolean;
+ /**
+ * If `true`, the checkbox will be indeterminate.
+ *
+ * @default false
+ */
+ indeterminate?: boolean;
+ /**
+ * The ref to the input element.
+ */
+ inputRef?: React.Ref;
+}
+
+export interface UseCheckboxReturnValue {
+ /**
+ * If `true`, the checkbox is checked.
+ */
+ checked: boolean;
+ /**
+ * Resolver for the input element's props.
+ * @param externalProps custom props for the input element
+ * @returns props that should be spread on the input element
+ */
+ getInputProps: (
+ externalProps?: React.ComponentPropsWithRef<'input'>,
+ ) => React.ComponentPropsWithRef<'input'>;
+ /**
+ * Resolver for the button element's props.
+ * @param externalProps custom props for the button element
+ * @returns props that should be spread on the button element
+ */
+ getButtonProps: (
+ externalProps?: React.ComponentPropsWithRef<'button'>,
+ ) => React.ComponentPropsWithRef<'button'>;
+}
diff --git a/packages/mui-base/src/utils/mergeReactProps.ts b/packages/mui-base/src/utils/mergeReactProps.ts
index 1d78730da8..4a34eff94b 100644
--- a/packages/mui-base/src/utils/mergeReactProps.ts
+++ b/packages/mui-base/src/utils/mergeReactProps.ts
@@ -12,7 +12,7 @@ import type { BaseUIEvent, WithBaseUIEvent } from './BaseUI.types';
* @returns the merged props.
*/
export function mergeReactProps(
- externalProps: WithBaseUIEvent>,
+ externalProps: WithBaseUIEvent>,
internalProps: React.ComponentPropsWithRef,
): WithBaseUIEvent> {
return Object.entries(externalProps).reduce(