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
@mnajdova
Copy link
Copy Markdown
Member

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

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

I would go with this option if we do decide to do something about it, as it reflects the primitives elements. Can you link some issues related to the problem? What are people typically complaining about.

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.

4 participants