mrkeyoor.com_
Thu 06 Aug 15:41 UTC
npmWeb Frontendupdated 06 Aug 2026

@mui/material

Material UI is a React component library that implements Google's Material Design: roughly a hundred finished, accessible components covering buttons, forms, tables, dialogs, navigation, feedback, and layout. You get them styled and keyboard-accessible out of the box, plus a theme object that centralizes palette, typography, spacing, breakpoints, and per-component defaults. Styling happens through three layers you can mix: the sx prop for one-off styles with theme-aware shorthands, styled() for reusable styled components, and theme.components overrides for changing a component everywhere at once. Under the hood it uses Emotion as the styling engine, which means CSS-in-JS at runtime. It is the most used React component library by a wide margin, and it sits alongside MUI X, a separate suite that adds the data grid, date pickers, charts, and tree view.

Verdict

For an internal tool, dashboard, or admin app in React, Material UI still gets you further in a week than anything else, and the docs and hiring pool back that up. Reach for something lighter if you are shipping a public marketing site, going server-component first, or planning a visual identity that is not Material.

API stability3/5Core APIs (sx, styled, ThemeProvider, createTheme) have held since v5, but majors arrive quickly and each one clears out deprecations: the current guide jumps from v7 to v9, GridLegacy and the Box and Typography system shorthands are gone, and slots plus slotProps replaced several component-specific prop bags. Codemods cover most of it, which is the only reason this is not lower
Docs5/5mui.com has an editable live demo for nearly every component, a full props and CSS class table per component, versioned sites back to v4, migration guides with matching codemod commands, and per-integration pages for Next.js, Remix, and Vite
Maintenance5/5Company-backed and shipping constantly: 9.3.1 published 6 August 2026, 9.3.0 the day before, and the repo was pushed the same day. The counterweight is a backlog of 1382 open issues and 114 open PRs
Ecosystem5/5About 10.2M weekly downloads and 98.7k stars, with MUI X for grids and pickers, official Next.js and Remix integrations, free and paid templates, and a very large body of third-party themes and Stack Overflow answers

Use it if

  • You are building an admin panel, internal tool, or dashboard where you need dozens of finished components tomorrow and a coherent look matters more than a distinctive one
  • Your team is React plus TypeScript and wants first-party types everywhere: theme augmentation, per-component prop types, and typed slots and slotProps rather than a bag of className strings
  • You want one theme object to control palette, typography scale, spacing, shape, breakpoints, and per-component defaults, so a design change is a diff in one file instead of a sweep across components
  • You need Material Design specifically, for example a web app that has to match an Android product or a Google-adjacent design system
  • You expect turnover or handover. It is the React component library the largest number of developers have already used, so onboarding cost is low and almost every question has an existing answer
  • You know a data grid or date picker is coming and want a paid escape hatch (MUI X Pro and Premium) instead of discovering later that your free library cannot do row grouping
Skip it if

Setup reality

The install is npm install @mui/material @emotion/react @emotion/styled. Emotion is listed as an optional peer dependency but you need it unless you deliberately swap in @mui/styled-engine-sc with styled-components, which the docs tell you not to do for server-rendered apps. React 17, 18, and 19 are supported peers, but on React 18 or below you must pin react-is to your React version through overrides or resolutions, because Material UI depends on react-is 19 and mismatched copies break element type checks at runtime. The Roboto font is not bundled: install @fontsource/roboto and import the 300, 400, 500, and 700 weights, or add the Google Fonts link, or your typography quietly falls back to system fonts. Icons are a separate install (@mui/icons-material), and both packages should be imported by path (import Button from '@mui/material/Button') rather than from the barrel, because barrel imports make dev startup and rebuilds noticeably slower even though production bundlers tree shake fine. Next.js 13.5 and later handle that automatically via optimizePackageImports. On the App Router you also want @mui/material-nextjs plus @emotion/cache and the AppRouterCacheProvider wrapper so styles land in the head instead of the body. Finally, keep the family in lockstep: @mui/material, @mui/system, @mui/utils, @mui/icons-material, @mui/styled-engine, and @mui/material-nextjs all need to be on matching 9.x versions.

Patterns

Wire up the theme and baseline stylestheme-provider-setup

import { ThemeProvider, createTheme } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import Button from '@mui/material/Button';

const theme = createTheme({
  cssVariables: true,
  palette: { primary: { main: '#1976d2' } },
  shape: { borderRadius: 8 },
});

export default function App() {
  return (
    <ThemeProvider theme={theme}>
      <CssBaseline />
      <Button variant="contained">Save</Button>
    </ThemeProvider>
  );
}

CssBaseline has to render inside ThemeProvider or it applies default colors rather than yours. Import components by path as shown; the barrel import from '@mui/material' works but slows dev startup and rebuilds.

Style one instance with the sx propsx-prop-styling

import Box from '@mui/material/Box';

<Box
  sx={{
    mt: 2,
    p: { xs: 1, md: 3 },
    color: 'text.secondary',
    bgcolor: 'background.paper',
    borderRadius: 1,
    '&:hover': { bgcolor: 'action.hover' },
  }}
/>

Numeric spacing values are multiples of theme.spacing (2 means 16px by default), object values are breakpoint maps, and strings like 'text.secondary' resolve against the palette. In v9 the old shorthand props are gone: mt={2} on Box, Stack, Grid, Link, or Typography has to move inside sx.

Support light, dark, and system with a toggledark-mode-toggle

import { ThemeProvider, createTheme, useColorScheme } from '@mui/material/styles';

const theme = createTheme({ cssVariables: true, colorSchemes: { dark: true } });

function ModeToggle() {
  const { mode, setMode } = useColorScheme();
  if (!mode) {
    return null;
  }
  return (
    <select value={mode} onChange={(e) => setMode(e.target.value)}>
      <option value="system">System</option>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
    </select>
  );
}

<ThemeProvider theme={theme} defaultMode="system" disableTransitionOnChange>
  <ModeToggle />
</ThemeProvider>

mode is undefined on the very first render, so the early return is not optional: skip it and you get a hydration mismatch. Use colorSchemes rather than palette.mode if you want cross-tab syncing and system preference, and style dark variants with theme.applyStyles('dark', {...}) instead of checking theme.palette.mode, which flickers during SSR.

Lay out a responsive gridresponsive-grid

import Grid from '@mui/material/Grid';
import Stack from '@mui/material/Stack';

<Grid container spacing={2}>
  <Grid size={{ xs: 12, md: 8 }}>main</Grid>
  <Grid size={{ xs: 12, md: 4 }}>sidebar</Grid>
  <Grid size="grow" offset={{ md: 2 }}>fills the rest</Grid>
</Grid>

<Stack spacing={2} sx={{ alignItems: 'center' }}>
  <div>stacked</div>
</Stack>

There is no item prop and no xs or md props any more; widths go in size. Grid only subdivides into columns, so v9 rejects direction="column": use Stack for vertical layout. Grid also has no row spanning and no auto-placement, so genuinely two-dimensional layouts belong in CSS Grid.

Change a component everywhere from the themetheme-component-overrides

const theme = createTheme({
  components: {
    MuiButton: {
      defaultProps: { disableElevation: true, size: 'small' },
      styleOverrides: {
        root: {
          textTransform: 'none',
          variants: [
            {
              props: { variant: 'contained', color: 'primary' },
              style: { boxShadow: 'none' },
            },
          ],
        },
      },
    },
  },
});

defaultProps changes behavior, styleOverrides changes CSS, and variants (nested inside the slot, not at the component level) applies styles conditionally on props. Order matters inside the variants array: later entries win.

Build a reusable styled componentstyled-component

import { styled } from '@mui/material/styles';
import Paper from '@mui/material/Paper';

const Panel = styled(Paper, {
  shouldForwardProp: (prop) => prop !== 'tone',
})(({ theme, tone }) => ({
  padding: theme.spacing(2),
  borderLeft: `4px solid ${theme.palette[tone].main}`,
  ...theme.applyStyles('dark', {
    backgroundColor: theme.vars.palette.grey[900],
  }),
}));

<Panel tone="warning" elevation={0}>heads up</Panel>

Import styled from '@mui/material/styles', not from '@emotion/styled', or the theme argument is empty. Without shouldForwardProp a custom prop like tone leaks onto the DOM and React logs an unknown-attribute warning. theme.vars only exists when cssVariables is enabled.

Reach into a TextField's inner elementstext-field-slot-props

import TextField from '@mui/material/TextField';
import InputAdornment from '@mui/material/InputAdornment';

<TextField
  label="Amount"
  error={Boolean(error)}
  helperText={error ?? 'Pre-tax'}
  slotProps={{
    input: {
      startAdornment: <InputAdornment position="start">$</InputAdornment>,
    },
    htmlInput: { maxLength: 10, inputMode: 'decimal' },
    inputLabel: { shrink: true },
  }}
/>

v9 removed InputProps, inputProps, InputLabelProps, SelectProps, and FormHelperTextProps; they are all keys under slotProps now. The old lowercase inputProps is slotProps.htmlInput (attributes on the actual input element), while slotProps.input targets the wrapper component. Run npx @mui/codemod@latest deprecations/text-field-props to convert existing code.

Control when a dialog is allowed to closedialog-close-reason

import Dialog from '@mui/material/Dialog';
import DialogTitle from '@mui/material/DialogTitle';
import DialogActions from '@mui/material/DialogActions';
import Button from '@mui/material/Button';

function handleClose(event, reason) {
  if (reason === 'escapeKeyDown' || reason === 'backdropClick') {
    return;
  }
  setOpen(false);
}

<Dialog open={open} onClose={handleClose} fullWidth maxWidth="sm">
  <DialogTitle>Delete project</DialogTitle>
  <DialogActions>
    <Button onClick={() => setOpen(false)}>Cancel</Button>
  </DialogActions>
</Dialog>

disableEscapeKeyDown was removed in v9, so filtering on the reason argument is now the only way to block escape. Note that onClose does not fire at all for the Cancel button; you close that path yourself.

Autocomplete backed by a remote searchautocomplete-async

import * as React from 'react';
import Autocomplete from '@mui/material/Autocomplete';
import TextField from '@mui/material/TextField';
import CircularProgress from '@mui/material/CircularProgress';

<Autocomplete
  options={options}
  loading={loading}
  getOptionLabel={(o) => o.name}
  isOptionEqualToValue={(o, v) => o.id === v.id}
  onInputChange={(event, value) => search(value)}
  filterOptions={(x) => x}
  renderInput={(params) => (
    <TextField
      {...params}
      label="User"
      slotProps={{
        ...params.slotProps,
        input: {
          ...params.slotProps.input,
          endAdornment: (
            <React.Fragment>
              {loading ? <CircularProgress color="inherit" size={20} /> : null}
              {params.slotProps.input.endAdornment}
            </React.Fragment>
          ),
        },
      }}
    />
  )}
/>

In v9 the object passed to renderInput carries slotProps rather than the old InputProps and inputProps, so spread params.slotProps first or you lose the clear button and the popup arrow. isOptionEqualToValue is required for object options; without it React warns and the selected value never highlights. filterOptions={(x) => x} turns off the client-side filter, which is what you want when the server already filtered, and you debounce onInputChange yourself because the component does not.

Set it up in a Next.js App Router projectnextjs-app-router

// app/theme.ts
'use client';
import { createTheme } from '@mui/material/styles';
export default createTheme({ cssVariables: true, colorSchemes: { dark: true } });

// app/layout.tsx
import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter';
import { ThemeProvider } from '@mui/material/styles';
import CssBaseline from '@mui/material/CssBaseline';
import theme from './theme';

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <AppRouterCacheProvider>
          <ThemeProvider theme={theme}>
            <CssBaseline />
            {children}
          </ThemeProvider>
        </AppRouterCacheProvider>
      </body>
    </html>
  );
}

Install @mui/material-nextjs and @emotion/cache for this, and match the import path to your Next.js major (v15-appRouter, v14-appRouter, and so on). The theme file needs 'use client' because createTheme returns functions that cannot cross the server boundary. Without AppRouterCacheProvider the styles get injected into the body during streaming.

Branch on viewport width in JavaScriptresponsive-breakpoints

import useMediaQuery from '@mui/material/useMediaQuery';

function Nav() {
  const isDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'));
  return isDesktop ? <SideBar /> : <BottomBar />;
}

The hook needs a ThemeProvider above it; there is no implicit default theme. It renders twice under SSR (once with the server value, once resolved), so prefer CSS-level responsiveness through sx or the Box display props when you can, and pass { noSsr: true } only for client-only trees. jsdom has no matchMedia, so tests need a polyfill.

Add a new variant and teach TypeScript about itcustom-variant-typescript

// theme.ts
declare module '@mui/material/Button' {
  interface ButtonPropsVariantOverrides {
    dashed: true;
  }
}

const theme = createTheme({
  components: {
    MuiButton: {
      styleOverrides: {
        root: {
          variants: [
            {
              props: { variant: 'dashed' },
              style: { textTransform: 'none', border: '2px dashed currentColor' },
            },
          ],
        },
      },
    },
  },
});

<Button variant="dashed">Import CSV</Button>

The module augmentation has to live in a file that is part of your TypeScript program and imports something from MUI, otherwise the declare module block is ignored and variant="dashed" fails to typecheck. The same pattern with PaletteOptions adds custom palette colors.

Alternatives

PackageRegistryPick it when
@mantine/corenpmYou want a similarly complete component set and hooks library with a less opinionated visual identity and CSS modules instead of runtime CSS-in-JS
antdnpmYou are building a dense enterprise back office and want a free data table, tree, and transfer list without a paid tier
@base-ui-components/reactnpmYou want the accessibility and behavior primitives from the same team with no styles attached, so you can pair them with Tailwind or your own CSS
@chakra-ui/reactnpmYou prefer a style-props API and a smaller, more composable set of components over a full Material Design implementation