en un clic
mui-to-bui-migration
// Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (BUI). Use this skill when migrating components, updating imports, replacing styling patterns, or converting MUI components to their BUI equivalents.
// Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (BUI). Use this skill when migrating components, updating imports, replacing styling patterns, or converting MUI components to their BUI equivalents.
| name | mui-to-bui-migration |
| description | Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (BUI). Use this skill when migrating components, updating imports, replacing styling patterns, or converting MUI components to their BUI equivalents. |
This skill helps migrate Backstage plugins from Material-UI (@material-ui/core, @material-ui/icons) to Backstage UI (@backstage/ui).
Before starting migration:
Install the BUI package:
yarn add @backstage/ui
Add the CSS import to your root file (typically src/index.ts or app entry point):
import '@backstage/ui/css/styles.css';
Box - Basic layout container with CSS propertiesContainer - Centered content container with max-widthGrid - CSS Grid-based layout (Grid.Root, Grid.Item)Flex - Flexbox layout componentAccordion - Collapsible content panelsAvatar - User/entity avatarsButton - Primary action buttons (variant="primary", variant="secondary", isDisabled)ButtonIcon - Icon-only buttons (icon, onPress, variant)ButtonLink - Link styled as buttonCard - Content cards (Card, CardHeader, CardBody, CardFooter)Checkbox - Checkbox inputDialog - Modal dialogs (DialogTrigger, Dialog, DialogHeader, DialogBody, DialogFooter)Header - Page headersHeaderPage - Full page header componentLink - Navigation linksMenu - Dropdown menus (MenuTrigger, Menu, MenuItem)PasswordField - Password input fieldPopover - Popover overlaysRadioGroup - Radio button groupsSearchField - Search inputSelect - Dropdown selectSkeleton - Loading skeletonSwitch - Toggle switchTable - Data tablesTabs - Tab navigation (Tabs, TabList, Tab, TabPanel)Tag - Tag/chip component (replaces MUI Chip)TagGroup - Tag/chip groupsText - Typography component (variant, color)TextField - Text input (isRequired, onChange receives string directly)ToggleButton - Toggle buttonsToggleButtonGroup - Grouped toggle buttonsTooltip - Tooltip overlays (used with TooltipTrigger from react-aria-components)VisuallyHidden - Accessibility helperuseBreakpoint - Responsive breakpoint hookRemove MUI imports:
// REMOVE these imports
import { Box, Typography, Tooltip, Paper } from '@material-ui/core';
import { makeStyles, Theme } from '@material-ui/core/styles';
import SomeIcon from '@material-ui/icons/SomeIcon';
Add BUI imports:
// ADD these imports
import { Box, Flex, Text, Tooltip, Card } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
import styles from './MyComponent.module.css';
Create a .module.css file alongside your component using BUI CSS variables.
Before (MUI makeStyles):
// MyComponent.tsx
import { makeStyles, Theme } from '@material-ui/core/styles';
const useStyles = makeStyles((theme: Theme) => ({
container: {
padding: theme.spacing(2),
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
},
title: {
marginBottom: theme.spacing(1),
color: theme.palette.text.primary,
},
listItem: {
display: 'flex',
alignItems: 'center',
},
icon: {
minWidth: 56,
color: theme.palette.text.secondary,
},
}));
function MyComponent() {
const classes = useStyles();
return (
<div className={classes.container}>
<Typography className={classes.title}>Title</Typography>
<div className={classes.listItem}>
<div className={classes.icon}><SomeIcon /></div>
<span>Content</span>
</div>
</div>
);
}
After (CSS Modules with BUI variables):
/* MyComponent.module.css */
@layer components {
.container {
padding: var(--bui-space-4);
background-color: var(--bui-bg-surface-1);
border-radius: var(--bui-radius-2);
}
.title {
margin-bottom: var(--bui-space-2);
color: var(--bui-fg-primary);
}
.listItem {
display: flex;
align-items: center;
padding: var(--bui-space-2) 0;
}
.icon {
min-width: 56px;
display: flex;
align-items: center;
justify-content: center;
color: var(--bui-fg-secondary);
}
}
// MyComponent.tsx
import { Box, Text } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
import styles from './MyComponent.module.css';
function MyComponent() {
return (
<Box className={styles.container}>
<Text className={styles.title}>Title</Text>
<div className={styles.listItem}>
<div className={styles.icon}><RiSomeIcon size={24} /></div>
<span>Content</span>
</div>
</Box>
);
}
Before (MUI Box with display prop):
<Box
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="space-between"
>
<Box display="flex" flexDirection="row" gap={2}>
{children}
</Box>
</Box>
After (BUI Flex component):
<Flex direction="column" align="center" justify="between">
<Flex direction="row" style={{ gap: 'var(--bui-space-4)' }}>
{children}
</Flex>
</Flex>
Note: BUI Flex uses justify="between" not justify="space-between".
Before (MUI Grid):
<Grid container spacing={3}>
<Grid item xs={12} md={6}>
{content}
</Grid>
</Grid>
After (BUI Grid):
<Grid.Root columns={{ sm: '12' }} gap="6">
<Grid.Item colSpan={{ sm: '12', md: '6' }}>
{content}
</Grid.Item>
</Grid.Root>
Before (MUI Typography):
<Typography variant="h1">Heading</Typography>
<Typography variant="h6">Subheading</Typography>
<Typography variant="body1">Body text</Typography>
<Typography variant="body2" color="textSecondary">Secondary text</Typography>
After (BUI Text):
<Text variant="title-large">Heading</Text>
<Text variant="title-small">Subheading</Text>
<Text variant="body-medium">Body text</Text>
<Text variant="body-small" color="secondary">Secondary text</Text>
Valid Text variants: title-large, title-medium, title-small, title-x-small, body-large, body-medium, body-small, body-x-small
Before (MUI Tooltip):
import { Tooltip, Typography } from '@material-ui/core';
<Tooltip title={<Typography>Tooltip content</Typography>}>
<span>Hover me</span>
</Tooltip>;
After (BUI TooltipTrigger pattern):
import { Tooltip, Text } from '@backstage/ui';
import { TooltipTrigger } from 'react-aria-components';
<TooltipTrigger>
<Text>Hover me</Text>
<Tooltip>Tooltip content</Tooltip>
</TooltipTrigger>;
Note: Add react-aria-components to your dependencies.
Before (MUI Dialog):
import { Dialog, DialogTitle, DialogActions, Button } from '@material-ui/core';
<Dialog open={isOpen} onClose={onClose}>
<DialogTitle>Title</DialogTitle>
<DialogActions>
<Button onClick={onClose}>Cancel</Button>
<Button onClick={onConfirm} color="primary">Confirm</Button>
</DialogActions>
</Dialog>
After (BUI Dialog):
import { Dialog, DialogTrigger, DialogHeader, DialogFooter, Button } from '@backstage/ui';
<DialogTrigger>
<Dialog isOpen={isOpen} isDismissable onOpenChange={(open) => { if (!open) onClose(); }}>
<DialogHeader>Title</DialogHeader>
<DialogFooter>
<Button onClick={onConfirm} variant="primary">Confirm</Button>
<Button onClick={onClose} variant="secondary" slot="close">Cancel</Button>
</DialogFooter>
</Dialog>
</DialogTrigger>
Before (MUI Button):
<Button variant="contained" color="primary" disabled={loading} onClick={handleClick}>
Submit
</Button>
<IconButton onClick={handleDelete} disabled={!canDelete}>
<DeleteIcon />
</IconButton>
After (BUI Button):
<Button variant="primary" isDisabled={loading} onClick={handleClick}>
Submit
</Button>
<ButtonIcon
aria-label="delete"
isDisabled={!canDelete}
onPress={handleDelete}
icon={<RiDeleteBinLine size={16} />}
variant="secondary"
/>
Before (MUI TextField):
<TextField
required
name="title"
label="Title"
value={value}
onChange={(e) => setValue(e.target.value)}
fullWidth
/>
After (BUI TextField):
<TextField
isRequired
id="title"
label="Title"
value={value}
onChange={(newValue) => setValue(newValue)} // receives string directly!
/>
Note: BUI TextField onChange receives the string value directly, not an event object.
Before (MUI Tabs):
import { Tab } from '@material-ui/core';
import { TabContext, TabList, TabPanel } from '@material-ui/lab';
<TabContext value={tab}>
<TabList onChange={handleChange}>
<Tab label="Tab 1" value="tab1" />
<Tab label="Tab 2" value="tab2" />
</TabList>
<TabPanel value="tab1">Content 1</TabPanel>
<TabPanel value="tab2">Content 2</TabPanel>
</TabContext>
After (BUI Tabs):
import { Tabs, TabList, Tab, TabPanel } from '@backstage/ui';
<Tabs defaultSelectedKey="tab1">
<TabList>
<Tab id="tab1">Tab 1</Tab>
<Tab id="tab2">Tab 2</Tab>
</TabList>
<TabPanel id="tab1">Content 1</TabPanel>
<TabPanel id="tab2">Content 2</TabPanel>
</Tabs>
Before (MUI Menu):
import { IconButton, Popover, MenuList, MenuItem } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
<IconButton onClick={handleOpen}><MoreVertIcon /></IconButton>
<Popover open={open} anchorEl={anchorEl} onClose={handleClose}>
<MenuList>
<MenuItem onClick={handleAction}>Action</MenuItem>
</MenuList>
</Popover>
After (BUI Menu):
import { ButtonIcon, Menu, MenuItem, MenuTrigger } from '@backstage/ui';
import { RiMore2Line } from '@remixicon/react';
<MenuTrigger>
<ButtonIcon aria-label="more" icon={<RiMore2Line />} variant="secondary" />
<Menu>
<MenuItem onAction={handleAction}>Action</MenuItem>
</Menu>
</MenuTrigger>
Before (MUI List):
import { List, ListItem, ListItemIcon, ListItemText } from '@material-ui/core';
<List>
<ListItem>
<ListItemIcon><SomeIcon /></ListItemIcon>
<ListItemText primary="Title" secondary="Description" />
</ListItem>
</List>
After (HTML list with BUI and CSS Modules):
/* MyList.module.css */
@layer components {
.list {
list-style: none;
padding: 0;
margin: 0;
}
.listItem {
display: flex;
align-items: flex-start;
padding: var(--bui-space-2) 0;
}
.listItemIcon {
min-width: 36px;
display: flex;
align-items: center;
color: var(--bui-fg-primary);
}
}
import { Flex, Text } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
import styles from './MyList.module.css';
<ul className={styles.list}>
<li className={styles.listItem}>
<div className={styles.listItemIcon}><RiSomeIcon size={20} /></div>
<Flex direction="column">
<Text>Title</Text>
<Text variant="body-small" color="secondary">Description</Text>
</Flex>
</li>
</ul>
Before (MUI Chip):
import { Chip } from '@material-ui/core';
<Chip label="Category" size="small" />
After (BUI Tag):
import { Tag } from '@backstage/ui';
<Tag size="small">Category</Tag>
Before (MUI Icons):
import CloseIcon from '@material-ui/icons/Close';
import SearchIcon from '@material-ui/icons/Search';
<CloseIcon />
<SearchIcon fontSize="small" />
After (Remix Icons):
import { RiCloseLine, RiSearchLine } from '@remixicon/react';
<RiCloseLine />
<RiSearchLine size={16} />
Common icon mappings:
| MUI Icon | Remix Icon |
|---|---|
Close | RiCloseLine |
Search | RiSearchLine |
Settings | RiSettingsLine |
Add | RiAddLine |
Delete | RiDeleteBinLine |
Edit | RiEditLine |
Check | RiCheckLine |
Error | RiErrorWarningLine |
Warning | RiAlertLine |
Info | RiInformationLine |
ExpandMore | RiArrowDownSLine |
ExpandLess | RiArrowUpSLine |
ChevronRight | RiArrowRightSLine |
ChevronLeft | RiArrowLeftSLine |
Menu | RiMenuLine |
MoreVert | RiMore2Line |
Visibility | RiEyeLine |
VisibilityOff | RiEyeOffLine |
NewReleases | RiMegaphoneLine |
RecordVoiceOver | RiMegaphoneLine |
Description | RiFileTextLine |
Find more icons at: https://remixicon.com/
| MUI theme.spacing() | BUI CSS Variable |
|---|---|
theme.spacing(0.5) | var(--bui-space-1) |
theme.spacing(1) | var(--bui-space-2) |
theme.spacing(1.5) | var(--bui-space-3) |
theme.spacing(2) | var(--bui-space-4) |
theme.spacing(3) | var(--bui-space-6) |
theme.spacing(4) | var(--bui-space-8) |
| MUI theme.palette | BUI CSS Variable |
|---|---|
text.primary | var(--bui-fg-primary) |
text.secondary | var(--bui-fg-secondary) |
background.paper | var(--bui-bg-surface-1) |
background.default | var(--bui-bg-surface-0) |
primary.main | var(--bui-bg-solid) or var(--bui-ring) |
error.main | var(--bui-fg-danger) |
action.hover | var(--bui-bg-hover) |
divider | var(--bui-border) |
| Property | BUI CSS Variable |
|---|---|
| Font family | var(--bui-font-regular) |
| Font size small | var(--bui-font-size-1) |
| Font size medium | var(--bui-font-size-2) |
| Font size large | var(--bui-font-size-3) |
| Font weight regular | var(--bui-font-weight-regular) |
| Font weight bold | var(--bui-font-weight-bold) |
| Property | BUI CSS Variable |
|---|---|
| Border radius small | var(--bui-radius-2) |
| Border radius medium | var(--bui-radius-3) |
| Border radius full | var(--bui-radius-full) |
| Link color | var(--bui-fg-link) |
Some Backstage APIs still require MUI-compatible icon types:
@backstage/frontend-plugin-api): The icon prop expects MUI IconComponent type. Remix icons are not type-compatible.@material-ui/lab): No BUI equivalent exists.@material-ui/lab): No BUI equivalent exists.@material-ui/lab): No BUI equivalent exists.For these cases, keep using MUI components.
When migrating a plugin:
@backstage/ui dependency@remixicon/react dependency (if using icons)react-aria-components dependency (if using Tooltip)@material-ui/core imports (except components with no BUI equivalent)@material-ui/icons importsmakeStyles and related imports.module.css files for component stylesTypography with TextBox display="flex" with FlexGrid container/item with Grid.Root/Grid.ItemPaper with CardDialog with BUI DialogTrigger patternTooltip with BUI TooltipTrigger patternTabs with BUI TabsMenu with BUI MenuTrigger patternChip with TagIconButton with ButtonIconButton props (disabled → isDisabled, variant="contained" → variant="primary")TextField props (required → isRequired, onChange signature)yarn tsc to check for type errorsyarn build to verify buildyarn lint to check for missing dependencies