Sep 01 2025
We’ve all been there - you’re integrating a UI library like Material-UI or Ant Design with your custom styling solution like Tailwind CSS, and the components don’t quite match your design. The quickest fix seems to be slapping some margin classes directly onto the library component. While this often works in the short term, it introduces hidden complexity that can negatively affect maintainability and flexibility later.
Suppose you want to slightly adjust the spacing of a Material-UI Button when styled with Tailwind CSS:
import { Button } from "@mui/material";
const UserInfoForm = () => {
return (
<form>
<input type="email" placeholder="Email" />
<input type="text" placeholder="Username" />
{/* Quick fix: margin-top applied directly */}
<Button className="mt-4" variant="contained" type="submit">
Save
</Button>
</form>
);
};
This looks fine initially. However, problems arise when the component is reused in different contexts:
const UserProfile = () => {
return (
<div className="profile-container">
{/* UserInfoForm enforces its own margin */}
<UserInfoForm />
{/* CarInfoForm has different spacing logic */}
<CarInfoForm />
</div>
);
};
The margin is now “baked in,” making it difficult to adapt the component across varied layouts without adding extra conditional props, which clutters the component API.
This tight coupling of external components and layout introduces maintenance overhead: replacing MUI with another library, applying different design systems, or creating reusable form patterns becomes unnecessarily complex.
External UI libraries are designed with clear boundaries:
When we blur these boundaries by applying layout styles directly to library components, two issues arise:
Put differently, it’s not just “technical debt that will bite you later” - it increases complexity every time you need to use the component in a new context.
Instead of styling the library button directly, wrap it in a container responsible for spacing:
import { Button } from "@mui/material";
const UserInfoForm = () => {
return (
<form>
<input type="email" placeholder="Email" />
<input type="text" placeholder="Username" />
{/* Properly isolating layout */}
<div className="mt-4">
<Button variant="contained" type="submit">
Save
</Button>
</div>
</form>
);
};
styleOverrides
For appearance-level customization (e.g., border radius, text casing, colors), prefer the library’s theming system:
import { createTheme, ThemeProvider } from "@mui/material/styles";
const theme = createTheme({
components: {
MuiButton: {
styleOverrides: {
root: {
textTransform: "none",
borderRadius: "8px",
},
},
},
},
});
This allows you to control visual identity globally without leaking layout rules into the component.
For specific use cases (e.g., a custom Select
with larger padding), consider wrapper components that extend the base API while encapsulating additional styles.
import { Select, MenuItem } from "@mui/material";
const SelectBigPadding = ({ children, ...props }) => {
return (
<Select {...props} className="[&_.MuiMenuItem-root]:p-4">
{children}
</Select>
);
};
// Usage:
const UserSettingsForm = () => {
return (
<SelectBigPadding value={selectedOption} onChange={handleChange}>
<MenuItem value="option1">Option 1</MenuItem>
<MenuItem value="option2">Option 2</MenuItem>
</SelectBigPadding>
);
};
In the spacing debate, padding tends to be more predictable because it avoids margin collapse. In CSS, if two adjacent elements both have vertical margins, the browser doesn’t add them together - instead, the larger of the two margins is applied. This often produces surprises when stacking components.
Padding, on the other hand, is included inside the component’s box, so two padded elements always stack consistently.
However, there is an important distinction:
Best practice:
Before (with margin in the component):
<Button className="mt-4" variant="contained" type="submit">
Save
</Button>
After (with wrapper handling layout):
<div className="mt-4">
<Button variant="contained" type="submit">
Save
</Button>
</div>
This small change significantly improves flexibility and maintainability.
That’s all. I hope you enjoyed this article. Thanks for reading!
Here’s an addon for you. Brief summary of an article. You can use it to create fiches cards (e.g. in Anki).
Flashcard 1
Front: How should you handle layout styling for external library components?
Back: Never add layout styles (margins, positioning) directly to library components. Keep layout external (via wrappers/containers) while using theming or overrides for internal styling.
Flashcard 2
Front: What’s the difference between layout and appearance in UI components?
Back: Layout determines how a component is positioned in its container (margins, alignment), while appearance controls the component’s visual details (colors, borders, typography).
Flashcard 3
Front: Why is styling external components directly with margins problematic?
Back: It couples layout with the component, reducing reusability, making redesigns harder, and increasing maintenance overhead.
Flashcard 4
Front: What is CSS margin collapse?
Back: When two vertical margins meet, they don’t add together; instead, the larger one is applied. This can cause unpredictable spacing issues.
Flashcard 5
Front: Why is padding often safer than margin for consistent spacing?
Back: Padding is internal, so values always add up predictably and don’t collapse like margins.
Flashcard 6
Front: How can you extend external library components with additional styles safely?
Back: Wrap them in custom wrapper components that preserve the original API but apply customized appearance rules (e.g., SelectBigPadding
).