diff --git a/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.js b/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.js
new file mode 100644
index 0000000000..d1d77de65a
--- /dev/null
+++ b/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.js
@@ -0,0 +1,49 @@
+import * as React from 'react';
+import { Tooltip } from '@mui/base/Tooltip';
+import { styled } from '@mui/system';
+
+export default function UnstyledTooltipIntroduction() {
+ return (
+
+
+ Anchor
+
+ Tooltip
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+export const TooltipContent = styled(Tooltip.Content)`
+ ${({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? 'white' : '#333'};
+ color: ${theme.palette.mode === 'dark' ? 'black' : 'white'};
+ padding: 4px 6px;
+ border-radius: 4px;
+ font-size: 95%;
+ `}
+`;
+
+export const AnchorButton = styled('button')`
+ border: none;
+ background: ${blue[600]};
+ color: white;
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-size: 16px;
+
+ &:focus-visible {
+ outline: 2px solid ${blue[400]};
+ outline-offset: 2px;
+ }
+
+ &:hover,
+ &[data-state='open'] {
+ background: ${blue[800]};
+ }
+`;
diff --git a/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.tsx b/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.tsx
new file mode 100644
index 0000000000..d1d77de65a
--- /dev/null
+++ b/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.tsx
@@ -0,0 +1,49 @@
+import * as React from 'react';
+import { Tooltip } from '@mui/base/Tooltip';
+import { styled } from '@mui/system';
+
+export default function UnstyledTooltipIntroduction() {
+ return (
+
+
+ Anchor
+
+ Tooltip
+
+ );
+}
+
+const blue = {
+ 400: '#3399FF',
+ 600: '#0072E6',
+ 800: '#004C99',
+};
+
+export const TooltipContent = styled(Tooltip.Content)`
+ ${({ theme }) => `
+ background: ${theme.palette.mode === 'dark' ? 'white' : '#333'};
+ color: ${theme.palette.mode === 'dark' ? 'black' : 'white'};
+ padding: 4px 6px;
+ border-radius: 4px;
+ font-size: 95%;
+ `}
+`;
+
+export const AnchorButton = styled('button')`
+ border: none;
+ background: ${blue[600]};
+ color: white;
+ padding: 8px 16px;
+ border-radius: 4px;
+ font-size: 16px;
+
+ &:focus-visible {
+ outline: 2px solid ${blue[400]};
+ outline-offset: 2px;
+ }
+
+ &:hover,
+ &[data-state='open'] {
+ background: ${blue[800]};
+ }
+`;
diff --git a/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.tsx.preview b/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.tsx.preview
new file mode 100644
index 0000000000..021f9bad1b
--- /dev/null
+++ b/docs/data/base/components/tooltip/UnstyledTooltipIntroduction/system/index.tsx.preview
@@ -0,0 +1,6 @@
+
+
+ Anchor
+
+ Tooltip
+
\ No newline at end of file
diff --git a/docs/data/base/components/tooltip/tooltip.md b/docs/data/base/components/tooltip/tooltip.md
index 02ea58050d..1dd2f723f4 100644
--- a/docs/data/base/components/tooltip/tooltip.md
+++ b/docs/data/base/components/tooltip/tooltip.md
@@ -1,14 +1,136 @@
---
productId: base-ui
title: React Tooltip component
+components: Tooltip, TooltipContent, TooltipAnchorFragment, TooltipDelayGroup
githubLabel: 'component: tooltip'
waiAria: https://www.w3.org/WAI/ARIA/apg/patterns/tooltip/
---
-# Tooltip 🚧
+# Tooltip
Tooltips display informative text when users hover over, focus on, or tap an element.
-:::warning
-The Base UI Tooltip component isn't available yet, but you can upvote [this GitHub issue](https://github.com/mui/base-ui/issues/32) to see it arrive sooner.
+{{"component": "modules/components/ComponentLinkHeader.js", "design": false}}
+
+{{"component": "modules/components/ComponentPageTabs.js"}}
+
+## Introduction
+
+{{"demo": "UnstyledTooltipIntroduction", "defaultCodeOpen": false, "bg": "gradient"}}
+
+## Component
+
+```jsx
+import { Tooltip } from '@mui/base/Tooltip';
+```
+
+### Anatomy
+
+The `Tooltip` component is composed of a root component that contains two main subcomponents: `AnchorFragment` and `Content`.
+
+```jsx
+
+
+
+
+
+
+```
+
+- `Content` is the tooltip element itself.
+- `AnchorFragment` provides props for its child element that lets the `Content` element be anchored to it.
+- `Arrow` is an optional element for displaying a caret (or triangle) that points to the center of the anchor.
+
+```jsx
+
+
+ Anchor
+
+ Tooltip
+
+```
+
+### Placement
+
+By default, the tooltip is placed on the top side of its anchor. To change this, use the `placement` prop:
+
+```jsx
+
+
+ Anchor
+
+ Tooltip
+
+```
+
+There are 12 possible placements:
+
+- Centered placements: `top`, `right`, `bottom`, `left`.
+- Edge-aligned placements: `top-start`, `top-end`, `right-start`, `right-end`, `bottom-start`, `bottom-end`, `left-start`, `left-end`.
+
+The edge-aligned placements are logical, adapting to the writing direction (LTR or RTL) as expected.
+
+### Anchor gap
+
+To increase the gap between the anchor and its tooltip, use the `anchorGap` prop:
+
+```jsx
+
+```
+
+### Delay
+
+To delay the tooltip from showing or hiding, use the `delay` prop, which represents how long the tooltip will wait after being triggered to show in milliseconds:
+
+```jsx
+
+```
+
+The open and close delay can be separately configured:
+
+```jsx
+
+```
+
+### Open state
+
+To control the tooltip with external state, use the `open` and `onOpenChange` props:
+
+```jsx
+function App() {
+ const [open, setOpen] = React.useState(false);
+ return (
+
+ {/* Subcomponents */}
+
+ );
+}
+```
+
+### Default open
+
+To show the tooltip initially while leaving it uncontrolled, use the `defaultOpen` prop:
+
+```jsx
+
+```
+
+### Hoverable content
+
+To prevent the content inside from being hoverable, use the `disableHoverableContent` prop:
+
+```jsx
+
+```
+
+:::info
+This should only be disabled when necessary, such as in high-density UIs where tooltips can block other controls.
:::
+
+### Touch input
+
+To prevent the tooltip from showing when using touch input, use the `allowTouch` prop:
+
+```jsx
+
+```
diff --git a/docs/data/base/pagesApi.js b/docs/data/base/pagesApi.js
index 58829d7f65..5d74110dec 100644
--- a/docs/data/base/pagesApi.js
+++ b/docs/data/base/pagesApi.js
@@ -75,6 +75,19 @@ module.exports = [
pathname: '/base-ui/react-textarea-autosize/components-api/#textarea-autosize',
title: 'TextareaAutosize',
},
+ { pathname: '/base-ui/react-tooltip/components-api/#tooltip', title: 'Tooltip' },
+ {
+ pathname: '/base-ui/react-tooltip/components-api/#tooltip-anchor-fragment',
+ title: 'TooltipAnchorFragment',
+ },
+ {
+ pathname: '/base-ui/react-tooltip/components-api/#tooltip-content',
+ title: 'TooltipContent',
+ },
+ {
+ pathname: '/base-ui/react-tooltip/components-api/#tooltip-delay-group',
+ title: 'TooltipDelayGroup',
+ },
{
pathname: '/base-ui/react-autocomplete/hooks-api/#use-autocomplete',
title: 'useAutocomplete',
diff --git a/docs/pages/base-ui/api/tooltip-anchor-fragment.json b/docs/pages/base-ui/api/tooltip-anchor-fragment.json
new file mode 100644
index 0000000000..615306750a
--- /dev/null
+++ b/docs/pages/base-ui/api/tooltip-anchor-fragment.json
@@ -0,0 +1,14 @@
+{
+ "props": {},
+ "name": "TooltipAnchorFragment",
+ "imports": [
+ "import { TooltipAnchorFragment } from '@mui/base/Tooltip';",
+ "import { TooltipAnchorFragment } from '@mui/base';"
+ ],
+ "classes": [],
+ "muiName": "TooltipAnchorFragment",
+ "filename": "/packages/mui-base/src/Tooltip/TooltipAnchorFragment.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/tooltip-content.json b/docs/pages/base-ui/api/tooltip-content.json
new file mode 100644
index 0000000000..1e4120c2d6
--- /dev/null
+++ b/docs/pages/base-ui/api/tooltip-content.json
@@ -0,0 +1,17 @@
+{
+ "props": {
+ "className": { "type": { "name": "union", "description": "func | string" } },
+ "render": { "type": { "name": "func" } }
+ },
+ "name": "TooltipContent",
+ "imports": [
+ "import { TooltipContent } from '@mui/base/Tooltip';",
+ "import { TooltipContent } from '@mui/base';"
+ ],
+ "classes": [],
+ "muiName": "TooltipContent",
+ "filename": "/packages/mui-base/src/Tooltip/TooltipContent.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/tooltip-delay-group.json b/docs/pages/base-ui/api/tooltip-delay-group.json
new file mode 100644
index 0000000000..3622039e75
--- /dev/null
+++ b/docs/pages/base-ui/api/tooltip-delay-group.json
@@ -0,0 +1,14 @@
+{
+ "props": {},
+ "name": "TooltipDelayGroup",
+ "imports": [
+ "import { TooltipDelayGroup } from '@mui/base/Tooltip';",
+ "import { TooltipDelayGroup } from '@mui/base';"
+ ],
+ "classes": [],
+ "muiName": "TooltipDelayGroup",
+ "filename": "/packages/mui-base/src/Tooltip/TooltipDelayGroup.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/tooltip.json b/docs/pages/base-ui/api/tooltip.json
new file mode 100644
index 0000000000..b4d8a19d37
--- /dev/null
+++ b/docs/pages/base-ui/api/tooltip.json
@@ -0,0 +1,16 @@
+{
+ "props": {},
+ "name": "Tooltip",
+ "imports": [
+ "import { Tooltip } from '@mui/base/Tooltip';",
+ "import { Tooltip } from '@mui/base';"
+ ],
+ "classes": [],
+ "spread": true,
+ "themeDefaultProps": true,
+ "muiName": "Tooltip",
+ "filename": "/packages/mui-base/src/Tooltip/Tooltip.tsx",
+ "inheritance": null,
+ "demos": "",
+ "cssComponent": false
+}
diff --git a/docs/pages/base-ui/api/use-tooltip.json b/docs/pages/base-ui/api/use-tooltip.json
new file mode 100644
index 0000000000..73e3dcc759
--- /dev/null
+++ b/docs/pages/base-ui/api/use-tooltip.json
@@ -0,0 +1,60 @@
+{
+ "parameters": {
+ "alignmentOffset": { "type": { "name": "number", "description": "number" }, "default": "0" },
+ "allowTouch": { "type": { "name": "boolean", "description": "boolean" }, "default": "true" },
+ "anchorGap": { "type": { "name": "number", "description": "number" }, "default": "0" },
+ "defaultOpen": { "type": { "name": "boolean", "description": "boolean" } },
+ "delay": {
+ "type": {
+ "name": "number | { open?: number; close?: number }",
+ "description": "number | { open?: number; close?: number }"
+ },
+ "default": "0"
+ },
+ "disableHoverableContent": {
+ "type": { "name": "boolean", "description": "boolean" },
+ "default": "false"
+ },
+ "onOpenChange": {
+ "type": {
+ "name": "(isOpen: boolean, event: Event) => void",
+ "description": "(isOpen: boolean, event: Event) => void"
+ }
+ },
+ "open": { "type": { "name": "boolean", "description": "boolean" } },
+ "placement": {
+ "type": { "name": "Placement", "description": "Placement" },
+ "default": "'top'"
+ },
+ "type": {
+ "type": {
+ "name": "'label' | 'description' | 'visual-only'",
+ "description": "'label' | 'description' | 'visual-only'"
+ },
+ "default": "'description'"
+ }
+ },
+ "returnValue": {
+ "getAnchorProps": {
+ "type": {
+ "name": "(externalProps?: React.HTMLAttributes<HTMLElement>) => React.HTMLAttributes<HTMLElement>",
+ "description": "(externalProps?: React.HTMLAttributes<HTMLElement>) => React.HTMLAttributes<HTMLElement>"
+ },
+ "required": true
+ },
+ "getTooltipProps": {
+ "type": {
+ "name": "(externalProps?: React.HTMLAttributes<HTMLElement>) => React.HTMLAttributes<HTMLElement>",
+ "description": "(externalProps?: React.HTMLAttributes<HTMLElement>) => React.HTMLAttributes<HTMLElement>"
+ },
+ "required": true
+ }
+ },
+ "name": "useTooltip",
+ "filename": "/packages/mui-base/src/useTooltip/useTooltip.ts",
+ "imports": [
+ "import { useTooltip } from '@mui/base/useTooltip';",
+ "import { useTooltip } from '@mui/base';"
+ ],
+ "demos": ""
+}
diff --git a/docs/pages/base-ui/react-tooltip/[docsTab]/index.js b/docs/pages/base-ui/react-tooltip/[docsTab]/index.js
new file mode 100644
index 0000000000..49957f29fd
--- /dev/null
+++ b/docs/pages/base-ui/react-tooltip/[docsTab]/index.js
@@ -0,0 +1,74 @@
+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/tooltip/tooltip.md?@mui/markdown';
+import mapApiPageTranslations from 'docs/src/modules/utils/mapApiPageTranslations';
+import TooltipApiJsonPageContent from '../../api/tooltip.json';
+import TooltipAnchorFragmentApiJsonPageContent from '../../api/tooltip-anchor-fragment.json';
+import TooltipContentApiJsonPageContent from '../../api/tooltip-content.json';
+import TooltipDelayGroupApiJsonPageContent from '../../api/tooltip-delay-group.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 TooltipApiReq = require.context(
+ 'docs/translations/api-docs-base/tooltip',
+ false,
+ /tooltip.*.json$/,
+ );
+ const TooltipApiDescriptions = mapApiPageTranslations(TooltipApiReq);
+
+ const TooltipAnchorFragmentApiReq = require.context(
+ 'docs/translations/api-docs-base/tooltip-anchor-fragment',
+ false,
+ /tooltip-anchor-fragment.*.json$/,
+ );
+ const TooltipAnchorFragmentApiDescriptions = mapApiPageTranslations(TooltipAnchorFragmentApiReq);
+
+ const TooltipContentApiReq = require.context(
+ 'docs/translations/api-docs-base/tooltip-content',
+ false,
+ /tooltip-content.*.json$/,
+ );
+ const TooltipContentApiDescriptions = mapApiPageTranslations(TooltipContentApiReq);
+
+ const TooltipDelayGroupApiReq = require.context(
+ 'docs/translations/api-docs-base/tooltip-delay-group',
+ false,
+ /tooltip-delay-group.*.json$/,
+ );
+ const TooltipDelayGroupApiDescriptions = mapApiPageTranslations(TooltipDelayGroupApiReq);
+
+ return {
+ props: {
+ componentsApiDescriptions: {
+ Tooltip: TooltipApiDescriptions,
+ TooltipAnchorFragment: TooltipAnchorFragmentApiDescriptions,
+ TooltipContent: TooltipContentApiDescriptions,
+ TooltipDelayGroup: TooltipDelayGroupApiDescriptions,
+ },
+ componentsApiPageContents: {
+ Tooltip: TooltipApiJsonPageContent,
+ TooltipAnchorFragment: TooltipAnchorFragmentApiJsonPageContent,
+ TooltipContent: TooltipContentApiJsonPageContent,
+ TooltipDelayGroup: TooltipDelayGroupApiJsonPageContent,
+ },
+ hooksApiDescriptions: {},
+ hooksApiPageContents: {},
+ },
+ };
+};
diff --git a/docs/translations/api-docs-base/tooltip-anchor-fragment/tooltip-anchor-fragment.json b/docs/translations/api-docs-base/tooltip-anchor-fragment/tooltip-anchor-fragment.json
new file mode 100644
index 0000000000..f93d4cbd8c
--- /dev/null
+++ b/docs/translations/api-docs-base/tooltip-anchor-fragment/tooltip-anchor-fragment.json
@@ -0,0 +1 @@
+{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} }
diff --git a/docs/translations/api-docs-base/tooltip-content/tooltip-content.json b/docs/translations/api-docs-base/tooltip-content/tooltip-content.json
new file mode 100644
index 0000000000..4bc12cf1e0
--- /dev/null
+++ b/docs/translations/api-docs-base/tooltip-content/tooltip-content.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-base/tooltip-delay-group/tooltip-delay-group.json b/docs/translations/api-docs-base/tooltip-delay-group/tooltip-delay-group.json
new file mode 100644
index 0000000000..f93d4cbd8c
--- /dev/null
+++ b/docs/translations/api-docs-base/tooltip-delay-group/tooltip-delay-group.json
@@ -0,0 +1 @@
+{ "componentDescription": "", "propDescriptions": {}, "classDescriptions": {} }
diff --git a/docs/translations/api-docs-base/tooltip/tooltip.json b/docs/translations/api-docs-base/tooltip/tooltip.json
new file mode 100644
index 0000000000..1a2a7ae684
--- /dev/null
+++ b/docs/translations/api-docs-base/tooltip/tooltip.json
@@ -0,0 +1,5 @@
+{
+ "componentDescription": "The foundation for building custom-styled tooltips.",
+ "propDescriptions": {},
+ "classDescriptions": {}
+}
diff --git a/docs/translations/api-docs/use-tooltip/use-tooltip.json b/docs/translations/api-docs/use-tooltip/use-tooltip.json
new file mode 100644
index 0000000000..5b39475019
--- /dev/null
+++ b/docs/translations/api-docs/use-tooltip/use-tooltip.json
@@ -0,0 +1,33 @@
+{
+ "hookDescription": "The basic building block for creating custom tooltips.",
+ "parametersDescriptions": {
+ "alignmentOffset": {
+ "description": "The offset of the tooltip element along its alignment axis."
+ },
+ "allowTouch": {
+ "description": "If true
, the tooltip will be shown when using touch input."
+ },
+ "anchorGap": { "description": "The gap between the anchor element and the tooltip element." },
+ "defaultOpen": {
+ "description": "If true
, the tooltip will be open by default. Use when uncontrolled."
+ },
+ "delay": {
+ "description": "The delay in milliseconds before the tooltip opens and closes after the anchor element is triggered."
+ },
+ "disableHoverableContent": {
+ "description": "If true
, the tooltip content will not be hoverable."
+ },
+ "onOpenChange": {
+ "description": "Callback fired when the tooltip is requested to be opened or closed."
+ },
+ "open": {
+ "description": "If true
, the tooltip will be open. Use when controlled."
+ },
+ "placement": { "description": "The placement of the tooltip element." },
+ "type": { "description": "The type of the tooltip." }
+ },
+ "returnValueDescriptions": {
+ "getAnchorProps": { "description": "Props to spread on the anchor element." },
+ "getTooltipProps": { "description": "Props to spread on the tooltip element." }
+ }
+}
diff --git a/packages/mui-base/package.json b/packages/mui-base/package.json
index 5d70f5df16..cba7feff29 100644
--- a/packages/mui-base/package.json
+++ b/packages/mui-base/package.json
@@ -40,6 +40,7 @@
},
"dependencies": {
"@babel/runtime": "^7.24.1",
+ "@floating-ui/react": "^0.26.10",
"@floating-ui/react-dom": "^2.0.8",
"@mui/types": "^7.2.14",
"@mui/utils": "^5.15.14",
diff --git a/packages/mui-base/src/Tooltip/Tooltip.test.tsx b/packages/mui-base/src/Tooltip/Tooltip.test.tsx
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/packages/mui-base/src/Tooltip/Tooltip.tsx b/packages/mui-base/src/Tooltip/Tooltip.tsx
new file mode 100644
index 0000000000..ee548d8b56
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/Tooltip.tsx
@@ -0,0 +1,50 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type { TooltipOwnerState, TooltipProps } from './Tooltip.types';
+import { TooltipContext } from './TooltipContext';
+import { useTooltip } from '../useTooltip';
+
+/**
+ * The foundation for building custom-styled tooltips.
+ *
+ * Demos:
+ *
+ * - [Tooltip](https://mui.com/base-ui/react-tooltip/)
+ *
+ * API:
+ *
+ * - [Tooltip API](https://mui.com/base-ui/react-tooltip/components-api/#tooltip)
+ */
+function Tooltip(props: TooltipProps) {
+ const tooltip = useTooltip(props);
+
+ const ownerState: TooltipOwnerState = React.useMemo(
+ () => ({
+ open: tooltip.isOpen,
+ }),
+ [tooltip.isOpen],
+ );
+
+ const contextValue = React.useMemo(
+ () => ({
+ ...tooltip,
+ ownerState,
+ }),
+ [tooltip, ownerState],
+ );
+
+ return {props.children} ;
+}
+
+Tooltip.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,
+} as any;
+
+export { Tooltip };
diff --git a/packages/mui-base/src/Tooltip/Tooltip.types.ts b/packages/mui-base/src/Tooltip/Tooltip.types.ts
new file mode 100644
index 0000000000..96a3e040c5
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/Tooltip.types.ts
@@ -0,0 +1,30 @@
+import type { BaseUIComponentProps } from '../utils/BaseUI.types';
+import { UseTooltipParameters } from '../useTooltip';
+
+type GenericHTMLProps = React.HTMLAttributes & { ref?: React.Ref | undefined };
+
+export interface TooltipContextValue {
+ getAnchorProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
+ getTooltipProps: (externalProps?: GenericHTMLProps) => GenericHTMLProps;
+ setAnchor: (value: HTMLElement | null) => void;
+ setTooltip: (value: HTMLElement | null) => void;
+ tooltipStyles: React.CSSProperties;
+ isOpen: boolean;
+ ownerState: TooltipOwnerState;
+}
+
+export type TooltipOwnerState = {
+ open: boolean;
+};
+
+export interface TooltipProps extends UseTooltipParameters {
+ children: React.ReactNode;
+}
+export interface TooltipDelayGroupProps {
+ children: React.ReactNode;
+ delay?: number | { open?: number; close?: number };
+}
+export interface TooltipContentProps extends BaseUIComponentProps<'div', TooltipOwnerState> {}
+export interface TooltipAnchorFragmentProps {
+ children: React.ReactElement | ((htmlProps: GenericHTMLProps) => React.ReactElement);
+}
diff --git a/packages/mui-base/src/Tooltip/TooltipAnchorFragment.tsx b/packages/mui-base/src/Tooltip/TooltipAnchorFragment.tsx
new file mode 100644
index 0000000000..0437adbb00
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/TooltipAnchorFragment.tsx
@@ -0,0 +1,40 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import type { TooltipAnchorFragmentProps } from './Tooltip.types';
+import { useTooltipContext } from './TooltipContext';
+import { useForkRef } from '../utils/useForkRef';
+import { useTooltipStyleHooks } from './utils';
+
+function TooltipAnchorFragment(props: TooltipAnchorFragmentProps) {
+ const { children } = props;
+
+ const { setAnchor, getAnchorProps, ownerState } = useTooltipContext();
+
+ const mergedRef = useForkRef(setAnchor, (children as any).ref);
+
+ const styleHooks = useTooltipStyleHooks(ownerState);
+
+ const anchorProps = getAnchorProps({
+ ref: mergedRef,
+ ...styleHooks,
+ });
+
+ if (typeof children === 'function') {
+ return children(anchorProps);
+ }
+
+ return React.cloneElement(children, anchorProps);
+}
+
+TooltipAnchorFragment.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.oneOfType([PropTypes.element, PropTypes.func]).isRequired,
+} as any;
+
+export { TooltipAnchorFragment };
diff --git a/packages/mui-base/src/Tooltip/TooltipContent.tsx b/packages/mui-base/src/Tooltip/TooltipContent.tsx
new file mode 100644
index 0000000000..50687c0791
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/TooltipContent.tsx
@@ -0,0 +1,64 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingPortal } from '@floating-ui/react';
+import type { TooltipContentProps } from './Tooltip.types';
+import { resolveClassName } from '../utils/resolveClassName';
+import { useTooltipContext } from './TooltipContext';
+import { useForkRef } from '../utils/useForkRef';
+
+function defaultRender(props: React.ComponentPropsWithRef<'div'>) {
+ return
;
+}
+
+const TooltipContent = React.forwardRef(function TooltipContent(
+ props: TooltipContentProps,
+ forwardedRef: React.ForwardedRef,
+) {
+ const { className, render: renderProp, style, ...otherProps } = props;
+ const render = renderProp ?? defaultRender;
+
+ const { isOpen, setTooltip, tooltipStyles, getTooltipProps, ownerState } = useTooltipContext();
+
+ const mergedRef = useForkRef(setTooltip, forwardedRef);
+
+ if (!isOpen) {
+ return null;
+ }
+
+ const contentProps = getTooltipProps({
+ ref: mergedRef,
+ className: resolveClassName(className, ownerState),
+ style: {
+ ...tooltipStyles,
+ ...style,
+ },
+ ...otherProps,
+ });
+
+ return {render(contentProps, ownerState)} ;
+});
+
+TooltipContent.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.func,
+ /**
+ * @ignore
+ */
+ style: PropTypes.object,
+} as any;
+
+export { TooltipContent };
diff --git a/packages/mui-base/src/Tooltip/TooltipContext.ts b/packages/mui-base/src/Tooltip/TooltipContext.ts
new file mode 100644
index 0000000000..5907227760
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/TooltipContext.ts
@@ -0,0 +1,12 @@
+import * as React from 'react';
+import type { TooltipContextValue } from './Tooltip.types';
+
+export const TooltipContext = React.createContext(null);
+
+export function useTooltipContext() {
+ const context = React.useContext(TooltipContext);
+ if (context === null) {
+ throw new Error('useTooltipContext must be used within a Tooltip component');
+ }
+ return context;
+}
diff --git a/packages/mui-base/src/Tooltip/TooltipDelayGroup.tsx b/packages/mui-base/src/Tooltip/TooltipDelayGroup.tsx
new file mode 100644
index 0000000000..aedbbfaa1c
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/TooltipDelayGroup.tsx
@@ -0,0 +1,46 @@
+import * as React from 'react';
+import PropTypes from 'prop-types';
+import { FloatingDelayGroup } from '@floating-ui/react';
+import type { TooltipDelayGroupProps } from './Tooltip.types';
+
+/**
+ *
+ * Demos:
+ *
+ * - [Tooltip](https://mui.com/base-ui/react-tooltip/)
+ *
+ * API:
+ *
+ * - [Tooltip API](https://mui.com/base-ui/react-tooltip/components-api/#tooltip)
+ */
+function TooltipDelayGroup(props: TooltipDelayGroupProps) {
+ const { delay = 0 } = props;
+ return (
+
+ {props.children}
+
+ );
+}
+
+TooltipDelayGroup.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,
+ /**
+ * @ignore
+ */
+ delay: PropTypes.oneOfType([
+ PropTypes.number,
+ PropTypes.shape({
+ close: PropTypes.number,
+ open: PropTypes.number,
+ }),
+ ]),
+} as any;
+
+export { TooltipDelayGroup };
diff --git a/packages/mui-base/src/Tooltip/constants.ts b/packages/mui-base/src/Tooltip/constants.ts
new file mode 100644
index 0000000000..a05329cbc7
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/constants.ts
@@ -0,0 +1 @@
+export const BOUNDARY_PADDING = 5;
diff --git a/packages/mui-base/src/Tooltip/index.ts b/packages/mui-base/src/Tooltip/index.ts
new file mode 100644
index 0000000000..25e6263017
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/index.ts
@@ -0,0 +1,14 @@
+'use client';
+import { Tooltip as TooltipRoot } from './Tooltip';
+import { TooltipContent } from './TooltipContent';
+import { TooltipAnchorFragment } from './TooltipAnchorFragment';
+import { combineComponentExports } from '../utils/combineComponentExports';
+
+export { TooltipDelayGroup } from './TooltipDelayGroup';
+
+export * from './Tooltip.types';
+
+export const Tooltip = combineComponentExports(TooltipRoot, {
+ Content: TooltipContent,
+ AnchorFragment: TooltipAnchorFragment,
+});
diff --git a/packages/mui-base/src/Tooltip/utils.ts b/packages/mui-base/src/Tooltip/utils.ts
new file mode 100644
index 0000000000..57f6aa899e
--- /dev/null
+++ b/packages/mui-base/src/Tooltip/utils.ts
@@ -0,0 +1,15 @@
+import * as React from 'react';
+import type { TooltipOwnerState } from './Tooltip.types';
+import { getStyleHookProps } from '../utils/getStyleHookProps';
+
+export function useTooltipStyleHooks(ownerState: TooltipOwnerState) {
+ return React.useMemo(() => {
+ return getStyleHookProps(ownerState, {
+ open(value) {
+ return {
+ 'data-state': value ? 'open' : 'closed',
+ };
+ },
+ });
+ }, [ownerState]);
+}
diff --git a/packages/mui-base/src/index.ts b/packages/mui-base/src/index.ts
index 233d193879..504e6c3d41 100644
--- a/packages/mui-base/src/index.ts
+++ b/packages/mui-base/src/index.ts
@@ -29,6 +29,7 @@ export * from './TabsList';
export * from './Tabs';
export * from './Tab';
export * from './TextareaAutosize';
+export * from './Tooltip';
export * from './Transitions';
export * from './useAutocomplete';
export * from './useBadge';
@@ -49,6 +50,7 @@ export * from './useTab';
export * from './useTabPanel';
export * from './useTabs';
export * from './useTabsList';
+export * from './useTooltip';
export * from './unstable_useModal';
export {
diff --git a/packages/mui-base/src/useTooltip/index.ts b/packages/mui-base/src/useTooltip/index.ts
new file mode 100644
index 0000000000..9c7834856b
--- /dev/null
+++ b/packages/mui-base/src/useTooltip/index.ts
@@ -0,0 +1,3 @@
+'use client';
+export { useTooltip } from './useTooltip';
+export * from './useTooltip.types';
diff --git a/packages/mui-base/src/useTooltip/useTooltip.ts b/packages/mui-base/src/useTooltip/useTooltip.ts
new file mode 100644
index 0000000000..5926ba25c5
--- /dev/null
+++ b/packages/mui-base/src/useTooltip/useTooltip.ts
@@ -0,0 +1,144 @@
+import * as React from 'react';
+import {
+ autoUpdate,
+ flip,
+ limitShift,
+ offset,
+ shift,
+ useFloating,
+ useHover,
+ useFocus,
+ useDismiss,
+ useRole,
+ useInteractions,
+ safePolygon,
+ useDelayGroupContext,
+ useDelayGroup,
+} from '@floating-ui/react';
+import type { UseTooltipParameters, UseTooltipReturnValue } from './useTooltip.types';
+import { mergeReactProps } from '../utils/mergeReactProps';
+import { useControlled } from '../utils/useControlled';
+import { BOUNDARY_PADDING } from '../Tooltip/constants';
+
+/**
+ * The basic building block for creating custom tooltips.
+ *
+ * API:
+ *
+ * - [useTooltip API](https://mui.com/base-ui/api/use-tooltip/)
+ */
+export function useTooltip(params: UseTooltipParameters) {
+ const {
+ open: externalOpen,
+ onOpenChange,
+ defaultOpen = false,
+ placement = 'top',
+ anchorGap = 0,
+ alignmentOffset = 0,
+ delay = 0,
+ disableHoverableContent = false,
+ allowTouch = true,
+ type = 'description',
+ } = params;
+
+ const [isOpen, setIsOpen] = useControlled({
+ controlled: externalOpen,
+ default: defaultOpen,
+ name: 'Tooltip',
+ state: 'open',
+ });
+
+ const flipMiddleware = flip({ padding: BOUNDARY_PADDING, fallbackAxisSideDirection: 'start' });
+ const shiftMiddleware = shift({ padding: BOUNDARY_PADDING, limiter: limitShift() });
+
+ const middleware = [
+ offset({
+ mainAxis: anchorGap,
+ crossAxis: alignmentOffset,
+ alignmentAxis: alignmentOffset,
+ }),
+ ];
+
+ // https://floating-ui.com/docs/flip#combining-with-shift
+ if (placement.includes('-')) {
+ middleware.push(flipMiddleware, shiftMiddleware);
+ } else {
+ middleware.push(shiftMiddleware, flipMiddleware);
+ }
+
+ const { refs, floatingStyles, context } = useFloating({
+ placement,
+ middleware,
+ whileElementsMounted: autoUpdate,
+ open: isOpen,
+ onOpenChange(open, event) {
+ if (event) {
+ onOpenChange?.(open, event);
+ }
+ setIsOpen(open);
+ },
+ });
+
+ useDelayGroup(context, {
+ id: context.floatingId,
+ });
+
+ const { delay: groupDelay } = useDelayGroupContext();
+
+ const hover = useHover(context, {
+ delay: groupDelay === 0 ? delay : groupDelay,
+ handleClose: disableHoverableContent ? null : safePolygon(),
+ mouseOnly: !allowTouch,
+ });
+ const focus = useFocus(context);
+ const dismiss = useDismiss(context);
+ const role = useRole(context, {
+ enabled: type !== 'visual-only',
+ role: type === 'description' ? 'tooltip' : 'label',
+ });
+
+ const { getReferenceProps, getFloatingProps } = useInteractions([hover, focus, dismiss, role]);
+
+ const getAnchorProps: UseTooltipReturnValue['getAnchorProps'] = React.useCallback(
+ (externalProps = {}) => mergeReactProps(externalProps, getReferenceProps()),
+ [getReferenceProps],
+ );
+
+ const getTooltipProps: UseTooltipReturnValue['getTooltipProps'] = React.useCallback(
+ (externalProps = {}) =>
+ mergeReactProps(
+ externalProps,
+ getFloatingProps({
+ style: {
+ zIndex: 2147483647, // max z-index
+ // Ensure the tooltip is never larger than the viewport minus the boundary padding.
+ // Note: this does not handle scrollbars if visible, but it's mainly important on mobile
+ // where overlay scrollbars are common.
+ maxWidth: `calc(100vw - ${2 * BOUNDARY_PADDING}px)`,
+ },
+ }),
+ ),
+ [getFloatingProps],
+ );
+
+ return React.useMemo(
+ () => ({
+ getAnchorProps,
+ getTooltipProps,
+ isOpen,
+ setIsOpen,
+ setAnchor: refs.setReference,
+ setTooltip: refs.setFloating,
+ tooltipStyles: floatingStyles,
+ }),
+ [
+ getAnchorProps,
+ getTooltipProps,
+ isOpen,
+ setIsOpen,
+ refs.setReference,
+ refs.setFloating,
+ floatingStyles,
+ ],
+ );
+}
diff --git a/packages/mui-base/src/useTooltip/useTooltip.types.ts b/packages/mui-base/src/useTooltip/useTooltip.types.ts
new file mode 100644
index 0000000000..770e38a29c
--- /dev/null
+++ b/packages/mui-base/src/useTooltip/useTooltip.types.ts
@@ -0,0 +1,67 @@
+import type { Placement } from '@floating-ui/react-dom';
+
+export interface UseTooltipParameters {
+ /**
+ * The placement of the tooltip element.
+ * @default 'top'
+ */
+ placement?: Placement;
+ /**
+ * The gap between the anchor element and the tooltip element.
+ * @default 0
+ */
+ anchorGap?: number;
+ /**
+ * The offset of the tooltip element along its alignment axis.
+ * @default 0
+ */
+ alignmentOffset?: number;
+ /**
+ * The delay in milliseconds before the tooltip opens and closes after the anchor element is
+ * triggered.
+ * @default 0
+ */
+ delay?: number | { open?: number; close?: number };
+ /**
+ * If `true`, the tooltip will be shown when using touch input.
+ * @default true
+ */
+ allowTouch?: boolean;
+ /**
+ * If `true`, the tooltip content will not be hoverable.
+ * @default false
+ */
+ disableHoverableContent?: boolean;
+ /**
+ * The type of the tooltip.
+ * @default 'description'
+ */
+ type?: 'label' | 'description' | 'visual-only';
+ /**
+ * If `true`, the tooltip will be open by default. Use when uncontrolled.
+ */
+ defaultOpen?: boolean;
+ /**
+ * If `true`, the tooltip will be open. Use when controlled.
+ */
+ open?: boolean;
+ /**
+ * Callback fired when the tooltip is requested to be opened or closed.
+ */
+ onOpenChange?: (isOpen: boolean, event: Event) => void;
+}
+
+export interface UseTooltipReturnValue {
+ /**
+ * Props to spread on the anchor element.
+ */
+ getAnchorProps: (
+ externalProps?: React.HTMLAttributes,
+ ) => React.HTMLAttributes;
+ /**
+ * Props to spread on the tooltip element.
+ */
+ getTooltipProps: (
+ externalProps?: React.HTMLAttributes,
+ ) => React.HTMLAttributes;
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ad43eceffa..900732ccf4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -616,6 +616,9 @@ importers:
'@babel/runtime':
specifier: ^7.24.1
version: 7.24.1
+ '@floating-ui/react':
+ specifier: ^0.26.10
+ version: 0.26.10(react-dom@18.2.0)(react@18.2.0)
'@floating-ui/react-dom':
specifier: ^2.0.8
version: 2.0.8(react-dom@18.2.0)(react@18.2.0)
@@ -2882,6 +2885,19 @@ packages:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
+ /@floating-ui/react@0.26.10(react-dom@18.2.0)(react@18.2.0):
+ resolution: {integrity: sha512-sh6f9gVvWQdEzLObrWbJ97c0clJObiALsFe0LiR/kb3tDRKwEhObASEH2QyfdoO/ZBPzwxa9j+nYFo+sqgbioA==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+ dependencies:
+ '@floating-ui/react-dom': 2.0.8(react-dom@18.2.0)(react@18.2.0)
+ '@floating-ui/utils': 0.2.1
+ react: 18.2.0
+ react-dom: 18.2.0(react@18.2.0)
+ tabbable: 6.2.0
+ dev: false
+
/@floating-ui/utils@0.2.1:
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
@@ -16021,6 +16037,10 @@ packages:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: true
+ /tabbable@6.2.0:
+ resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
+ dev: false
+
/table@6.8.1:
resolution: {integrity: sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==}
engines: {node: '>=10.0.0'}