Skip to content

[utils] Add opt-in DataAttributesOverrides augmentation for slot props#48554

Open
LukasTy wants to merge 1 commit into
mui:masterfrom
LukasTy:claude/data-attributes-overrides
Open

[utils] Add opt-in DataAttributesOverrides augmentation for slot props#48554
LukasTy wants to merge 1 commit into
mui:masterfrom
LukasTy:claude/data-attributes-overrides

Conversation

@LukasTy
Copy link
Copy Markdown
Member

@LukasTy LukasTy commented May 20, 2026

Summary

Adds a single opt-in switch — DataAttributesOverrides — that lets consumers declare typed support for data-* attributes on every MUI slot prop. Augmenting the interface is the one sanctioned way to flip the level of strictness; nothing is widened by default.

Today, code like

<Backdrop slotProps={{ root: { 'data-testid': 'backdrop' } }} />

is a TypeScript error even though the attribute is forwarded to the DOM at runtime. After this PR, consumers can opt in with a one-time augmentation:

declare module '@mui/utils/types' {
  interface DataAttributesOverrides {
    'data-testid'?: string;
  }
}

Or, for the loose / "anything goes" form:

declare module '@mui/utils/types' {
  interface DataAttributesOverrides {
    [k: `data-${string}`]: string | number | boolean | undefined;
  }
}

After augmentation, every Material component that wires slot props through SlotComponentProps / SlotComponentPropsWithSlotState (or through SlotProps in @mui/material) picks up the augmented keys automatically.

Changes

@mui/utils/types

  • New DataAttributes.ts:
    • DataAttributesOverrides — empty, module-augmentable interface. The single switch consumers flip.
    • DataAttributes = DataAttributesOverrides — dormant by default; activates when augmented.
    • WithDataAttributes<T> = T | (T & DataAttributes) — union form so the original T stays assignable as-is (preserves backwards compatibility with x as CustomProps style casts on slot values), while augmented keys flow through the widened branch when consumers opt in.
  • index.ts re-exports the new symbols, and wraps both SlotComponentProps and SlotComponentPropsWithSlotState (object branch and callback branch) with WithDataAttributes.

Module-augmentation test

packages/mui-material/test/typescript/moduleAugmentation/dataAttributesOverrides.{spec.tsx,tsconfig.json} augments @mui/utils/types and uses Backdrop's root slot to verify the augmentation flows through SlotPropsSlotComponentPropsWithDataAttributes.

Notes for reviewers

Why a union, not an intersection

export type SlotComponentProps<...> =
  | WithDataAttributes<Partial<React.ComponentPropsWithRef<TSlotComponent>> & TOverrides>
  | ((ownerState: TOwnerState) =>
      WithDataAttributes<Partial<React.ComponentPropsWithRef<TSlotComponent>> & TOverrides>);

export type WithDataAttributes<T> = T | (T & DataAttributes);

Only the widened variant carries DataAttributes. This preserves backwards compatibility:

  • { id: 'foo' } as CustomLabelProps assigns to the narrow variant, so CustomLabelProps does not need to declare a data-${string} index signature.
  • { 'data-testid': 'x' } (after augmentation) assigns to the widened variant.
  • The callback branch also uses WithDataAttributes, so consumers returning someObj as CustomProps from a slot callback stay assignable.

Why opt-in (no default widening)

Slot prop types should not silently accept arbitrary data-* keys — that hides typos, makes the surface less discoverable in hover/autocomplete, and disagrees with the principle that React's typed surface is what consumers see.

The augmentation hook is a single, well-known module path (@mui/utils/types) that consumers can shape to whichever level they want: strict (one named key), loose (full data-* template-literal index signature), or anywhere in between.

Why @mui/utils/types is the right home

Every MUI slot prop type ultimately flows through SlotComponentProps / SlotComponentPropsWithSlotState in @mui/utils/types. Putting the augmentation hook here means:

  • A consumer's single augmentation lights up every Material component's slots automatically.
  • Downstream packages (@mui/x-data-grid, @mui/x-date-pickers, @mui/x-charts, ...) inherit the augmentation transitively — they don't need to mirror the helper themselves.
  • The contract is documented in one place.

Test plan

  • pnpm --filter "@mui/utils" run typescript passes.
  • pnpm --filter "@mui/material" run typescript passes.
  • pnpm typescript:module-augmentation — all 21+ existing tests pass, plus the new one.
  • pnpm prettier --check and pnpm eslint clean on the changed files.
  • Manual: with the four-line DataAttributesOverrides augmentation above, <Backdrop slotProps={{ root: { 'data-testid': 'foo' } }} /> type-checks and forwards to the DOM as expected. Without the augmentation, the same code is a TS error (matches today).

Today, passing `data-testid` (or any `data-*` attribute) through `slotProps`
on a MUI component is a TypeScript error even though the attribute is
forwarded to the DOM at runtime. This adds a single, opt-in switch that
lets consumers declare exactly which `data-*` keys they want typed, at
whichever level of strictness they choose.

- `DataAttributesOverrides`: module-augmentable empty interface in
  `@mui/utils/types`. The single sanctioned switch.
- `DataAttributes = DataAttributesOverrides`: dormant by default;
  populated only when a consumer augments.
- `WithDataAttributes<T> = T | (T & DataAttributes)`: union form so the
  original `T` stays assignable as-is (preserves backwards compatibility
  with `x as CustomProps` style casts on slot values), while augmented
  keys flow through the widened branch when consumers opt in.
- `SlotComponentProps` and `SlotComponentPropsWithSlotState` now wrap
  their object and callback branches with `WithDataAttributes`. With an
  empty default the wrapping is a no-op until a consumer augments — once
  they do, every Material component that reaches slot props through these
  helpers (or through `SlotProps` in `@mui/material`) picks up the new
  keys automatically.

Consumers opt in with a single `declare module '@mui/utils/types' { ... }`
block. Examples are documented in the new `DataAttributes.ts` file and
exercised by a module-augmentation test using Backdrop's root slot.

This is the canonical place for the helper because every MUI slot prop
type ultimately flows through `@mui/utils/types`; downstream packages
(`@mui/x-*` and friends) get the augmentation transitively without
having to mirror the helper themselves.
@code-infra-dashboard
Copy link
Copy Markdown

Deploy preview

https://deploy-preview-48554--material-ui.netlify.app/

Bundle size

Bundle Parsed size Gzip size
@mui/material 0B(0.00%) 0B(0.00%)
@mui/lab 0B(0.00%) 0B(0.00%)
@mui/private-theming 0B(0.00%) 0B(0.00%)
@mui/system 0B(0.00%) 0B(0.00%)
@mui/utils 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@zannager zannager added the package: utils Specific to the utils package. label May 20, 2026
@zannager zannager requested a review from mnajdova May 20, 2026 13:23
* augmentation is the single switch consumers can flip to choose their level
* of strictness.
*
* Examples:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: could use JSdoc @example

* }
* }
*
* // Loose: accept any `data-*` key on slots.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could the spec cover this as well?

@LukasTy LukasTy added the type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature. label May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

package: utils Specific to the utils package. type: enhancement It’s an improvement, but we can’t make up our mind whether it's a bug fix or a new feature.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants