بنقرة واحدة
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.
Use this skill when the user wants to migrate an existing Backstage backend plugin's hand-written Express router to the typed OpenAPI tooling. Optionally, can also add typed client generation and migrate router tests to the OpenAPI test wrapper.
Migrate a Backstage app from the old frontend system to the new one. Use this skill when converting an app to use the new extension-based frontend system, including the hybrid migration phase and the full migration of routes, sidebar, plugins, APIs, themes, and other app-level concerns.
Instrument a Backstage frontend plugin with analytics events using the Backstage Analytics API. Use this skill when adding, reviewing, or extending event capture (`captureEvent`, `AnalyticsContext`) in plugin components, deciding whether an interaction warrants an event, or writing tests for analytics behavior.
Fully migrate a Backstage plugin to the new frontend system, dropping all old system support. Use this skill for internal plugins that only need to run in a single app, or when you are ready to remove backward compatibility entirely.
Add new frontend system support to an existing Backstage plugin while keeping the old system working. Use this skill for published or shared plugins that need to work in both old and new frontend system apps.
| 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-widthFlex - Flex layout componentFullPage - Full-page layout wrapperGrid - CSS Grid-based layout (Grid.Root, Grid.Item)Accordion - Collapsible content panels (Accordion, AccordionTrigger, AccordionPanel, AccordionGroup)Alert - Alert/notification banners (status, title, description)Avatar - User/entity avatarsBadge - Inline badge/label with optional icon (size, icon)Button - Action buttons (variant="primary", variant="secondary", variant="tertiary", isDisabled, destructive, loading)ButtonIcon - Icon-only buttons (icon, onPress, variant)ButtonLink - Link styled as buttonCard - Content cards (Card, CardHeader, CardBody, CardFooter)Checkbox - Checkbox inputCheckboxGroup - Grouped checkboxes with shared label (label, orientation, isRequired)DateRangePicker - Date range input field (label, value, onChange)Dialog - Modal dialogs (DialogTrigger, Dialog, DialogHeader, DialogBody, DialogFooter)FieldLabel - Form field label with description and secondary labelHeader - Page headers with breadcrumbs and tabsLink - Navigation linksList - List component (List, ListRow)Menu - Dropdown menus (MenuTrigger, Menu, MenuItem, MenuSection, MenuSeparator, SubmenuTrigger)PasswordField - Password input fieldPluginHeader - Plugin-level header with icon, title, tabs, and actionsPopover - Popover overlaysRadioGroup - Radio button groups (RadioGroup, Radio)SearchAutocomplete - Search input with autocomplete popover (SearchAutocomplete, SearchAutocompleteItem)SearchField - Search inputSelect - Dropdown select (single and multiple selection modes)Skeleton - Loading skeletonSlider - Range slider input (label, minValue, maxValue, step)Switch - Toggle switchTable - Data tables (with useTable hook for data management)TablePagination - Standalone pagination componentTabs - Tab navigation (Tabs, TabList, Tab, TabPanel)Tag - Tag/chip component (replaces MUI Chip)TagGroup - Tag/chip groupsText - Typography component (variant, color, weight, truncate)TextField - Text input (isRequired, onChange receives string directly)ToggleButton - Toggle buttonsToggleButtonGroup - Grouped toggle buttonsTooltip - Tooltip overlays (TooltipTrigger, Tooltip — both from @backstage/ui)VisuallyHidden - Accessibility helperuseBreakpoint - Responsive breakpoint hookuseTable - Table data management hook (supports complete, offset, and cursor pagination modes)Remove 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';
makeStyles to CSS ModulesCreate 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-neutral-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>
);
}
FlexBefore (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, TooltipTrigger, Text } from '@backstage/ui';
<TooltipTrigger>
<Text>Hover me</Text>
<Tooltip>Tooltip content</Tooltip>
</TooltipTrigger>;
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 (BUI List):
import { List, ListRow } from '@backstage/ui';
import { RiSomeIcon } from '@remixicon/react';
<List>
<ListRow icon={<RiSomeIcon size={20} />} description="Description">
Title
</ListRow>
</List>;
Note: ListRow supports icon, description, menuItems, and customActions props.
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 Alert):
import { Alert, AlertTitle } from '@material-ui/lab';
<Alert severity="error">
<AlertTitle>Error</AlertTitle>
Something went wrong.
</Alert>;
After (BUI Alert):
import { Alert } from '@backstage/ui';
<Alert
status="danger"
icon
title="Error"
description="Something went wrong."
/>;
Status mapping: severity="error" → status="danger", severity="warning" → status="warning",
severity="info" → status="info", severity="success" → status="success".
Set icon to true for automatic status icons, or pass a custom ReactElement.
Use loading for a loading spinner, and customActions for action buttons.
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/
Before (MUI Paper):
import { Paper, Typography } from '@material-ui/core';
<Paper elevation={2}>
<Typography variant="h6">Title</Typography>
<Typography>Body content</Typography>
</Paper>;
After (BUI Card):
import { Card, CardHeader, CardBody, Text } from '@backstage/ui';
<Card>
<CardHeader>Title</CardHeader>
<CardBody>
<Text>Body content</Text>
</CardBody>
</Card>;
Before (MUI Select):
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
<FormControl fullWidth>
<InputLabel>Framework</InputLabel>
<Select value={value} onChange={e => setValue(e.target.value as string)}>
<MenuItem value="react">React</MenuItem>
<MenuItem value="angular">Angular</MenuItem>
</Select>
</FormControl>;
After (BUI Select):
import { Select } from '@backstage/ui';
<Select
label="Framework"
selectedKey={value}
onSelectionChange={key => setValue(key as string)}
options={[
{ value: 'react', label: 'React' },
{ value: 'angular', label: 'Angular' },
]}
/>;
Note: BUI Select accepts flat options arrays or grouped OptionSection arrays. Pass multiple for multi-select.
Before (MUI Accordion):
import {
Accordion,
AccordionSummary,
AccordionDetails,
} from '@material-ui/core';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
<Accordion>
<AccordionSummary expandIcon={<ExpandMoreIcon />}>
Section title
</AccordionSummary>
<AccordionDetails>Content goes here</AccordionDetails>
</Accordion>;
After (BUI Accordion):
import { Accordion, AccordionTrigger, AccordionPanel } from '@backstage/ui';
<Accordion>
<AccordionTrigger title="Section title" />
<AccordionPanel>Content goes here</AccordionPanel>
</Accordion>;
Use AccordionGroup to wrap multiple Accordion items and control whether multiple panels can be open simultaneously.
Before (MUI RadioGroup):
import {
FormControl,
FormLabel,
RadioGroup,
FormControlLabel,
Radio,
} from '@material-ui/core';
<FormControl>
<FormLabel>Frequency</FormLabel>
<RadioGroup value={value} onChange={e => setValue(e.target.value)}>
<FormControlLabel value="daily" control={<Radio />} label="Daily" />
<FormControlLabel value="weekly" control={<Radio />} label="Weekly" />
</RadioGroup>
</FormControl>;
After (BUI RadioGroup):
import { RadioGroup, Radio } from '@backstage/ui';
<RadioGroup label="Frequency" value={value} onChange={setValue}>
<Radio value="daily">Daily</Radio>
<Radio value="weekly">Weekly</Radio>
</RadioGroup>;
Before (MUI Badge):
import { Badge } from '@material-ui/core';
<Badge badgeContent={4} color="primary">
<MailIcon />
</Badge>;
After (BUI Badge):
import { Badge } from '@backstage/ui';
import { RiMailLine } from 'react-icons/ri';
<Badge>New</Badge>
<Badge size="small" icon={<RiMailLine size={12} />}>4</Badge>
Note: BUI Badge is a label-style badge (inline text with optional icon), not a notification counter overlay.
For notification counters overlaid on icons, use CSS positioning.
Before (MUI Slider):
import { Slider } from '@material-ui/core';
<Slider
value={value}
onChange={(_, newValue) => setValue(newValue as number)}
min={0}
max={100}
step={10}
/>;
After (BUI Slider):
import { Slider } from '@backstage/ui';
<Slider
label="Volume"
value={value}
onChange={setValue}
minValue={0}
maxValue={100}
step={10}
/>;
Note: BUI Slider onChange receives the new value directly. Use minValue/maxValue instead of min/max.
Before (MUI FormGroup with Checkboxes):
import {
FormControl,
FormLabel,
FormGroup,
FormControlLabel,
Checkbox,
} from '@material-ui/core';
<FormControl>
<FormLabel>Options</FormLabel>
<FormGroup>
<FormControlLabel
control={
<Checkbox
checked={values.a}
onChange={e => handleChange('a', e.target.checked)}
/>
}
label="Option A"
/>
<FormControlLabel
control={
<Checkbox
checked={values.b}
onChange={e => handleChange('b', e.target.checked)}
/>
}
label="Option B"
/>
</FormGroup>
</FormControl>;
After (BUI CheckboxGroup):
import { CheckboxGroup, Checkbox } from '@backstage/ui';
<CheckboxGroup label="Options" value={selected} onChange={setSelected}>
<Checkbox value="a">Option A</Checkbox>
<Checkbox value="b">Option B</Checkbox>
</CheckboxGroup>;
| 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-neutral-1) |
background.default | var(--bui-bg-app) |
primary.main | var(--bui-bg-solid) or var(--bui-ring) |
error.main | var(--bui-fg-danger) |
action.hover | var(--bui-bg-neutral-1-hover) |
divider | var(--bui-border-1) |
| 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-info) |
Some Backstage APIs still require MUI-compatible icon types:
@backstage/frontend-plugin-api): The icon param on page extensions expects an IconElement. MUI icon components can still be used via <Icon fontSize="inherit" />.@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)@material-ui/core imports (except components with no BUI equivalent)@material-ui/icons imports@material-ui/lab imports (Alert, Pagination now in BUI)makeStyles 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 pattern (both from @backstage/ui)Tabs with BUI TabsMenu/Popover with BUI MenuTrigger patternChip with TagIconButton with ButtonIconAlert with BUI AlertList with BUI List and ListRowSelect/FormControl with BUI SelectAccordion with BUI Accordion/AccordionTrigger/AccordionPanelRadioGroup/FormControlLabel with BUI RadioGroup/RadioFormGroup with BUI CheckboxGroupSlider with BUI SliderButton props (disabled → isDisabled, variant="contained" → variant="primary")TextField props (required → isRequired, onChange signature)yarn tsc to check for type errorsyarn build, yarn build:all, or yarn workspace <pkg> build) to verify buildyarn lint to check for missing dependencies