Using MUI and Component Libraries in Production React Apps
When Material UI speeds up client delivery, when it hurts performance, and how I customize, tree-shake, and integrate MUI with Tailwind on real projects.
A US logistics company wanted an internal admin dashboard in eight weeks. Figma files covered maybe 40% of the screens — tables, filters, date ranges, modals, snackbars, form validation. Building a custom design system first would have consumed half the budget.
We shipped with Material UI (MUI). Not because it’s the only option — Radix, Chakra, and headless primitives all have merits — but because the team knew it, accessibility was solid out of the box, and data-dense UIs are MUI’s strength.
Six months later, the dashboard handles 200+ daily users. Bundle size is acceptable. The client is happy. Here’s how I make component libraries work in production without the “generic Material look” problem.
When a Component Library Makes Sense
I recommend MUI or similar when:
- Timeline is tight and screens are standard (tables, forms, dialogs)
- The product is internal — brand uniqueness matters less than function
- Accessibility is non-negotiable — MUI components ship with ARIA patterns
- The team has existing MUI experience
I avoid defaulting to MUI when:
- Marketing site with strong brand identity (custom Tailwind wins)
- Extreme performance budget (marketing landing, Core Web Vitals critical)
- Highly custom interaction design that fights the library’s defaults
Theming to Match Client Brand
Out-of-the-box MUI looks like Google. Clients notice.
import { createTheme, ThemeProvider } from "@mui/material/styles";
const theme = createTheme({
palette: {
mode: "dark",
primary: { main: "#00fff7" },
secondary: { main: "#bf00ff" },
background: { default: "#020204", paper: "#0a0a12" },
},
typography: {
fontFamily: '"ui-monospace", "Courier New", monospace',
button: { textTransform: "none", fontWeight: 700 },
},
shape: { borderRadius: 4 },
components: {
MuiButton: {
styleOverrides: {
root: {
clipPath:
"polygon(0 0, calc(100% - 8px) 0, 100% 8px, 100% 100%, 8px 100%, 0 calc(100% - 8px))",
},
},
},
},
});
export function AppProviders({ children }: { children: React.ReactNode }) {
return <ThemeProvider theme={theme}>{children}</ThemeProvider>;
}
On the logistics project, custom styleOverrides on MuiDataGrid, MuiTextField, and MuiChip aligned the UI with their brand guidelines in two days.
Tree-Shaking and Import Discipline
MUI bundle bloat comes from barrel imports:
// Bad — pulls more than you need
import { Button, TextField, Dialog } from "@mui/material";
// Better — path imports (MUI v5+)
import Button from "@mui/material/Button";
import TextField from "@mui/material/TextField";
Enable the mui-path-imports ESLint rule or use a Babel/Vite plugin. On a UK fintech admin panel, path imports reduced the MUI chunk by ~35%.
For icons:
import SearchIcon from "@mui/icons-material/Search";
// Not: import { Search } from "@mui/icons-material";
MUI + Tailwind on the Same Project
Clients often want Tailwind for marketing pages and MUI for the app shell. It works — with boundaries.
// App shell — MUI
<Box sx={{ display: "flex", minHeight: "100vh" }}>
<Drawer variant="permanent">{nav}</Drawer>
<Box component="main" sx={{ flex: 1, p: 3 }}>
{children}
</Box>
</Box>
// Marketing section — Tailwind in separate route/layout
<div className="bg-surface px-6 py-20">
<h1 className="text-4xl font-black text-white">Pricing</h1>
</div>
Don’t mix Tailwind classes on MUI components in the same element — specificity wars result. Pick one system per component.
For this portfolio, I use Tailwind exclusively. For SaaS dashboards, MUI + Tailwind split by route group is common in my client work.
DataGrid for Admin UIs
MUI X DataGrid is why many clients choose MUI:
import { DataGrid, GridColDef } from "@mui/x-data-grid";
const columns: GridColDef<Order>[] = [
{ field: "reference", headerName: "Reference", flex: 1 },
{ field: "status", headerName: "Status", width: 140 },
{ field: "total", headerName: "Total", type: "number", width: 120 },
];
<DataGrid
rows={orders}
columns={columns}
getRowId={(row) => row.id}
pageSizeOptions={[25, 50, 100]}
checkboxSelection
disableRowSelectionOnClick
/>
Server-side pagination, sorting, and filtering are built in. Rebuilding this with headless primitives takes weeks.
License note: MUI X advanced features require a commercial license for large orgs. I clarify this in client proposals upfront.
Forms: React Hook Form + MUI
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import TextField from "@mui/material/TextField";
const schema = z.object({
email: z.string().email(),
company: z.string().min(2),
});
function ContactForm() {
const { control, handleSubmit } = useForm({
resolver: zodResolver(schema),
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
name="email"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
label="Email"
error={!!fieldState.error}
helperText={fieldState.error?.message}
fullWidth
margin="normal"
/>
)}
/>
</form>
);
}
MUI handles visuals. React Hook Form handles state. Zod handles validation. This stack ships fast and types cleanly.
Performance Considerations
- Lazy-load heavy components — DataGrid, DatePicker bundles are large
- Path imports everywhere
- Avoid mounting 50 DataGrids — virtualization helps but isn’t magic
- Server Components in Next.js for static chrome; MUI in client islands
const AnalyticsGrid = dynamic(() => import("./AnalyticsGrid"), {
ssr: false,
loading: () => <GridSkeleton />,
});
LCP on admin apps is usually less critical than INP — users are authenticated and tolerant of slightly slower loads. Still, don’t ship 400KB of MUI on the login page.
Accessibility
MUI components implement WAI-ARIA patterns. That doesn’t mean you can ignore a11y:
- Always wire
label+idon form fields - Test DataGrid keyboard navigation with screen readers
- Don’t override focus styles without replacements
- Verify color contrast after theming — cyan on dark often fails
I run axe DevTools on every MUI screen before handoff.
Common Mistakes
Using MUI for a marketing landing page. Wrong tool.
Barrel imports. Bundle size explodes.
Fighting the theme system with inline sx on every element instead of centralized overrides.
Skipping the commercial license check for MUI X Pro features.
Mixing MUI and Tailwind on the same DOM node. Specificity bugs for days.
Snackbars, Dialogs, and Feedback Patterns
Admin UIs live on feedback loops — save confirmations, error alerts, undo actions.
import Snackbar from "@mui/material/Snackbar";
import Alert from "@mui/material/Alert";
function useAppToast() {
const [toast, setToast] = useState<{ message: string; severity: "success" | "error" } | null>(null);
const show = useCallback((message: string, severity: "success" | "error" = "success") => {
setToast({ message, severity });
}, []);
const Toast = (
<Snackbar open={!!toast} autoHideDuration={4000} onClose={() => setToast(null)}>
<Alert severity={toast?.severity} variant="filled" onClose={() => setToast(null)}>
{toast?.message}
</Alert>
</Snackbar>
);
return { show, Toast };
}
Centralize toast logic once. Every feature team uses the same show("Order updated") API instead of inventing modal patterns per screen.
For destructive actions, MUI Dialog with explicit aria-labelledby and focus trap:
<Dialog open={open} onClose={onClose} aria-labelledby="delete-dialog-title">
<DialogTitle id="delete-dialog-title">Delete shipment?</DialogTitle>
<DialogContent>This cannot be undone.</DialogContent>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button color="error" variant="contained" onClick={onConfirm}>Delete</Button>
</DialogActions>
</Dialog>
Date and Time Pickers in Logistics UIs
The US logistics dashboard needed timezone-aware scheduling. MUI X Date Pickers with date-fns adapter:
import { LocalizationProvider } from "@mui/x-date-pickers/LocalizationProvider";
import { AdapterDateFns } from "@mui/x-date-pickers/AdapterDateFns";
import { DateTimePicker } from "@mui/x-date-pickers/DateTimePicker";
<LocalizationProvider dateAdapter={AdapterDateFns}>
<DateTimePicker
label="Pickup window"
value={pickupTime}
onChange={setPickupTime}
slotProps={{ textField: { fullWidth: true } }}
/>
</LocalizationProvider>
Lazy-load the picker module — @mui/x-date-pickers is not small. We loaded it only on the scheduling route, saving ~45KB on the dashboard home.
Dark Mode With MUI
Most admin tools I ship support dark mode — operators stare at dashboards for hours.
const theme = createTheme({
palette: {
mode: prefersDark ? "dark" : "light",
primary: { main: "#00fff7" },
background: {
default: prefersDark ? "#020204" : "#f8fafc",
paper: prefersDark ? "#0a0a12" : "#ffffff",
},
},
});
Store preference in localStorage, default to prefers-color-scheme, and expose a toggle in the app bar. MUI’s CssBaseline handles background consistency across portals and modals.
Test contrast after switching modes — semantic colors like error.main and success.main often need adjustment in dark palette.
SEO Considerations for Admin vs Marketing
MUI admin apps are usually behind authentication — SEO doesn’t apply. But marketing pages in the same monorepo do. Keep MUI out of public landing routes entirely. Google doesn’t care about your DataGrid; it cares about LCP on the hero H1.
For authenticated apps, focus on INP and perceived speed. For public pages, Server Components + minimal client JS beats any component library.
Client Handoff and Documentation
When I deliver MUI projects, I include:
- Theme file with documented token overrides
- Storybook or Ladle stories for customized components
- Import conventions doc (path imports, no barrels)
- List of licensed MUI X features in use
This reduces “why does this button look different on staging” Slack messages after handoff.
Headless Alternatives
When MUI feels heavy, I reach for:
- Radix UI + Tailwind — full design control, more build time
- shadcn/ui — copy-paste Radix primitives, great for branded SaaS
- Chakra UI — similar to MUI, different API
For a EU startup’s customer-facing app, we used shadcn/ui. For their internal ops dashboard, MUI. Same company, different tools per surface.
Migration Away From MUI Later
Clients sometimes ask: “Are we locked in?” You’re not, but migration has cost.
I structure MUI projects so business logic doesn’t import @mui/material:
features/orders/
api.ts # fetchOrders — no MUI
hooks.ts # useOrders — no MUI
OrderTable.tsx # MUI DataGrid here only
If they rebrand in two years, you swap OrderTable.tsx for a Tailwind implementation without touching data layers. Coupling logic to sx props everywhere is what creates lock-in — not the library itself.
Real Timeline: Eight-Week Dashboard
Week 1–2: theme, layout shell, auth screens. Week 3–5: core DataGrid features, filters, export. Week 6: forms and settings. Week 7: performance pass, bundle analysis. Week 8: QA, a11y audit, deploy.
MUI made weeks 3–5 possible in three weeks instead of six. That margin funded the performance pass — lazy loading, path imports, route splitting.
Best Practices Recap for Production MUI
Ship a ThemeProvider wrapper from day one — never patch colors per component later. Enforce path imports in ESLint. Lazy-load MUI X. Keep business logic out of styled components. Run axe on every major screen. Document licensed features. Split marketing routes from admin routes when bundle size matters.
These habits are what separate “we used MUI” from “we shipped a maintainable admin product.”
Conclusion
Component libraries aren’t a shortcut for thinking — they’re a foundation for standard UI patterns. MUI shines on data-dense admin tools with tight timelines. It struggles on highly branded marketing experiences.
My job is matching the library to the product surface, theming it properly, and keeping bundles disciplined.
Key Takeaways
- Use MUI for admin dashboards and internal tools with standard UI patterns
- Theme aggressively with
createThemeandstyleOverridesto avoid generic Material look - Use path imports and icon path imports to control bundle size
- Pair MUI with React Hook Form + Zod for typed, accessible forms
- Keep Tailwind and MUI on separate components or route groups — don’t mix on one element
- Lazy-load DataGrid and DatePicker; they’re heavy
- Verify MUI X licensing for commercial features before committing
- Consider Radix/shadcn for branded customer-facing apps; MUI for ops dashboards