Problem
MUI X Charts (LineChart, BarChart, etc.) positions its tooltip via a Popper component that uses absolute/fixed positioning internally. When the chart is rendered inside a Paper, Card, or Dialog ancestor, those components create a new CSS stacking context (due to box-shadow + z-index). This causes the tooltip to be clipped or rendered behind the ancestor — invisible or in the wrong location on screen.
v8 Fix (superseded)
The original fix portalled the Popper to document.body:
slotProps={{ tooltip: { container: document.body } }}This worked in v8 because the container prop was respected and placed the tooltip outside all ancestor stacking contexts.
v9 Behaviour Change
@mui/x-charts@9.3.0 silently overrides container — it always uses chartsLayerContainerRef.current (the chart’s own root <div>) regardless of what is passed. The container prop is a no-op in v9.
v9 also switches the Popper to popperOptions: { strategy: 'fixed' }, which makes the tooltip viewport-relative and partially addresses the stacking-context issue. However, the tooltip’s z-index defaults to theme.zIndex.modal (1300), which conflicts with Dialog (also 1300). Inside a Dialog the tooltip can still be obscured.
Current Fix (v9)
Both wrappers apply a fixedTooltipSx that raises the tooltip’s z-index to theme.zIndex.tooltip (1500), placing it above all modal-level components:
const fixedTooltipSx = (theme: Theme) => ({ zIndex: theme.zIndex.tooltip });
function mergeTooltipSx(callerSx: SxProps<Theme> | undefined): SxProps<Theme> {
const normalized = Array.isArray(callerSx)
? callerSx
: callerSx
? [callerSx]
: [];
return [...normalized, fixedTooltipSx];
}
export const AppLineChart = ({ slotProps, ...rest }: LineChartProps) => {
const { tooltip: callerTooltip, ...otherSlotProps } = slotProps ?? {};
return (
<LineChart
{...rest}
slotProps={{
...otherSlotProps,
tooltip: { ...callerTooltip, sx: mergeTooltipSx(callerTooltip?.sx) },
}}
/>
);
};Key decisions:
| Point | Detail |
|---|---|
container removed | Was a no-op in v9 — removed to avoid confusion |
sx array merge | Caller’s sx is normalised to an array and fixedTooltipSx is appended last, so it always wins |
slotProps deep-merge | callerTooltip spread before the sx override — callers can pass their own tooltip slotProps without losing the z-index fix |
| z-index value | theme.zIndex.tooltip = 1500, above modal (1300) and dialog (1300) |
AppBarChart follows the identical pattern with BarChart / BarChartProps.
Wrapper Components — Always Use These
Project-wide wrappers live in frontend/shared/components/:
| Wrapper | Replaces |
|---|---|
AppLineChart | @mui/x-charts/LineChart |
AppBarChart | @mui/x-charts/BarChart |
Never import LineChart or BarChart directly from @mui/x-charts. Always use the app wrappers — they carry the tooltip fix and forward all props.
// ✅ Correct
import { AppLineChart } from '@frontend/shared/components/AppLineChart';
// ❌ Wrong — tooltip will break inside Paper/Card/Dialog
import { LineChart } from '@mui/x-charts/LineChart';If a new chart type is needed (e.g. ScatterChart), create a matching AppScatterChart.tsx wrapper using the same pattern.