diff --git a/docs/data/base/components/progress/IndeterminateProgress.js b/docs/data/base/components/progress/IndeterminateProgress.js
new file mode 100644
index 0000000000..3e504194c3
--- /dev/null
+++ b/docs/data/base/components/progress/IndeterminateProgress.js
@@ -0,0 +1,75 @@
+import * as React from 'react';
+import { styled, keyframes, css } from '@mui/system';
+import * as BaseProgress from '@base_ui/react/Progress';
+
+export default function IndeterminateProgress() {
+ return (
+
+
+
+ );
+}
+
+const Progress = styled(BaseProgress.Root)`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1rem;
+`;
+
+const ProgressTrack = styled(BaseProgress.Track)(
+ ({ theme }) => `
+ position: relative;
+ width: 100%;
+ height: 4px;
+ border-radius: 9999px;
+ background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]};
+ display: flex;
+ overflow: hidden;
+ `,
+);
+
+const indeterminateLoading = keyframes`
+ from {
+ transform: translateX(-100%);
+ }
+
+ to {
+ transform: translateX(20rem);
+ }
+`;
+
+const ProgressIndicator = styled(BaseProgress.Indicator)(
+ ({ theme }) => css`
+ background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500};
+ border-radius: inherit;
+
+ &[data-state='indeterminate'] {
+ width: 25%;
+ animation: ${indeterminateLoading} 1.5s infinite ease-in-out;
+ will-change: transform;
+ }
+ `,
+);
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const BLUE400 = '#3399FF';
+const BLUE500 = '#007FFF';
diff --git a/docs/data/base/components/progress/IndeterminateProgress.tsx b/docs/data/base/components/progress/IndeterminateProgress.tsx
new file mode 100644
index 0000000000..3e504194c3
--- /dev/null
+++ b/docs/data/base/components/progress/IndeterminateProgress.tsx
@@ -0,0 +1,75 @@
+import * as React from 'react';
+import { styled, keyframes, css } from '@mui/system';
+import * as BaseProgress from '@base_ui/react/Progress';
+
+export default function IndeterminateProgress() {
+ return (
+
+
+
+ );
+}
+
+const Progress = styled(BaseProgress.Root)`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1rem;
+`;
+
+const ProgressTrack = styled(BaseProgress.Track)(
+ ({ theme }) => `
+ position: relative;
+ width: 100%;
+ height: 4px;
+ border-radius: 9999px;
+ background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]};
+ display: flex;
+ overflow: hidden;
+ `,
+);
+
+const indeterminateLoading = keyframes`
+ from {
+ transform: translateX(-100%);
+ }
+
+ to {
+ transform: translateX(20rem);
+ }
+`;
+
+const ProgressIndicator = styled(BaseProgress.Indicator)(
+ ({ theme }) => css`
+ background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500};
+ border-radius: inherit;
+
+ &[data-state='indeterminate'] {
+ width: 25%;
+ animation: ${indeterminateLoading} 1.5s infinite ease-in-out;
+ will-change: transform;
+ }
+ `,
+);
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const BLUE400 = '#3399FF';
+const BLUE500 = '#007FFF';
diff --git a/docs/data/base/components/progress/IndeterminateProgress.tsx.preview b/docs/data/base/components/progress/IndeterminateProgress.tsx.preview
new file mode 100644
index 0000000000..e45ea5cb8b
--- /dev/null
+++ b/docs/data/base/components/progress/IndeterminateProgress.tsx.preview
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/data/base/components/progress/RtlProgress.js b/docs/data/base/components/progress/RtlProgress.js
new file mode 100644
index 0000000000..149c0dd84c
--- /dev/null
+++ b/docs/data/base/components/progress/RtlProgress.js
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import * as BaseProgress from '@base_ui/react/Progress';
+
+export default function RtlProgress() {
+ return (
+
+
+
+ );
+}
+
+const Progress = styled(BaseProgress.Root)`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1rem;
+`;
+
+const ProgressTrack = styled(BaseProgress.Track)(
+ ({ theme }) => `
+ position: relative;
+ width: 100%;
+ height: 4px;
+ border-radius: 9999px;
+ background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]};
+ display: flex;
+ overflow: hidden;
+ `,
+);
+
+const ProgressIndicator = styled(BaseProgress.Indicator)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500};
+ border-radius: inherit;
+ `,
+);
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const BLUE400 = '#3399FF';
+const BLUE500 = '#007FFF';
diff --git a/docs/data/base/components/progress/RtlProgress.tsx b/docs/data/base/components/progress/RtlProgress.tsx
new file mode 100644
index 0000000000..149c0dd84c
--- /dev/null
+++ b/docs/data/base/components/progress/RtlProgress.tsx
@@ -0,0 +1,59 @@
+import * as React from 'react';
+import { styled } from '@mui/system';
+import * as BaseProgress from '@base_ui/react/Progress';
+
+export default function RtlProgress() {
+ return (
+
+
+
+ );
+}
+
+const Progress = styled(BaseProgress.Root)`
+ display: flex;
+ flex-flow: column nowrap;
+ gap: 1rem;
+`;
+
+const ProgressTrack = styled(BaseProgress.Track)(
+ ({ theme }) => `
+ position: relative;
+ width: 100%;
+ height: 4px;
+ border-radius: 9999px;
+ background-color: ${theme.palette.mode === 'dark' ? grey[400] : grey[400]};
+ display: flex;
+ overflow: hidden;
+ `,
+);
+
+const ProgressIndicator = styled(BaseProgress.Indicator)(
+ ({ theme }) => `
+ background-color: ${theme.palette.mode === 'dark' ? BLUE400 : BLUE500};
+ border-radius: inherit;
+ `,
+);
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const BLUE400 = '#3399FF';
+const BLUE500 = '#007FFF';
diff --git a/docs/data/base/components/progress/RtlProgress.tsx.preview b/docs/data/base/components/progress/RtlProgress.tsx.preview
new file mode 100644
index 0000000000..daa832be54
--- /dev/null
+++ b/docs/data/base/components/progress/RtlProgress.tsx.preview
@@ -0,0 +1,8 @@
+
\ No newline at end of file
diff --git a/docs/data/base/components/progress/UnstyledProgressIntroduction.js b/docs/data/base/components/progress/UnstyledProgressIntroduction.js
new file mode 100644
index 0000000000..d39f50a80f
--- /dev/null
+++ b/docs/data/base/components/progress/UnstyledProgressIntroduction.js
@@ -0,0 +1,95 @@
+import * as React from 'react';
+import { useTheme } from '@mui/system';
+import * as Progress from '@base_ui/react/Progress';
+
+export default function UnstyledProgressIntroduction() {
+ return (
+
+
+
+ Uploading files
+
+
+
+
+
+
+
+ );
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+export function Styles() {
+ const isDarkMode = useIsDarkMode();
+ return (
+
+ );
+}
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const BLUE400 = '#3399FF';
+const BLUE500 = '#007FFF';
diff --git a/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx
new file mode 100644
index 0000000000..d39f50a80f
--- /dev/null
+++ b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx
@@ -0,0 +1,95 @@
+import * as React from 'react';
+import { useTheme } from '@mui/system';
+import * as Progress from '@base_ui/react/Progress';
+
+export default function UnstyledProgressIntroduction() {
+ return (
+
+
+
+ Uploading files
+
+
+
+
+
+
+
+ );
+}
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+export function Styles() {
+ const isDarkMode = useIsDarkMode();
+ return (
+
+ );
+}
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const BLUE400 = '#3399FF';
+const BLUE500 = '#007FFF';
diff --git a/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx.preview b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx.preview
new file mode 100644
index 0000000000..b710e80892
--- /dev/null
+++ b/docs/data/base/components/progress/UnstyledProgressIntroduction.tsx.preview
@@ -0,0 +1,9 @@
+
+
+ Uploading files
+
+
+
+
+
+
\ No newline at end of file
diff --git a/docs/data/base/components/progress/progress.md b/docs/data/base/components/progress/progress.md
new file mode 100644
index 0000000000..43a85431ea
--- /dev/null
+++ b/docs/data/base/components/progress/progress.md
@@ -0,0 +1,137 @@
+---
+productId: base-ui
+title: React Progress components
+components: ProgressRoot, ProgressTrack, ProgressIndicator
+hooks: useProgressRoot, useProgressIndicator
+githubLabel: 'component: progress'
+waiAria: https://www.w3.org/TR/wai-aria-1.2/#progressbar
+packageName: '@base_ui/react'
+---
+
+# Progress
+
+The Progress component displays the status of a task or operation over time.
+
+{{"component": "@mui/docs/ComponentLinkHeader", "design": false}}
+
+{{"component": "modules/components/ComponentPageTabs.js"}}
+
+{{"demo": "UnstyledProgressIntroduction.js", "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 component.
+
+```jsx
+import * as Progress from '@base_ui/react/Progress';
+```
+
+### Anatomy
+
+Progress
+
+- `` is a top-level component that wraps the other components.
+- `` renders the rail that represents the total length or duration of progress.
+- `` renders the filled portion of the track.
+
+```tsx
+
+
+
+
+
+```
+
+## Value
+
+### Determinate
+
+The `value` prop represents the percentage value of the Progress component. The default minimum and maximum values are `0` and `100`, and can be changed with the `min` and `max` props. When progress is determinate the `data-state` attribute is initially `'loading'`, changing to `'complete'` when `value` equals `max`.
+
+```tsx
+function App() {
+ const [progressValue] = React.useState(25);
+ return (
+
+
+
+
+
+ );
+}
+```
+
+### Indeterminate
+
+Set `value` to `null` to configure an indeterminate progress bar. The `data-state` attribute will be set to `indeterminate`.
+
+```tsx
+
+
+
+
+
+```
+
+{{"demo": "IndeterminateProgress.js"}}
+
+## RTL
+
+Set the `direction` prop to `'rtl'` to change the direction that the `Indicator` fills towards for right-to-left languages:
+
+```jsx
+{/* Subcomponents */}
+```
+
+{{"demo": "RtlProgress.js"}}
+
+## Overriding default components
+
+Use the `render` prop to override the rendered element for all subcomponents:
+
+```jsx
+} />
+// or
+ } />
+```
+
+## Accessibility
+
+The Progress component implements the [ARIA progressbar specification](https://www.w3.org/TR/wai-aria-1.2/#progressbar).
+
+When using Progress, ensure that it has a human-readable text label by using either the `aria-label`, `aria-labelledby`, or `getAriaLabel` prop:
+
+```tsx
+
+ Loading progress
+
+
+
+
+
+// or
+
+
+
+
+
+
+```
diff --git a/docs/data/base/pages.ts b/docs/data/base/pages.ts
index 0ce753a28b..63c7f332e9 100644
--- a/docs/data/base/pages.ts
+++ b/docs/data/base/pages.ts
@@ -49,7 +49,7 @@ const pages: readonly MuiPage[] = [
children: [
{ pathname: '/base-ui/react-alert-dialog', title: 'Alert Dialog' },
{ pathname: '/base-ui/react-dialog', title: 'Dialog' },
- // { pathname: '/base-ui/react-snackbar', title: 'Snackbar' },
+ { pathname: '/base-ui/react-progress', title: 'Progress' },
],
},
// {
diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js
index d9d16fe2e6..a90cdc701d 100644
--- a/docs/data/base/pagesApi.js
+++ b/docs/data/base/pagesApi.js
@@ -169,6 +169,18 @@ module.exports = [
{ pathname: '/base-ui/react-popper/components-api/#popper', title: 'Popper' },
{ pathname: '/base-ui/react-popup/components-api/#popup', title: 'Popup' },
{ pathname: '/base-ui/react-portal/components-api/#portal', title: 'Portal' },
+ {
+ pathname: '/base-ui/react-progress/components-api/#progress-indicator',
+ title: 'ProgressIndicator',
+ },
+ {
+ pathname: '/base-ui/react-progress/components-api/#progress-root',
+ title: 'ProgressRoot',
+ },
+ {
+ pathname: '/base-ui/react-progress/components-api/#progress-track',
+ title: 'ProgressTrack',
+ },
{ pathname: '/base-ui/react-select/components-api/#select', title: 'Select' },
{
pathname: '/base-ui/react-slider/components-api/#slider-control',
@@ -298,6 +310,14 @@ module.exports = [
pathname: '/base-ui/react-select/hooks-api/#use-option-context-stabilizer',
title: 'useOptionContextStabilizer',
},
+ {
+ pathname: '/base-ui/react-progress/hooks-api/#use-progress-indicator',
+ title: 'useProgressIndicator',
+ },
+ {
+ pathname: '/base-ui/react-progress/hooks-api/#use-progress-root',
+ title: 'useProgressRoot',
+ },
{ pathname: '/base-ui/react-select/hooks-api/#use-select', title: 'useSelect' },
{
pathname: '/base-ui/react-slider/hooks-api/#use-slider-control',
diff --git a/docs/pages/base-ui/api/progress-indicator.json b/docs/pages/base-ui/api/progress-indicator.json
new file mode 100644
index 0000000000..aa7e0347eb
--- /dev/null
+++ b/docs/pages/base-ui/api/progress-indicator.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func
| string" } },
+ "render": { "type": { "name": "union", "description": "element
| func" } }
+ },
+ "name": "ProgressIndicator",
+ "imports": [
+ "import * as Progress from '@base_ui/react/Progress';\nconst ProgressIndicator = Progress.Indicator;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "ProgressIndicator",
+ "forwardsRefTo": "HTMLSpanElement",
+ "filename": "/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/progress-root.json b/docs/pages/base-ui/api/progress-root.json
new file mode 100644
index 0000000000..3f19432036
--- /dev/null
+++ b/docs/pages/base-ui/api/progress-root.json
@@ -0,0 +1,43 @@
+{
+ "props": {
+ "aria-label": { "type": { "name": "string" } },
+ "aria-labelledby": { "type": { "name": "string" } },
+ "aria-valuetext": { "type": { "name": "string" } },
+ "className": { "type": { "name": "union", "description": "func
| string" } },
+ "direction": {
+ "type": { "name": "enum", "description": "'ltr'
| 'rtl'" },
+ "default": "'ltr'"
+ },
+ "getAriaLabel": {
+ "type": { "name": "func" },
+ "signature": {
+ "type": "function(value: number | null) => string",
+ "describedArgs": ["value"]
+ }
+ },
+ "getAriaValueText": {
+ "type": { "name": "func" },
+ "signature": {
+ "type": "function(value: number | null) => string",
+ "describedArgs": ["value"]
+ }
+ },
+ "max": { "type": { "name": "number" }, "default": "100" },
+ "min": { "type": { "name": "number" }, "default": "0" },
+ "render": { "type": { "name": "union", "description": "element
| func" } },
+ "value": { "type": { "name": "number" }, "default": "null" }
+ },
+ "name": "ProgressRoot",
+ "imports": [
+ "import * as Progress from '@base_ui/react/Progress';\nconst ProgressRoot = Progress.Root;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "ProgressRoot",
+ "forwardsRefTo": "HTMLDivElement",
+ "filename": "/packages/mui-base/src/Progress/Root/ProgressRoot.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/progress-track.json b/docs/pages/base-ui/api/progress-track.json
new file mode 100644
index 0000000000..d3c67ca359
--- /dev/null
+++ b/docs/pages/base-ui/api/progress-track.json
@@ -0,0 +1,19 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func
| string" } },
+ "render": { "type": { "name": "union", "description": "element
| func" } }
+ },
+ "name": "ProgressTrack",
+ "imports": [
+ "import * as Progress from '@base_ui/react/Progress';\nconst ProgressTrack = Progress.Track;"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "ProgressTrack",
+ "forwardsRefTo": "HTMLSpanElement",
+ "filename": "/packages/mui-base/src/Progress/Track/ProgressTrack.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/use-progress-indicator.json b/docs/pages/base-ui/api/use-progress-indicator.json
new file mode 100644
index 0000000000..7d06812894
--- /dev/null
+++ b/docs/pages/base-ui/api/use-progress-indicator.json
@@ -0,0 +1,27 @@
+{
+ "parameters": {
+ "value": {
+ "type": { "name": "number | null", "description": "number | null" },
+ "required": true
+ },
+ "direction": {
+ "type": { "name": "ProgressDirection", "description": "ProgressDirection" },
+ "default": "'ltr'"
+ },
+ "max": { "type": { "name": "number", "description": "number" }, "default": "100" },
+ "min": { "type": { "name": "number", "description": "number" }, "default": "0" }
+ },
+ "returnValue": {
+ "getRootProps": {
+ "type": {
+ "name": "(externalProps?: React.ComponentPropsWithRef<'span'>) => React.ComponentPropsWithRef<'span'>",
+ "description": "(externalProps?: React.ComponentPropsWithRef<'span'>) => React.ComponentPropsWithRef<'span'>"
+ },
+ "required": true
+ }
+ },
+ "name": "useProgressIndicator",
+ "filename": "/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts",
+ "imports": ["import { useProgressIndicator } from '@base_ui/react/Progress';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/api/use-progress-root.json b/docs/pages/base-ui/api/use-progress-root.json
new file mode 100644
index 0000000000..bd60ccd8f7
--- /dev/null
+++ b/docs/pages/base-ui/api/use-progress-root.json
@@ -0,0 +1,57 @@
+{
+ "parameters": {
+ "value": {
+ "type": { "name": "number | null", "description": "number | null" },
+ "default": "null",
+ "required": true
+ },
+ "aria-label": { "type": { "name": "string", "description": "string" } },
+ "aria-labelledby": { "type": { "name": "string", "description": "string" } },
+ "aria-valuetext": { "type": { "name": "string", "description": "string" } },
+ "direction": {
+ "type": { "name": "ProgressDirection", "description": "ProgressDirection" },
+ "default": "'ltr'"
+ },
+ "getAriaLabel": {
+ "type": {
+ "name": "(index: number | null) => string",
+ "description": "(index: number | null) => string"
+ }
+ },
+ "getAriaValueText": {
+ "type": {
+ "name": "(value: number | null) => string",
+ "description": "(value: number | null) => string"
+ }
+ },
+ "max": { "type": { "name": "number", "description": "number" }, "default": "100" },
+ "min": { "type": { "name": "number", "description": "number" }, "default": "0" }
+ },
+ "returnValue": {
+ "direction": {
+ "type": { "name": "ProgressDirection", "description": "ProgressDirection" },
+ "required": true
+ },
+ "getRootProps": {
+ "type": {
+ "name": "(externalProps?: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>",
+ "description": "(externalProps?: React.ComponentPropsWithRef<'div'>) => React.ComponentPropsWithRef<'div'>"
+ },
+ "required": true
+ },
+ "max": { "type": { "name": "number", "description": "number" }, "required": true },
+ "min": { "type": { "name": "number", "description": "number" }, "required": true },
+ "state": {
+ "type": { "name": "ProgressStatus", "description": "ProgressStatus" },
+ "required": true
+ },
+ "value": {
+ "type": { "name": "number | null", "description": "number | null" },
+ "required": true
+ }
+ },
+ "name": "useProgressRoot",
+ "filename": "/packages/mui-base/src/Progress/Root/useProgressRoot.ts",
+ "imports": ["import { useProgressRoot } from '@base_ui/react/Progress';"],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/react-progress/[docsTab]/index.js b/docs/pages/base-ui/react-progress/[docsTab]/index.js
new file mode 100644
index 0000000000..e96594e077
--- /dev/null
+++ b/docs/pages/base-ui/react-progress/[docsTab]/index.js
@@ -0,0 +1,86 @@
+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/progress/progress.md?@mui/markdown';
+import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+import ProgressIndicatorApiJsonPageContent from '../../api/progress-indicator.json';
+import ProgressRootApiJsonPageContent from '../../api/progress-root.json';
+import ProgressTrackApiJsonPageContent from '../../api/progress-track.json';
+import useProgressIndicatorApiJsonPageContent from '../../api/use-progress-indicator.json';
+import useProgressRootApiJsonPageContent from '../../api/use-progress-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 ProgressIndicatorApiReq = require.context(
+ 'docs-base/translations/api-docs/progress-indicator',
+ false,
+ /\.\/progress-indicator.*.json$/,
+ );
+ const ProgressIndicatorApiDescriptions = mapApiPageTranslations(ProgressIndicatorApiReq);
+
+ const ProgressRootApiReq = require.context(
+ 'docs-base/translations/api-docs/progress-root',
+ false,
+ /\.\/progress-root.*.json$/,
+ );
+ const ProgressRootApiDescriptions = mapApiPageTranslations(ProgressRootApiReq);
+
+ const ProgressTrackApiReq = require.context(
+ 'docs-base/translations/api-docs/progress-track',
+ false,
+ /\.\/progress-track.*.json$/,
+ );
+ const ProgressTrackApiDescriptions = mapApiPageTranslations(ProgressTrackApiReq);
+
+ const useProgressIndicatorApiReq = require.context(
+ 'docs-base/translations/api-docs/use-progress-indicator',
+ false,
+ /\.\/use-progress-indicator.*.json$/,
+ );
+ const useProgressIndicatorApiDescriptions = mapApiPageTranslations(useProgressIndicatorApiReq);
+
+ const useProgressRootApiReq = require.context(
+ 'docs-base/translations/api-docs/use-progress-root',
+ false,
+ /\.\/use-progress-root.*.json$/,
+ );
+ const useProgressRootApiDescriptions = mapApiPageTranslations(useProgressRootApiReq);
+
+ return {
+ props: {
+ componentsApiDescriptions: {
+ ProgressIndicator: ProgressIndicatorApiDescriptions,
+ ProgressRoot: ProgressRootApiDescriptions,
+ ProgressTrack: ProgressTrackApiDescriptions,
+ },
+ componentsApiPageContents: {
+ ProgressIndicator: ProgressIndicatorApiJsonPageContent,
+ ProgressRoot: ProgressRootApiJsonPageContent,
+ ProgressTrack: ProgressTrackApiJsonPageContent,
+ },
+ hooksApiDescriptions: {
+ useProgressIndicator: useProgressIndicatorApiDescriptions,
+ useProgressRoot: useProgressRootApiDescriptions,
+ },
+ hooksApiPageContents: {
+ useProgressIndicator: useProgressIndicatorApiJsonPageContent,
+ useProgressRoot: useProgressRootApiJsonPageContent,
+ },
+ },
+ };
+};
diff --git a/docs/pages/base-ui/react-progress/index.js b/docs/pages/base-ui/react-progress/index.js
new file mode 100644
index 0000000000..ae0ef5d8da
--- /dev/null
+++ b/docs/pages/base-ui/react-progress/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/progress/progress.md?@mui/markdown';
+
+export default function Page(props) {
+ const { userLanguage, ...other } = props;
+ return ;
+}
+
+Page.getLayout = (page) => {
+ return {page};
+};
diff --git a/docs/pages/experiments/progress.tsx b/docs/pages/experiments/progress.tsx
new file mode 100644
index 0000000000..f98f217bb4
--- /dev/null
+++ b/docs/pages/experiments/progress.tsx
@@ -0,0 +1,241 @@
+import * as React from 'react';
+import { useTheme } from '@mui/system';
+import * as Progress from '@base_ui/react/Progress';
+
+const VAL1 = 33;
+
+const CUSTOM_BUFFER_VAL = 77;
+
+export default function ProgressDemos() {
+ return (
+
+
+
+
+
+
+
+
+
+ Indeterminate Progress
+
+
+
+
+
+
+
+
+ Progress (RTL)
+
+
+
+
+
+
+
+
+ Indeterminate (RTL)
+
+
+
+
+
+
+
Customizations
+
+
`${value}% complete, ${CUSTOM_BUFFER_VAL}% buffered`}
+ max={Math.min(100, CUSTOM_BUFFER_VAL)}
+ >
+
+ Custom Buffer Component
+
+
+
+
+
+
+
+
`${value}% complete, ${CUSTOM_BUFFER_VAL}% buffered`}
+ max={Math.min(100, CUSTOM_BUFFER_VAL)}
+ direction="rtl"
+ >
+
+ Custom Buffer Component (RTL)
+
+
+
+
+
+
+
+
+ );
+}
+
+function MyBuffer(props: any) {
+ const { value, style, ...rest } = props;
+ const percentageValue = valueToPercent(value, 0, 100);
+ return (
+
+ );
+}
+
+function valueToPercent(value: number | undefined, min: number, max: number) {
+ if (value === undefined) {
+ return value;
+ }
+
+ return ((value - min) * 100) / (max - min);
+}
+
+const grey = {
+ 50: '#F3F6F9',
+ 100: '#E5EAF2',
+ 200: '#DAE2ED',
+ 300: '#C7D0DD',
+ 400: '#B0B8C4',
+ 500: '#9DA8B7',
+ 600: '#6B7A90',
+ 700: '#434D5B',
+ 800: '#303740',
+ 900: '#1C2025',
+};
+
+const BLUE400 = '#3399FF';
+const BLUE500 = '#007FFF';
+
+function useIsDarkMode() {
+ const theme = useTheme();
+ return theme.palette.mode === 'dark';
+}
+
+export function Styles() {
+ const isDarkMode = useIsDarkMode();
+ return (
+
+ );
+}
diff --git a/docs/translations/api-docs/progress-indicator/progress-indicator.json b/docs/translations/api-docs/progress-indicator/progress-indicator.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/progress-indicator/progress-indicator.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/progress-root/progress-root.json b/docs/translations/api-docs/progress-root/progress-root.json
new file mode 100644
index 0000000000..cdac80157b
--- /dev/null
+++ b/docs/translations/api-docs/progress-root/progress-root.json
@@ -0,0 +1,31 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "aria-label": { "description": "The label for the Indicator component." },
+ "aria-labelledby": {
+ "description": "An id or space-separated list of ids of elements that label the Indicator component."
+ },
+ "aria-valuetext": {
+ "description": "A string value that provides a human-readable text alternative for the current value of the progress indicator."
+ },
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "direction": { "description": "The direction that progress bars fill in" },
+ "getAriaLabel": {
+ "description": "Accepts a function which returns a string value that provides an accessible name for the Indicator component",
+ "typeDescriptions": { "value": "The component's value" }
+ },
+ "getAriaValueText": {
+ "description": "Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator.",
+ "typeDescriptions": { "value": "The component's value to format" }
+ },
+ "max": { "description": "The maximum value" },
+ "min": { "description": "The minimum value" },
+ "render": { "description": "A function to customize rendering of the component." },
+ "value": {
+ "description": "The current value. The component is indeterminate when value is null
."
+ }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/progress-track/progress-track.json b/docs/translations/api-docs/progress-track/progress-track.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs/progress-track/progress-track.json
@@ -0,0 +1,10 @@
+{
+ "componentDescription": "",
+ "propDescriptions": {
+ "className": {
+ "description": "Class names applied to the element or a function that returns them based on the component's state."
+ },
+ "render": { "description": "A function to customize rendering of the component." }
+ },
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/use-progress-indicator/use-progress-indicator.json b/docs/translations/api-docs/use-progress-indicator/use-progress-indicator.json
new file mode 100644
index 0000000000..3c787fafc5
--- /dev/null
+++ b/docs/translations/api-docs/use-progress-indicator/use-progress-indicator.json
@@ -0,0 +1,12 @@
+{
+ "hookDescription": "",
+ "parametersDescriptions": {
+ "direction": { "description": "The direction that progress bars fill in" },
+ "max": { "description": "The maximum value" },
+ "min": { "description": "The minimum value" },
+ "value": {
+ "description": "The current value. The component is indeterminate when value is null
."
+ }
+ },
+ "returnValueDescriptions": {}
+}
diff --git a/docs/translations/api-docs/use-progress-root/use-progress-root.json b/docs/translations/api-docs/use-progress-root/use-progress-root.json
new file mode 100644
index 0000000000..a9fdd3927f
--- /dev/null
+++ b/docs/translations/api-docs/use-progress-root/use-progress-root.json
@@ -0,0 +1,30 @@
+{
+ "hookDescription": "",
+ "parametersDescriptions": {
+ "aria-label": { "description": "The label for the Indicator component." },
+ "aria-labelledby": {
+ "description": "An id or space-separated list of ids of elements that label the Indicator component."
+ },
+ "aria-valuetext": {
+ "description": "A string value that provides a human-readable text alternative for the current value of the progress indicator."
+ },
+ "direction": { "description": "The direction that progress bars fill in" },
+ "getAriaLabel": {
+ "description": "Accepts a function which returns a string value that provides an accessible name for the Indicator component"
+ },
+ "getAriaValueText": {
+ "description": "Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator."
+ },
+ "max": { "description": "The maximum value" },
+ "min": { "description": "The minimum value" },
+ "value": {
+ "description": "The current value. The component is indeterminate when value is null
."
+ }
+ },
+ "returnValueDescriptions": {
+ "direction": { "description": "The direction that progress bars fill in" },
+ "max": { "description": "The maximum value" },
+ "min": { "description": "The minimum value" },
+ "value": { "description": "Value of the component" }
+ }
+}
diff --git a/docs/translations/translations.json b/docs/translations/translations.json
index 7be5cd967b..c7b63b0824 100644
--- a/docs/translations/translations.json
+++ b/docs/translations/translations.json
@@ -230,6 +230,7 @@
"feedback": "Feedback",
"/base-ui/react-alert-dialog": "Alert Dialog",
"/base-ui/react-dialog": "Dialog",
+ "/base-ui/react-progress": "Progress",
"navigation": "Navigation",
"/base-ui/react-tabs": "Tabs",
"/base-ui/guides": "How-to guides",
diff --git a/packages/mui-base/src/Progress/Indicator/ProgressIndicator.test.tsx b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.test.tsx
new file mode 100644
index 0000000000..3fe97be71f
--- /dev/null
+++ b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.test.tsx
@@ -0,0 +1,77 @@
+import * as React from 'react';
+import { expect } from 'chai';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Progress from '@base_ui/react/Progress';
+import { ProgressContext } from '@base_ui/react/Progress';
+import { describeConformance } from '../../../test/describeConformance';
+import type { ProgressContextValue } from '../Root/ProgressRoot.types';
+
+const contextValue: ProgressContextValue = {
+ direction: 'ltr',
+ max: 100,
+ min: 0,
+ value: 30,
+ state: 'loading',
+ ownerState: {
+ direction: 'ltr',
+ max: 100,
+ min: 0,
+ },
+};
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ inheritComponent: 'span',
+ render: (node) => {
+ const { container, ...other } = render(
+ {node},
+ );
+
+ return { container, ...other };
+ },
+ refInstanceof: window.HTMLSpanElement,
+ }));
+
+ describe('internal styles', () => {
+ it('determinate', function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+
+ const { getByTestId } = render(
+
+
+
+
+ ,
+ );
+
+ const indicator = getByTestId('indicator');
+
+ expect(indicator).toHaveComputedStyle({
+ left: '0px',
+ width: '33%',
+ });
+ });
+
+ it('indeterminate', function test() {
+ if (/jsdom/.test(window.navigator.userAgent)) {
+ this.skip();
+ }
+
+ const { getByTestId } = render(
+
+
+
+
+ ,
+ );
+
+ const indicator = getByTestId('indicator');
+
+ expect(indicator).toHaveComputedStyle({});
+ });
+ });
+});
diff --git a/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx
new file mode 100644
index 0000000000..180072c34c
--- /dev/null
+++ b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.tsx
@@ -0,0 +1,57 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useProgressIndicator } from './useProgressIndicator';
+import { useProgressContext } from '../Root/ProgressContext';
+import { progressStyleHookMapping } from '../Root/styleHooks';
+import { ProgressIndicatorProps } from './ProgressIndicator.types';
+
+const ProgressIndicator = React.forwardRef(function ProgressIndicator(
+ props: ProgressIndicatorProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, ...otherProps } = props;
+
+ const { direction, max, min, value, ownerState } = useProgressContext();
+
+ const { getRootProps } = useProgressIndicator({
+ direction,
+ max,
+ min,
+ value,
+ });
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'span',
+ ownerState,
+ className,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: progressStyleHookMapping,
+ });
+
+ return renderElement();
+});
+
+ProgressIndicator.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]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { ProgressIndicator };
diff --git a/packages/mui-base/src/Progress/Indicator/ProgressIndicator.types.ts b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.types.ts
new file mode 100644
index 0000000000..dcdf95ae69
--- /dev/null
+++ b/packages/mui-base/src/Progress/Indicator/ProgressIndicator.types.ts
@@ -0,0 +1,33 @@
+import { BaseUIComponentProps } from '../../utils/types';
+import { ProgressDirection, ProgressRootOwnerState } from '../Root/ProgressRoot.types';
+
+export interface ProgressIndicatorProps
+ extends BaseUIComponentProps<'span', ProgressRootOwnerState> {}
+
+export interface UseProgressIndicatorParameters {
+ /**
+ * The direction that progress bars fill in
+ * @default 'ltr'
+ */
+ direction?: ProgressDirection;
+ /**
+ * The maximum value
+ * @default 100
+ */
+ max?: number;
+ /**
+ * The minimum value
+ * @default 0
+ */
+ min?: number;
+ /**
+ * The current value. The component is indeterminate when value is `null`.
+ */
+ value: number | null;
+}
+
+export interface UseProgressIndicatorReturnValue {
+ getRootProps: (
+ externalProps?: React.ComponentPropsWithRef<'span'>,
+ ) => React.ComponentPropsWithRef<'span'>;
+}
diff --git a/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts b/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts
new file mode 100644
index 0000000000..883501567a
--- /dev/null
+++ b/packages/mui-base/src/Progress/Indicator/useProgressIndicator.ts
@@ -0,0 +1,57 @@
+'use client';
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import {
+ UseProgressIndicatorParameters,
+ UseProgressIndicatorReturnValue,
+} from './ProgressIndicator.types';
+
+function valueToPercent(value: number, min: number, max: number) {
+ return ((value - min) * 100) / (max - min);
+}
+/**
+ *
+ * Demos:
+ *
+ * - [Progress](https://mui.com/base-ui/react-progress/#hooks)
+ *
+ * API:
+ *
+ * - [useProgressIndicator API](https://mui.com/base-ui/react-progress/hooks-api/#use-progress-indicator)
+ */
+function useProgressIndicator(
+ parameters: UseProgressIndicatorParameters,
+): UseProgressIndicatorReturnValue {
+ const { direction, max = 100, min = 0, value } = parameters;
+
+ const isRtl = direction === 'rtl';
+
+ const percentageValue =
+ Number.isFinite(value) && value !== null ? valueToPercent(value, min, max) : null;
+
+ const getStyles = React.useCallback(() => {
+ if (!percentageValue) {
+ return {};
+ }
+
+ return {
+ [isRtl ? 'right' : 'left']: 0,
+ height: 'inherit',
+ width: `${percentageValue}%`,
+ };
+ }, [isRtl, percentageValue]);
+
+ const getRootProps: UseProgressIndicatorReturnValue['getRootProps'] = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps<'span'>(externalProps, {
+ style: getStyles(),
+ }),
+ [getStyles],
+ );
+
+ return {
+ getRootProps,
+ };
+}
+
+export { useProgressIndicator };
diff --git a/packages/mui-base/src/Progress/Root/ProgressContext.tsx b/packages/mui-base/src/Progress/Root/ProgressContext.tsx
new file mode 100644
index 0000000000..cfdddc11a1
--- /dev/null
+++ b/packages/mui-base/src/Progress/Root/ProgressContext.tsx
@@ -0,0 +1,25 @@
+'use client';
+import * as React from 'react';
+import { ProgressContextValue } from './ProgressRoot.types';
+
+export interface ProgressProviderProps {
+ value: ProgressContextValue;
+ children: React.ReactNode;
+}
+
+/**
+ * @ignore - internal component.
+ */
+export const ProgressContext = React.createContext(undefined);
+
+if (process.env.NODE_ENV !== 'production') {
+ ProgressContext.displayName = 'ProgressContext';
+}
+
+export function useProgressContext() {
+ const context = React.useContext(ProgressContext);
+ if (context === undefined) {
+ throw new Error('useProgressContext must be used inside a Progress component');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/Progress/Root/ProgressRoot.test.tsx b/packages/mui-base/src/Progress/Root/ProgressRoot.test.tsx
new file mode 100644
index 0000000000..928b063f71
--- /dev/null
+++ b/packages/mui-base/src/Progress/Root/ProgressRoot.test.tsx
@@ -0,0 +1,64 @@
+import { expect } from 'chai';
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Progress from '@base_ui/react/Progress';
+import { describeConformance } from '../../../test/describeConformance';
+import type { ProgressRootProps } from './ProgressRoot.types';
+
+function TestProgress(props: ProgressRootProps) {
+ return (
+
+
+
+
+
+ );
+}
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ inheritComponent: 'div',
+ render,
+ refInstanceof: window.HTMLDivElement,
+ }));
+
+ it('renders a progressbar', () => {
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ expect(getByRole('progressbar')).to.have.attribute('aria-valuenow', '30');
+ });
+
+ describe('ARIA attributes', () => {
+ it('sets the correct aria attributes', () => {
+ const { getByRole } = render(
+
+
+
+
+ ,
+ );
+
+ const progressbar = getByRole('progressbar');
+
+ expect(progressbar).to.have.attribute('aria-valuenow', '30');
+ expect(progressbar).to.have.attribute('aria-valuemin', '0');
+ expect(progressbar).to.have.attribute('aria-valuemax', '100');
+ expect(progressbar).to.have.attribute('aria-valuetext', '30%');
+ });
+
+ it('should update aria-valuenow when value changes', () => {
+ const { getByRole, setProps } = render();
+ const progressbar = getByRole('progressbar');
+ setProps({ value: 77 });
+ expect(progressbar).to.have.attribute('aria-valuenow', '77');
+ });
+ });
+});
diff --git a/packages/mui-base/src/Progress/Root/ProgressRoot.tsx b/packages/mui-base/src/Progress/Root/ProgressRoot.tsx
new file mode 100644
index 0000000000..6e3787d3ba
--- /dev/null
+++ b/packages/mui-base/src/Progress/Root/ProgressRoot.tsx
@@ -0,0 +1,141 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useProgressRoot } from './useProgressRoot';
+import { ProgressContext } from './ProgressContext';
+import { progressStyleHookMapping } from './styleHooks';
+import {
+ ProgressContextValue,
+ ProgressRootOwnerState,
+ ProgressRootProps,
+} from './ProgressRoot.types';
+
+const ProgressRoot = React.forwardRef(function ProgressRoot(
+ props: ProgressRootProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const {
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledby,
+ 'aria-valuetext': ariaValuetext,
+ direction = 'ltr',
+ getAriaLabel,
+ getAriaValueText,
+ max = 100,
+ min = 0,
+ value,
+ render,
+ className,
+ ...otherProps
+ } = props;
+
+ const { getRootProps, ...progress } = useProgressRoot({
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledby,
+ 'aria-valuetext': ariaValuetext,
+ direction,
+ getAriaLabel,
+ getAriaValueText,
+ max,
+ min,
+ value,
+ });
+
+ const ownerState: ProgressRootOwnerState = React.useMemo(
+ () => ({
+ direction,
+ max,
+ min,
+ state: progress.state,
+ }),
+ [direction, max, min, progress.state],
+ );
+
+ const contextValue: ProgressContextValue = React.useMemo(
+ () => ({
+ ...progress,
+ ownerState,
+ }),
+ [progress, ownerState],
+ );
+
+ const { renderElement } = useComponentRenderer({
+ propGetter: getRootProps,
+ render: render ?? 'div',
+ ownerState,
+ className,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: progressStyleHookMapping,
+ });
+
+ return (
+ {renderElement()}
+ );
+});
+
+ProgressRoot.propTypes /* remove-proptypes */ = {
+ // ┌────────────────────────────── Warning ──────────────────────────────┐
+ // │ These PropTypes are generated from the TypeScript type definitions. │
+ // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │
+ // └─────────────────────────────────────────────────────────────────────┘
+ /**
+ * The label for the Indicator component.
+ */
+ 'aria-label': PropTypes.string,
+ /**
+ * An id or space-separated list of ids of elements that label the Indicator component.
+ */
+ 'aria-labelledby': PropTypes.string,
+ /**
+ * A string value that provides a human-readable text alternative for the current value of the progress indicator.
+ */
+ 'aria-valuetext': 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 direction that progress bars fill in
+ * @default 'ltr'
+ */
+ direction: PropTypes.oneOf(['ltr', 'rtl']),
+ /**
+ * Accepts a function which returns a string value that provides an accessible name for the Indicator component
+ * @param {number | null} value The component's value
+ * @returns {string}
+ */
+ getAriaLabel: PropTypes.func,
+ /**
+ * Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator.
+ * @param {number | null} value The component's value to format
+ * @returns {string}
+ */
+ getAriaValueText: PropTypes.func,
+ /**
+ * The maximum value
+ * @default 100
+ */
+ max: PropTypes.number,
+ /**
+ * The minimum value
+ * @default 0
+ */
+ min: PropTypes.number,
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+ /**
+ * The current value. The component is indeterminate when value is `null`.
+ * @default null
+ */
+ value: PropTypes.number,
+} as any;
+
+export { ProgressRoot };
diff --git a/packages/mui-base/src/Progress/Root/ProgressRoot.types.ts b/packages/mui-base/src/Progress/Root/ProgressRoot.types.ts
new file mode 100644
index 0000000000..5dec7d0f1a
--- /dev/null
+++ b/packages/mui-base/src/Progress/Root/ProgressRoot.types.ts
@@ -0,0 +1,89 @@
+import { BaseUIComponentProps } from '../../utils/types';
+
+export type ProgressContextValue = Omit & {
+ ownerState: ProgressRootOwnerState;
+};
+
+export type ProgressRootOwnerState = {
+ direction: ProgressDirection;
+ max: number;
+ min: number;
+};
+
+export interface ProgressRootProps
+ extends UseProgressRootParameters,
+ BaseUIComponentProps<'div', ProgressRootOwnerState> {}
+
+export type ProgressDirection = 'ltr' | 'rtl';
+
+export type ProgressStatus = 'indeterminate' | 'loading' | 'complete';
+
+export interface UseProgressRootParameters {
+ /**
+ * The label for the Indicator component.
+ */
+ 'aria-label'?: string;
+ /**
+ * An id or space-separated list of ids of elements that label the Indicator component.
+ */
+ 'aria-labelledby'?: string;
+ /**
+ * A string value that provides a human-readable text alternative for the current value of the progress indicator.
+ */
+ 'aria-valuetext'?: string;
+ /**
+ * The direction that progress bars fill in
+ * @default 'ltr'
+ */
+ direction?: ProgressDirection;
+ /**
+ * Accepts a function which returns a string value that provides an accessible name for the Indicator component
+ * @param {number | null} value The component's value
+ * @returns {string}
+ */
+ getAriaLabel?: (index: number | null) => string;
+ /**
+ * Accepts a function which returns a string value that provides a human-readable text alternative for the current value of the progress indicator.
+ * @param {number | null} value The component's value to format
+ * @returns {string}
+ */
+ getAriaValueText?: (value: number | null) => string;
+ /**
+ * The maximum value
+ * @default 100
+ */
+ max?: number;
+ /**
+ * The minimum value
+ * @default 0
+ */
+ min?: number;
+ /**
+ * The current value. The component is indeterminate when value is `null`.
+ * @default null
+ */
+ value: number | null;
+}
+
+export interface UseProgressRootReturnValue {
+ getRootProps: (
+ externalProps?: React.ComponentPropsWithRef<'div'>,
+ ) => React.ComponentPropsWithRef<'div'>;
+ /**
+ * The direction that progress bars fill in
+ */
+ direction: ProgressDirection;
+ /**
+ * The maximum value
+ */
+ max: number;
+ /**
+ * The minimum value
+ */
+ min: number;
+ /**
+ * Value of the component
+ */
+ value: number | null;
+ state: ProgressStatus;
+}
diff --git a/packages/mui-base/src/Progress/Root/styleHooks.ts b/packages/mui-base/src/Progress/Root/styleHooks.ts
new file mode 100644
index 0000000000..808c29c561
--- /dev/null
+++ b/packages/mui-base/src/Progress/Root/styleHooks.ts
@@ -0,0 +1,8 @@
+import type { CustomStyleHookMapping } from '../../utils/getStyleHookProps';
+import type { ProgressRootOwnerState } from './ProgressRoot.types';
+
+export const progressStyleHookMapping: CustomStyleHookMapping = {
+ direction: () => null,
+ max: () => null,
+ min: () => null,
+};
diff --git a/packages/mui-base/src/Progress/Root/useProgressRoot.ts b/packages/mui-base/src/Progress/Root/useProgressRoot.ts
new file mode 100644
index 0000000000..c8d612ecab
--- /dev/null
+++ b/packages/mui-base/src/Progress/Root/useProgressRoot.ts
@@ -0,0 +1,81 @@
+'use client';
+import * as React from 'react';
+import { mergeReactProps } from '../../utils/mergeReactProps';
+import {
+ ProgressStatus,
+ UseProgressRootParameters,
+ UseProgressRootReturnValue,
+} from './ProgressRoot.types';
+
+function getDefaultAriaValueText(value: number | null) {
+ if (value === null) {
+ return 'indeterminate progress';
+ }
+
+ return `${value}%`;
+} /**
+ *
+ * Demos:
+ *
+ * - [Progress](https://mui.com/base-ui/react-progress/#hooks)
+ *
+ * API:
+ *
+ * - [useProgressRoot API](https://mui.com/base-ui/react-progress/hooks-api/#use-progress-root)
+ */
+function useProgressRoot(parameters: UseProgressRootParameters): UseProgressRootReturnValue {
+ const {
+ 'aria-label': ariaLabel,
+ 'aria-labelledby': ariaLabelledby,
+ 'aria-valuetext': ariaValuetext,
+ direction = 'ltr',
+ getAriaLabel,
+ getAriaValueText,
+ max = 100,
+ min = 0,
+ value,
+ } = parameters;
+
+ let state: ProgressStatus = 'indeterminate';
+ if (Number.isFinite(value)) {
+ state = value === max ? 'complete' : 'loading';
+ }
+
+ const getRootProps: UseProgressRootReturnValue['getRootProps'] = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps<'div'>(externalProps, {
+ 'aria-label': getAriaLabel ? getAriaLabel(value) : ariaLabel,
+ 'aria-labelledby': ariaLabelledby,
+ 'aria-valuemax': max,
+ 'aria-valuemin': min,
+ 'aria-valuenow': value ?? undefined,
+ 'aria-valuetext': getAriaValueText
+ ? getAriaValueText(value)
+ : ariaValuetext ?? getDefaultAriaValueText(value),
+ dir: direction,
+ role: 'progressbar',
+ }),
+ [
+ ariaLabel,
+ ariaLabelledby,
+ ariaValuetext,
+ direction,
+ getAriaLabel,
+ getAriaValueText,
+ max,
+ min,
+ value,
+ ],
+ );
+
+ return {
+ getRootProps,
+ direction,
+ max,
+ min,
+ value,
+ state,
+ };
+}
+
+export { useProgressRoot };
diff --git a/packages/mui-base/src/Progress/Track/ProgressTrack.test.tsx b/packages/mui-base/src/Progress/Track/ProgressTrack.test.tsx
new file mode 100644
index 0000000000..0fb5ec9415
--- /dev/null
+++ b/packages/mui-base/src/Progress/Track/ProgressTrack.test.tsx
@@ -0,0 +1,35 @@
+import * as React from 'react';
+import { createRenderer } from '@mui/internal-test-utils';
+import * as Progress from '@base_ui/react/Progress';
+import { ProgressContext } from '@base_ui/react/Progress';
+import { describeConformance } from '../../../test/describeConformance';
+import type { ProgressContextValue } from '../Root/ProgressRoot.types';
+
+const contextValue: ProgressContextValue = {
+ direction: 'ltr',
+ max: 100,
+ min: 0,
+ value: 30,
+ state: 'loading',
+ ownerState: {
+ direction: 'ltr',
+ max: 100,
+ min: 0,
+ },
+};
+
+describe('', () => {
+ const { render } = createRenderer();
+
+ describeConformance(, () => ({
+ inheritComponent: 'span',
+ render: (node) => {
+ const { container, ...other } = render(
+ {node},
+ );
+
+ return { container, ...other };
+ },
+ refInstanceof: window.HTMLSpanElement,
+ }));
+});
diff --git a/packages/mui-base/src/Progress/Track/ProgressTrack.tsx b/packages/mui-base/src/Progress/Track/ProgressTrack.tsx
new file mode 100644
index 0000000000..c18036eae8
--- /dev/null
+++ b/packages/mui-base/src/Progress/Track/ProgressTrack.tsx
@@ -0,0 +1,48 @@
+'use client';
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { useComponentRenderer } from '../../utils/useComponentRenderer';
+import { useProgressContext } from '../Root/ProgressContext';
+import { progressStyleHookMapping } from '../Root/styleHooks';
+import { ProgressTrackProps } from './ProgressTrack.types';
+
+const ProgressTrack = React.forwardRef(function ProgressTrack(
+ props: ProgressTrackProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { render, className, ...otherProps } = props;
+
+ const { ownerState } = useProgressContext();
+
+ const { renderElement } = useComponentRenderer({
+ render: render ?? 'span',
+ ownerState,
+ className,
+ ref: forwardedRef,
+ extraProps: otherProps,
+ customStyleHookMapping: progressStyleHookMapping,
+ });
+
+ return renderElement();
+});
+
+ProgressTrack.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]),
+ /**
+ * A function to customize rendering of the component.
+ */
+ render: PropTypes.oneOfType([PropTypes.element, PropTypes.func]),
+} as any;
+
+export { ProgressTrack };
diff --git a/packages/mui-base/src/Progress/Track/ProgressTrack.types.ts b/packages/mui-base/src/Progress/Track/ProgressTrack.types.ts
new file mode 100644
index 0000000000..3700c4dfbe
--- /dev/null
+++ b/packages/mui-base/src/Progress/Track/ProgressTrack.types.ts
@@ -0,0 +1,4 @@
+import { BaseUIComponentProps } from '../../utils/types';
+import { ProgressRootOwnerState } from '../Root/ProgressRoot.types';
+
+export interface ProgressTrackProps extends BaseUIComponentProps<'span', ProgressRootOwnerState> {}
diff --git a/packages/mui-base/src/Progress/index.barrel.ts b/packages/mui-base/src/Progress/index.barrel.ts
new file mode 100644
index 0000000000..a22c330c34
--- /dev/null
+++ b/packages/mui-base/src/Progress/index.barrel.ts
@@ -0,0 +1,15 @@
+export { ProgressRoot } from './Root/ProgressRoot';
+export type * from './Root/ProgressRoot.types';
+export { useProgressRoot } from './Root/useProgressRoot';
+export * from './Root/ProgressContext';
+
+export { ProgressTrack } from './Track/ProgressTrack';
+export type { ProgressTrackProps } from './Track/ProgressTrack.types';
+
+export { ProgressIndicator } from './Indicator/ProgressIndicator';
+export type {
+ ProgressIndicatorProps,
+ UseProgressIndicatorParameters,
+ UseProgressIndicatorReturnValue,
+} from './Indicator/ProgressIndicator.types';
+export { useProgressIndicator } from './Indicator/useProgressIndicator';
diff --git a/packages/mui-base/src/Progress/index.ts b/packages/mui-base/src/Progress/index.ts
new file mode 100644
index 0000000000..b0997635a6
--- /dev/null
+++ b/packages/mui-base/src/Progress/index.ts
@@ -0,0 +1,21 @@
+export { ProgressRoot as Root } from './Root/ProgressRoot';
+export {
+ ProgressRootOwnerState as ProgressOwnerState,
+ ProgressRootProps as RootProps,
+ UseProgressRootParameters,
+ UseProgressRootReturnValue,
+ ProgressContextValue,
+} from './Root/ProgressRoot.types';
+export { useProgressRoot } from './Root/useProgressRoot';
+export * from './Root/ProgressContext';
+
+export { ProgressTrack as Track } from './Track/ProgressTrack';
+export type { ProgressTrackProps as TrackProps } from './Track/ProgressTrack.types';
+
+export { ProgressIndicator as Indicator } from './Indicator/ProgressIndicator';
+export type {
+ ProgressIndicatorProps as IndicatorProps,
+ UseProgressIndicatorParameters,
+ UseProgressIndicatorReturnValue,
+} from './Indicator/ProgressIndicator.types';
+export { useProgressIndicator } from './Indicator/useProgressIndicator';
diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts
index 9a78cfba56..7f29893f01 100644
--- a/packages/mui-base/src/index.ts
+++ b/packages/mui-base/src/index.ts
@@ -3,6 +3,7 @@ export * from './Checkbox/index.barrel';
export * from './Dialog/index.barrel';
export * from './NumberField/index.barrel';
export * from './Popover/index.barrel';
+export * from './Progress/index.barrel';
export * from './Slider/index.barrel';
export * from './Switch/index.barrel';
export * from './Tabs/index.barrel';